GeckoView Save to PDF ===================== Olivia Hall , Jonathan Almeida Why --- - The Save to PDF feature was originally available in Fennec and users would like to see the return of this feature. There are a lot of user requests for Save to PDF in Fenix. - We would have more parity with Desktop, and be able to share the same underlying implementation with them. - Product is currently evaluating the addition of pdf.js as well; having Save to PDF would be an added bonus. Goals ----- - Save the current page to a text-based PDF document. - Embedders should also be able to call into GeckoView to provide a PDF copy of the selected GeckoSession. - Enable the ability to iterate on PDF customizations. Non-Goals --------- - We do not want to implement a PDF “preview” of the document prior to the download. This has open questions: does Product want this, should this be implemented by the embedder, etc. - The generated PDF should not match the theme (e.g., light or dark mode) of the currently displayed page - the PDF will always appear as themeless or as a plain document. - No customizable settings. The current API design will not include customization settings that the embedder can control. This can be worked on in a follow-up feature request. Our current API design however, would enable for these particular iterations. What ---- This work will add a method to ``GeckoSession`` called ``savePdf`` for embedders to use, which will communicate with a new ``GeckoViewPdf.sys.mjs`` to create the PDF file. When the document is available, the ``GeckoViewPdfController`` will notify the ``ContentDelegate.onExternalResponse`` with the downloadable document. - ``GeckoViewPdf.sys.mjs`` - JavaScript implementation that converts the content to a PDF and saves the file, also responds to messaging from ``GeckoViewPdfController``. - ``GeckoViewPdfController.java`` - The Controller coordinates between the Java and JS through response messaging and notifies the content delegate when the PDF is available for use. API --- GeckoSession.java ^^^^^^^^^^^^^^^^^ .. code:: java public class GeckoSession { public GeckoSession(final @Nullable GeckoSessionSettings settings) { mPdfController = new PdfController(this); } @UiThread public void saveAsPdf(PdfSettings settings) { mPdfController.savePdf(null); } } GeckoViewPdf.sys.mjs ^^^^^^^^^^^^^^^^^^^^ .. code:: java this.registerListener([ "GeckoView:SavePdf", ]); async onEvent(aEvent, aData, aCallback) { debug`onEvent: event=${aEvent}, data=${aData}`; switch (aEvent) { case "GeckoView:SavePdf": this.saveToPDF(); Break; } } } async saveToPDF() { // Reference: https://searchfox.org/mozilla-central/source/remote/cdp/domains/parent/Page.sys.mjs#519 } GeckoViewPdfController.java ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code:: java class PdfController { private static final String LOGTAG = "PdfController"; private final GeckoSession mSession; PdfController(final GeckoSession session) { mSession = session; } private PdfDelegate mDelegate; private BundleEventListener mEventListener; /* package */ PdfController() { mEventListener = new EventListener(); EventDispatcher.getInstance() .registerUiThreadListener(mEventListener,"GeckoView:PdfSaved"); } @UiThread public void setDelegate(final @Nullable PdfDelegate delegate) { ThreadUtils.assertOnUiThread(); mDelegate = delegate; } @UiThread @Nullable public PdfDelegate getDelegate() { ThreadUtils.assertOnUiThread(); return mDelegate; } @UiThread public void savePdf() { ThreadUtils.assertOnUiThread(); mEventDispatcher.dispatch("GeckoView:SavePdf", null); } private class EventListener implements BundleEventListener { @Override public void handleMessage( final String event, final GeckoBundle message, final EventCallback callback ) { if (mDelegate == null) { callback.sendError("Not allowed"); return; } switch (event) { case "GeckoView:PdfSaved": { final ContentDelegate delegate = mSession.getContentDelegate(); if (message.containsKey("pdfPath")) { InputStream inputStream; /* construct InputStream from local file path */ WebResponse response = WebResponse.Builder() .body(inputStream) // Add other attributes as well. .build(); if (delegate != null) { delegate.onExternalResponse(mSession, response); } else { throw Exception("Needs ContentDelegate for this to work.") } } break; } } } } } geckoview.js ^^^^^^^^^^^^ .. code:: java { name: "GeckoViewPdf", onInit: { resource: "resource://gre/modules/GeckoViewPdf.sys.mjs", } } Testing ------- - Tests for the sys.mjs and java code will be covered by mochitests and junit. - Make assertions to check that the text and images are in the finished PDF; the PDF is a non-zero file size. Risks ----- The API and the code that this work would be using are pretty new, currently pref'd off in Nightly and could contain implementation bugs.