Skip to content

Modifying Firefox to Save PDF files automagically to MemoryCache

katetaylormoz edited this page Feb 6, 2024 · 2 revisions

Modifying Firefox to Save PDF files automagically to MemoryCache by building from source.

What we're doing

If the pageSettings.silentMode parameter is set to true when calling saveAsPDF, we are bypassing opening up the file browser and asking the user to input a name and location for the file. This allows the extension to specify the subdirectory in the Downloads path, and then we use the print service to save there.

But why?

There's another API (downloads.download()) that essentially performs this task, but it requires a file, rather than a URL, while the saveAsPDF functionality can perform the HTML -> PDF conversion process. I chose to start with this approach since it was manageable for me, but in practice, there are considerations for implementing something like this at scale.


How to modify Firefox to use Memory Cache

  1. Follow the instructions from Mozilla Central to acquire a copy of the Firefox source code and build it. I'm using Ubuntu as my primary operating system, but you can build Firefox on Windows and MacOS.

  2. Modify tabs.json to add a new property in the pageSettings JSON properties:

 "silentMode" : {
            "type" : "boolean", 
            "optional" : true, 
            "description" : "Whether to silently save the PDF or open the file picker. Default: false"
          }

For me, this was at line 408 after the property footerRight was declared.

  1. Modify Parent / ext-tabs.js, replacing the code for the saveAsPDF API with the code that follows:
 saveAsPDF(pageSettings) {
          let activeTab = getTabOrActive(null);
          console.log ("Silent mode: " + pageSettings.silentMode);

          let filename;
          if (
            pageSettings.toFileName !== null &&
            pageSettings.toFileName != ""
          ) {
            filename = pageSettings.toFileName;
          } else if (activeTab.linkedBrowser.contentTitle != "") {
            filename = activeTab.linkedBrowser.contentTitle;
          } else {
            let url = new URL(activeTab.linkedBrowser.currentURI.spec);
            let path = decodeURIComponent(url.pathname);
            path = path.replace(/\/$/, "");
            filename = path.split("/").pop();
            if (filename == "") {
              filename = url.hostname;
            }
          }
          //filename = DownloadPaths.sanitize(filename);

          if(!pageSettings.silentMode) {
            
            let picker = Cc["@mozilla.org/filepicker;1"].createInstance(
              Ci.nsIFilePicker
            );
            let title = strBundle.GetStringFromName(
              "saveaspdf.saveasdialog.title"
            );

            picker.init(activeTab.ownerGlobal, title, Ci.nsIFilePicker.modeSave);
            picker.appendFilter("PDF", "*.pdf");
            picker.defaultExtension = "pdf";
            picker.defaultString = filename;
  
            return new Promise(resolve => {
              picker.open(function (retval) {
                if (retval == 0 || retval == 2) {
                  // OK clicked (retval == 0) or replace confirmed (retval == 2)
  
                  // Workaround: When trying to replace an existing file that is open in another application (i.e. a locked file),
                  // the print progress listener is never called. This workaround ensures that a correct status is always returned.
                  try {
                    let fstream = Cc[
                      "@mozilla.org/network/file-output-stream;1"
                    ].createInstance(Ci.nsIFileOutputStream);
                    fstream.init(picker.file, 0x2a, 0o666, 0); // ioflags = write|create|truncate, file permissions = rw-rw-rw-
                    fstream.close();
                  } catch (e) {
                    resolve(retval == 0 ? "not_saved" : "not_replaced");
                    return;
                  }
  
                  let psService = Cc[
                    "@mozilla.org/gfx/printsettings-service;1"
                  ].getService(Ci.nsIPrintSettingsService);
                  let printSettings = psService.createNewPrintSettings();
  
                  printSettings.printerName = "";
                  printSettings.isInitializedFromPrinter = true;
                  printSettings.isInitializedFromPrefs = true;
  
                  printSettings.outputDestination =
                    Ci.nsIPrintSettings.kOutputDestinationFile;
                  printSettings.toFileName = picker.file.path;
                  
                  printSettings.printSilent = true;
  
                  printSettings.outputFormat =
                    Ci.nsIPrintSettings.kOutputFormatPDF;
  
                  if (pageSettings.paperSizeUnit !== null) {
                    printSettings.paperSizeUnit = pageSettings.paperSizeUnit;
                  }
                  if (pageSettings.paperWidth !== null) {
                    printSettings.paperWidth = pageSettings.paperWidth;
                  }
                  if (pageSettings.paperHeight !== null) {
                    printSettings.paperHeight = pageSettings.paperHeight;
                  }
                  if (pageSettings.orientation !== null) {
                    printSettings.orientation = pageSettings.orientation;
                  }
                  if (pageSettings.scaling !== null) {
                    printSettings.scaling = pageSettings.scaling;
                  }
                  if (pageSettings.shrinkToFit !== null) {
                    printSettings.shrinkToFit = pageSettings.shrinkToFit;
                  }
                  if (pageSettings.showBackgroundColors !== null) {
                    printSettings.printBGColors =
                      pageSettings.showBackgroundColors;
                  }
                  if (pageSettings.showBackgroundImages !== null) {
                    printSettings.printBGImages =
                      pageSettings.showBackgroundImages;
                  }
                  if (pageSettings.edgeLeft !== null) {
                    printSettings.edgeLeft = pageSettings.edgeLeft;
                  }
                  if (pageSettings.edgeRight !== null) {
                    printSettings.edgeRight = pageSettings.edgeRight;
                  }
                  if (pageSettings.edgeTop !== null) {
                    printSettings.edgeTop = pageSettings.edgeTop;
                  }
                  if (pageSettings.edgeBottom !== null) {
                    printSettings.edgeBottom = pageSettings.edgeBottom;
                  }
                  if (pageSettings.marginLeft !== null) {
                    printSettings.marginLeft = pageSettings.marginLeft;
                  }
                  if (pageSettings.marginRight !== null) {
                    printSettings.marginRight = pageSettings.marginRight;
                  }
                  if (pageSettings.marginTop !== null) {
                    printSettings.marginTop = pageSettings.marginTop;
                  }
                  if (pageSettings.marginBottom !== null) {
                    printSettings.marginBottom = pageSettings.marginBottom;
                  }
                  if (pageSettings.headerLeft !== null) {
                    printSettings.headerStrLeft = pageSettings.headerLeft;
                  }
                  if (pageSettings.headerCenter !== null) {
                    printSettings.headerStrCenter = pageSettings.headerCenter;
                  }
                  if (pageSettings.headerRight !== null) {
                    printSettings.headerStrRight = pageSettings.headerRight;
                  }
                  if (pageSettings.footerLeft !== null) {
                    printSettings.footerStrLeft = pageSettings.footerLeft;
                  }
                  if (pageSettings.footerCenter !== null) {
                    printSettings.footerStrCenter = pageSettings.footerCenter;
                  }
                  if (pageSettings.footerRight !== null) {
                    printSettings.footerStrRight = pageSettings.footerRight;
                  }
  
                  activeTab.linkedBrowser.browsingContext
                    .print(printSettings)
                    .then(() => resolve(retval == 0 ? "saved" : "replaced"))
                    .catch(() =>
                      resolve(retval == 0 ? "not_saved" : "not_replaced")
                    );
                } else {
                  // Cancel clicked (retval == 1)
                  resolve("canceled");
                }
              });
            });
          }
          else {
            return new Promise(resolve => {
              let psService = Cc[
                "@mozilla.org/gfx/printsettings-service;1"
              ].getService(Ci.nsIPrintSettingsService);
              let printSettings = psService.createNewPrintSettings();

              printSettings.printerName = "";
              printSettings.isInitializedFromPrinter = true;
              printSettings.isInitializedFromPrefs = true;
              
              printSettings.outputDestination = Ci.nsIPrintSettings.kOutputDestinationFile;
              
              // For now, limit to downloads directory
              printSettings.toFileName = "~/Downloads" + filename;
              
              printSettings.printSilent = true;
              
              printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
              activeTab.linkedBrowser.browsingContext
              .print(printSettings)
              .then(() => resolve(retval == 0 ? "saved" : "replaced"))
              .catch(() =>
                resolve("error with silent printing")); 
            }) 
          }
        }

Rebuild an incremental build.