# GeckoView Save to PDF

Olivia Hall \<<mailto:ohall@mozilla.com>>, Jonathan Almeida \<<mailto: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

```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

```javascript
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: :searchfox:`remote/cdp/domains/parent/Page.sys.mjs#519`
}
```

### GeckoViewPdfController.java

```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

```javascript
{
  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.
