Source code
Revision control
Copy as Markdown
Other Tools
GeckoView Save to PDF
=====================
Olivia Hall <ohall@mozilla.com>, Jonathan Almeida <jon@mozilla.com>
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: {
}
}
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.