flypig.co.uk

List items

Items from the current list are shown below.

Gecko

1 Jan 2024 : Day 125 #
It's 2024! Happy New Year to everyone. The start of the year is all about tidying up the PDF printing code for me, following the final steps to get things working yesterday. Since everything is now working, this is my chance to break things by making my changes cleaner, attempting to fix any edge cases and moving code around so there are more changes to Sailfish code and fewer changes to gecko-dev code.

Back on Day 99 when we first started looking at the problem I reverted an upstream change that removed a JavaScript class called DownloadPDFSaver. That is, the upstream change removed it, then when I reverted the change it put it back.

But it doesn't look like there's much in DownloadPDFSaver that's actually dependent on being inside gecko-dev, so it would make sense to move it into the embedlite-embedding JavaScript codebase instead. The embedlite-embedding code is specific to Sailfish OS and so entirely under our control. If I can move the changes there it will not only make things cleaner by avoiding a patch to the upstream code, it will also make the code much simpler to maintain for the future.

So my plan is to move everything related to DownloadPDFSaver into the EmbedliteDownloadManager.js file, which is the place where we actually need to make use of it.

So I've copied over the class prototype code with zero changes. I won't show it all here because there's quite a lot of it, but here's what the start of it looks like along with its docstring:
/**
 * This DownloadSaver type creates a PDF file from the current document in a
 * given window, specified using the windowRef property of the DownloadSource
 * object associated with the download.
 *
 * In order to prevent the download from saving a different document than the one
 * originally loaded in the window, any attempt to restart the download will fail.
 *
 * Since this DownloadSaver type requires a live document as a source, it cannot
 * be persisted across sessions, unless the download already succeeded.
 */
DownloadPDFSaver.prototype = {
  __proto__: DownloadSaver.prototype,
[...]
This code essentially allows printing to happen but with a DownloadSaver interface which means we can hook it into our "downloads" code really easily.

Now previously we'd have called Downloads.createDownload() with the saver key set to "pdf". The original code that did this looked like this:
            if (Services.ww.activeWindow) {
              (async function() {
                let list = await Downloads.getList(Downloads.ALL);
                let download = await Downloads.createDownload({
                  source: Services.ww.activeWindow,
                  target: data.to,
                  saver: "pdf",
                  contentType: "application/pdf"
                });
                download["saveAsPdf"] = true;
                download.start();
                list.add(download);
              })().then(null, Cu.reportError);
You can see the saver: "pdf" entry in there. This was consumed by the Downloads class from gecko-dev which handed it off to the DownloadCore class. However the upstream change carefully excised all of the functionality that handled this PDF code. All of the other download types were maintained, only the PDF code was removed.

So while previously I reverted the changes I've now created a new method inside EmbedliteDownloadManager.js in the aim of achieving the same thing and that looks like this:
/**
 * Creates a new DownloadPDFSaver object, with its initial state derived from
 * the provided properties.
 *
 * @param aProperties
 *        Provides the initial properties for the newly created download.
 *        This matches the serializable representation of a Download object.
 *        Some of the most common properties in this object include:
 *        {
 *          source: An object providing a Ci.nsIDOMWindow interface.
 *          target: String containing the path of the target file.
 *        }
 *
 * @return The newly created DownloadPDFSaver object.
 */
DownloadPDFSaver.createDownload = async function(aProperties) {
  let download = await Downloads.createDownload({
    source: aProperties.source,
    target: aProperties.target,
    contentType: "application/pdf"
  });

  download.saver = new DownloadPDFSaver();
  download.saver.download = download;
  download["saveAsPdf"] = true;

  return download;
};
Note that DownloadPDFSaver is now in the same file, so can be used here just fine. This code will end up creating the structure that we talked about yesterday; the end result should look like this:
download = Download
{
	source: {
	    url: Services.ww.activeWindow.location.href,
	    isPrivate: PrivateBrowsingUtils.isContentWindowPrivate(
	        Services.ww.activeWindow
	    ),
	    windowRef: Cu.getWeakReference(Services.ww.activeWindow),
	}
	target: {
	    path: data.to,
	}
	saver: DownloadPDFSaver(
	    download: download,
	),
	contentType: "application/pdf",
	saveAsPdf: true,
}
Some of the functionality is hidden inside the existing call to Downloads.createDownload() which creates a basic Download object that we then add our stuff to. If you look carefully you should be able to match up the code shown in our new DownloadPDFSaver.createDownload() method with the elements of the structure shown above.

With all this in place we can now change our code that creates the download like this:
@@ -254,13 +269,10 @@ EmbedliteDownloadManager.prototype = {
             if (Services.ww.activeWindow) {
               (async function() {
                 let list = await Downloads.getList(Downloads.ALL);
-                let download = await Downloads.createDownload({
+                let download = await DownloadPDFSaver.createDownload({
                   source: Services.ww.activeWindow,
-                  target: data.to,
-                  saver: "pdf",
-                  contentType: "application/pdf"
+                  target: data.to
                 });
-                download["saveAsPdf"] = true;
                 download.start();
                 list.add(download);
               })().then(null, Cu.reportError);
To give our final version, which looks like this:
          case "saveAsPdf":
            if (Services.ww.activeWindow) {
              (async function() {
                let list = await Downloads.getList(Downloads.ALL);
                let download = await DownloadPDFSaver.createDownload({
                  source: Services.ww.activeWindow,
                  target: data.to
                });
                download.start();
                list.add(download);
              })().then(null, Cu.reportError);
            } else {
              Logger.warn("No active window to print to pdf")
            }
            break;
Nice! But when I try to execute this code I hit several errors. Various classes and objects that were available in the DownloadCore.jsm file are no longer available here: DownloadSaver, DownloadError, OS and gPrintSettingsService.

Thankfully we can still pull these into our EmbedliteDownloadManager.js code to make use of them. I've added the following at the top of the file for this:
const { DownloadSaver, DownloadError } = ChromeUtils.import(
  "resource://gre/modules/DownloadCore.jsm"
);

XPCOMUtils.defineLazyModuleGetters(this, {
  OS: "resource://gre/modules/osfile.jsm",
});

XPCOMUtils.defineLazyServiceGetter(
  this,
  "gPrintSettingsService",
  "@mozilla.org/gfx/printsettings-service;1",
  Ci.nsIPrintSettingsService
);
Now when I try to print everything works just as before. Lovely! It remains to revert the revert applied previously and to commit these changes to my working tree.

There is also now just one last task I need to do to round off this PDF print work and that's to "Record an issue for the setBrowserCover() hang." This is probably the easiest task of the lot, which is why I've left it until last. But that'll be a task for tomorrow.

Once this last step is done I can finally move on to something unrelated to the printing stack, which will be a relief!

If you'd like to read any of my other gecko diary entries, they're all available on my Gecko-dev Diary page.

Comments

Uncover Disqus comments