save-to-pdf.rst (5486B)
1 GeckoView Save to PDF 2 ===================== 3 4 Olivia Hall <ohall@mozilla.com>, Jonathan Almeida <jon@mozilla.com> 5 6 Why 7 --- 8 9 - The Save to PDF feature was originally available in Fennec and users would 10 like to see the return of this feature. There are a lot of user requests for 11 Save to PDF in Fenix. 12 - We would have more parity with Desktop, and be able to share the same 13 underlying implementation with them. 14 - Product is currently evaluating the addition of pdf.js as well; having Save 15 to PDF would be an added bonus. 16 17 Goals 18 ----- 19 20 - Save the current page to a text-based PDF document. 21 - Embedders should also be able to call into GeckoView to provide a PDF copy of 22 the selected GeckoSession. 23 - Enable the ability to iterate on PDF customizations. 24 25 Non-Goals 26 --------- 27 28 - We do not want to implement a PDF “preview” of the document prior to the 29 download. This has open questions: does Product want this, should this be 30 implemented by the embedder, etc. 31 - The generated PDF should not match the theme (e.g., light or dark mode) of 32 the currently displayed page - the PDF will always appear as themeless or as 33 a plain document. 34 - No customizable settings. The current API design will not include 35 customization settings that the embedder can control. This can be worked on 36 in a follow-up feature request. Our current API design however, would enable 37 for these particular iterations. 38 39 What 40 ---- 41 42 This work will add a method to ``GeckoSession`` called ``savePdf`` for 43 embedders to use, which will communicate with a new ``GeckoViewPdf.sys.mjs`` to 44 create the PDF file. When the document is available, the 45 ``GeckoViewPdfController`` will notify the 46 ``ContentDelegate.onExternalResponse`` with the downloadable document. 47 48 - ``GeckoViewPdf.sys.mjs`` - JavaScript implementation that converts the content to 49 a PDF and saves the file, also responds to messaging from 50 ``GeckoViewPdfController``. 51 - ``GeckoViewPdfController.java`` - The Controller coordinates between the Java 52 and JS through response messaging and notifies the content delegate when the 53 PDF is available for use. 54 55 API 56 --- 57 58 GeckoSession.java 59 ^^^^^^^^^^^^^^^^^ 60 61 .. code:: java 62 63 public class GeckoSession { 64 public GeckoSession(final @Nullable GeckoSessionSettings settings) { 65 mPdfController = new PdfController(this); 66 } 67 68 @UiThread 69 public void saveAsPdf(PdfSettings settings) { 70 mPdfController.savePdf(null); 71 } 72 } 73 74 75 GeckoViewPdf.sys.mjs 76 ^^^^^^^^^^^^^^^^^^^^ 77 .. code:: java 78 79 this.registerListener([ 80 "GeckoView:SavePdf", 81 ]); 82 83 async onEvent(aEvent, aData, aCallback) { 84 debug`onEvent: event=${aEvent}, data=${aData}`; 85 86 switch (aEvent) { 87 case "GeckoView:SavePdf": 88 this.saveToPDF(); 89 Break; 90 } 91 } 92 } 93 94 async saveToPDF() { 95 // Reference: https://searchfox.org/mozilla-central/source/remote/cdp/domains/parent/Page.sys.mjs#519 96 } 97 98 99 GeckoViewPdfController.java 100 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 101 .. code:: java 102 103 class PdfController { 104 private static final String LOGTAG = "PdfController"; 105 private final GeckoSession mSession; 106 107 PdfController(final GeckoSession session) { 108 mSession = session; 109 } 110 111 private PdfDelegate mDelegate; 112 private BundleEventListener mEventListener; 113 114 /* package */ 115 PdfController() { 116 mEventListener = new EventListener(); 117 EventDispatcher.getInstance() 118 .registerUiThreadListener(mEventListener,"GeckoView:PdfSaved"); 119 } 120 121 @UiThread 122 public void setDelegate(final @Nullable PdfDelegate delegate) { 123 ThreadUtils.assertOnUiThread(); 124 mDelegate = delegate; 125 } 126 127 @UiThread 128 @Nullable 129 public PdfDelegate getDelegate() { 130 ThreadUtils.assertOnUiThread(); 131 return mDelegate; 132 } 133 134 @UiThread 135 public void savePdf() { 136 ThreadUtils.assertOnUiThread(); 137 mEventDispatcher.dispatch("GeckoView:SavePdf", null); 138 } 139 140 141 private class EventListener implements BundleEventListener { 142 143 @Override 144 public void handleMessage( 145 final String event, 146 final GeckoBundle message, 147 final EventCallback callback 148 ) { 149 if (mDelegate == null) { 150 callback.sendError("Not allowed"); 151 return; 152 } 153 154 switch (event) { 155 case "GeckoView:PdfSaved": { 156 final ContentDelegate delegate = mSession.getContentDelegate(); 157 158 if (message.containsKey("pdfPath")) { 159 InputStream inputStream; /* construct InputStream from local file path */ 160 WebResponse response = WebResponse.Builder() 161 .body(inputStream) 162 // Add other attributes as well. 163 .build(); 164 165 if (delegate != null) { 166 delegate.onExternalResponse(mSession, response); 167 } else { 168 throw Exception("Needs ContentDelegate for this to work.") 169 } 170 } 171 172 break; 173 } 174 } 175 } 176 } 177 } 178 179 geckoview.js 180 ^^^^^^^^^^^^ 181 .. code:: java 182 183 { 184 name: "GeckoViewPdf", 185 onInit: { 186 resource: "resource://gre/modules/GeckoViewPdf.sys.mjs", 187 } 188 } 189 190 191 Testing 192 ------- 193 194 - Tests for the sys.mjs and java code will be covered by mochitests and junit. 195 - Make assertions to check that the text and images are in the finished PDF; 196 the PDF is a non-zero file size. 197 198 Risks 199 ----- 200 201 The API and the code that this work would be using are pretty new, currently 202 pref'd off in Nightly and could contain implementation bugs.