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 fromGeckoViewPdfController
.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
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
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
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
{
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.