GeckoView 保存为 PDF

Olivia Hall <ohall@mozilla.com>,Jonathan Almeida <jon@mozilla.com>

原因

  • 保存为 PDF 功能最初在 Fennec 中可用,用户希望看到此功能的回归。Fenix 中有很多用户请求保存为 PDF 的功能。

  • 我们将与桌面版有更多的一致性,并能够与它们共享相同的底层实现。

  • 产品目前也在评估添加 pdf.js;拥有保存为 PDF 功能将是一个额外的奖励。

目标

  • 将当前页面保存为基于文本的 PDF 文档。

  • 嵌入器还应该能够调用 GeckoView 以提供所选 GeckoSession 的 PDF 副本。

  • 启用迭代 PDF 自定义的功能。

非目标

  • 我们不想在下载之前实现文档的 PDF“预览”。这有一些悬而未决的问题:产品是否需要这个,是否应该由嵌入器实现等等。

  • 生成的 PDF 不应与当前显示页面的主题(例如,浅色或深色模式)匹配 - PDF 将始终显示为无主题或纯文档。

  • 没有可自定义的设置。当前的 API 设计不包含嵌入器可以控制的自定义设置。这可以在后续的功能请求中进行处理。但是,我们当前的 API 设计将支持这些特定迭代。

内容

这项工作将在 GeckoSession 中添加一个名为 savePdf 的方法供嵌入器使用,该方法将与新的 GeckoViewPdf.sys.mjs 通信以创建 PDF 文件。当文档可用时,GeckoViewPdfController 将使用可下载的文档通知 ContentDelegate.onExternalResponse

  • GeckoViewPdf.sys.mjs - 将内容转换为 PDF 并保存文件的 JavaScript 实现,还响应来自 GeckoViewPdfController 的消息。

  • GeckoViewPdfController.java - 控制器通过响应消息协调 Java 和 JS 之间的通信,并在 PDF 可供使用时通知内容委托。

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",
  }
}

测试

  • sys.mjs 和 java 代码的测试将由 mochitests 和 junit 覆盖。

  • 进行断言以检查文本和图像是否在完成的 PDF 中;PDF 的文件大小不为零。

风险

这项工作将使用到的 API 和代码都比较新,目前在 Nightly 中处于首选项关闭状态,可能包含实现错误。