From 36ce3e954679c93740e0e6aa9006df7d45df39e0 Mon Sep 17 00:00:00 2001 From: Milan Burda Date: Mon, 11 Feb 2019 20:20:04 +0100 Subject: [PATCH] feat: promisify webContents.printToPDF() (#16795) --- atom/browser/api/atom_api_web_contents.cc | 9 +-- atom/browser/api/atom_api_web_contents.h | 4 +- .../printing/print_preview_message_handler.cc | 71 +++++++++++-------- .../printing/print_preview_message_handler.h | 18 ++--- docs/api/promisification.md | 4 +- docs/api/web-contents.md | 19 +++++ docs/api/webview-tag.md | 18 +++++ lib/browser/api/web-contents.js | 15 ++-- lib/common/web-view-methods.js | 6 +- lib/renderer/web-view/web-view-impl.js | 4 +- spec/api-web-contents-spec.js | 17 ++++- spec/webview-spec.js | 33 +++++++++ 12 files changed, 161 insertions(+), 57 deletions(-) diff --git a/atom/browser/api/atom_api_web_contents.cc b/atom/browser/api/atom_api_web_contents.cc index bb77450ab4abe..fb4e4cfe3efe8 100644 --- a/atom/browser/api/atom_api_web_contents.cc +++ b/atom/browser/api/atom_api_web_contents.cc @@ -1499,11 +1499,12 @@ std::vector WebContents::GetPrinterList() { return printers; } -void WebContents::PrintToPDF( - const base::DictionaryValue& settings, - const PrintPreviewMessageHandler::PrintToPDFCallback& callback) { +v8::Local WebContents::PrintToPDF( + const base::DictionaryValue& settings) { + scoped_refptr promise = new util::Promise(isolate()); PrintPreviewMessageHandler::FromWebContents(web_contents()) - ->PrintToPDF(settings, callback); + ->PrintToPDF(settings, promise); + return promise->GetHandle(); } #endif diff --git a/atom/browser/api/atom_api_web_contents.h b/atom/browser/api/atom_api_web_contents.h index af8d5aa25669b..fae25a556233a 100644 --- a/atom/browser/api/atom_api_web_contents.h +++ b/atom/browser/api/atom_api_web_contents.h @@ -174,9 +174,7 @@ class WebContents : public mate::TrackableObject, void Print(mate::Arguments* args); std::vector GetPrinterList(); // Print current page as PDF. - void PrintToPDF( - const base::DictionaryValue& settings, - const PrintPreviewMessageHandler::PrintToPDFCallback& callback); + v8::Local PrintToPDF(const base::DictionaryValue& settings); #endif // DevTools workspace api. diff --git a/atom/browser/printing/print_preview_message_handler.cc b/atom/browser/printing/print_preview_message_handler.cc index 27779f4cb3de8..bed50b2bc32f6 100644 --- a/atom/browser/printing/print_preview_message_handler.cc +++ b/atom/browser/printing/print_preview_message_handler.cc @@ -89,7 +89,7 @@ void PrintPreviewMessageHandler::OnMetafileReadyForPrinting( const PrintHostMsg_DidPrintContent_Params& content = params.content; if (!content.metafile_data_region.IsValid() || params.expected_pages_count <= 0) { - RunPrintToPDFCallback(ids.request_id, nullptr); + RejectPromise(ids.request_id); return; } @@ -102,10 +102,9 @@ void PrintPreviewMessageHandler::OnMetafileReadyForPrinting( base::BindOnce(&PrintPreviewMessageHandler::OnCompositePdfDocumentDone, weak_ptr_factory_.GetWeakPtr(), ids)); } else { - RunPrintToPDFCallback( - ids.request_id, - base::RefCountedSharedMemoryMapping::CreateFromWholeRegion( - content.metafile_data_region)); + ResolvePromise(ids.request_id, + base::RefCountedSharedMemoryMapping::CreateFromWholeRegion( + content.metafile_data_region)); } } @@ -117,11 +116,11 @@ void PrintPreviewMessageHandler::OnCompositePdfDocumentDone( if (status != printing::mojom::PdfCompositor::Status::SUCCESS) { DLOG(ERROR) << "Compositing pdf failed with error " << status; - RunPrintToPDFCallback(ids.request_id, nullptr); + RejectPromise(ids.request_id); return; } - RunPrintToPDFCallback( + ResolvePromise( ids.request_id, base::RefCountedSharedMemoryMapping::CreateFromWholeRegion(region)); } @@ -131,7 +130,7 @@ void PrintPreviewMessageHandler::OnPrintPreviewFailed( const PrintHostMsg_PreviewIds& ids) { StopWorker(document_cookie); - RunPrintToPDFCallback(ids.request_id, nullptr); + RejectPromise(ids.request_id); } void PrintPreviewMessageHandler::OnPrintPreviewCancelled( @@ -139,15 +138,15 @@ void PrintPreviewMessageHandler::OnPrintPreviewCancelled( const PrintHostMsg_PreviewIds& ids) { StopWorker(document_cookie); - RunPrintToPDFCallback(ids.request_id, nullptr); + RejectPromise(ids.request_id); } void PrintPreviewMessageHandler::PrintToPDF( const base::DictionaryValue& options, - const PrintToPDFCallback& callback) { + scoped_refptr promise) { int request_id; options.GetInteger(printing::kPreviewRequestID, &request_id); - print_to_pdf_callback_map_[request_id] = callback; + promise_map_[request_id] = std::move(promise); auto* focused_frame = web_contents()->GetFocusedFrame(); auto* rfh = focused_frame && focused_frame->HasSelection() @@ -156,28 +155,44 @@ void PrintPreviewMessageHandler::PrintToPDF( rfh->Send(new PrintMsg_PrintPreview(rfh->GetRoutingID(), options)); } -void PrintPreviewMessageHandler::RunPrintToPDFCallback( +scoped_refptr PrintPreviewMessageHandler::GetPromise( + int request_id) { + auto it = promise_map_.find(request_id); + DCHECK(it != promise_map_.end()); + + auto promise = it->second; + promise_map_.erase(it); + + return promise; +} + +void PrintPreviewMessageHandler::ResolvePromise( int request_id, scoped_refptr data_bytes) { DCHECK_CURRENTLY_ON(BrowserThread::UI); - v8::Isolate* isolate = v8::Isolate::GetCurrent(); - v8::Locker locker(isolate); + auto promise = GetPromise(request_id); + + v8::Isolate* isolate = promise->isolate(); + mate::Locker locker(isolate); v8::HandleScope handle_scope(isolate); - if (data_bytes && data_bytes->size()) { - v8::Local buffer = - node::Buffer::Copy(isolate, - reinterpret_cast(data_bytes->front()), - data_bytes->size()) - .ToLocalChecked(); - print_to_pdf_callback_map_[request_id].Run(v8::Null(isolate), buffer); - } else { - v8::Local error_message = - v8::String::NewFromUtf8(isolate, "Failed to generate PDF"); - print_to_pdf_callback_map_[request_id].Run( - v8::Exception::Error(error_message), v8::Null(isolate)); - } - print_to_pdf_callback_map_.erase(request_id); + v8::Context::Scope context_scope( + v8::Local::New(isolate, promise->GetContext())); + + v8::Local buffer = + node::Buffer::Copy(isolate, + reinterpret_cast(data_bytes->front()), + data_bytes->size()) + .ToLocalChecked(); + + promise->Resolve(buffer); +} + +void PrintPreviewMessageHandler::RejectPromise(int request_id) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + auto promise = GetPromise(request_id); + promise->RejectWithErrorMessage("Failed to generate PDF"); } } // namespace atom diff --git a/atom/browser/printing/print_preview_message_handler.h b/atom/browser/printing/print_preview_message_handler.h index e1ba677843492..4e2a80ee9f34b 100644 --- a/atom/browser/printing/print_preview_message_handler.h +++ b/atom/browser/printing/print_preview_message_handler.h @@ -7,6 +7,7 @@ #include +#include "atom/common/promise_util.h" #include "base/memory/ref_counted_memory.h" #include "base/memory/weak_ptr.h" #include "components/services/pdf_compositor/public/interfaces/pdf_compositor.mojom.h" @@ -28,13 +29,10 @@ class PrintPreviewMessageHandler : public content::WebContentsObserver, public content::WebContentsUserData { public: - using PrintToPDFCallback = - base::Callback, v8::Local)>; - ~PrintPreviewMessageHandler() override; void PrintToPDF(const base::DictionaryValue& options, - const PrintToPDFCallback& callback); + scoped_refptr promise); protected: // content::WebContentsObserver implementation. @@ -57,11 +55,15 @@ class PrintPreviewMessageHandler const PrintHostMsg_PreviewIds& ids); void OnPrintPreviewCancelled(int document_cookie, const PrintHostMsg_PreviewIds& ids); - void RunPrintToPDFCallback(int request_id, - scoped_refptr data_bytes); - using PrintToPDFCallbackMap = std::map; - PrintToPDFCallbackMap print_to_pdf_callback_map_; + scoped_refptr GetPromise(int request_id); + + void ResolvePromise(int request_id, + scoped_refptr data_bytes); + void RejectPromise(int request_id); + + using PromiseMap = std::map>; + PromiseMap promise_map_; base::WeakPtrFactory weak_ptr_factory_; diff --git a/docs/api/promisification.md b/docs/api/promisification.md index c0948e9fe307a..2af73b102bbc1 100644 --- a/docs/api/promisification.md +++ b/docs/api/promisification.md @@ -28,17 +28,16 @@ When a majority of affected functions are migrated, this flag will be enabled by - [ses.clearAuthCache(options[, callback])](https://github.com/electron/electron/blob/master/docs/api/session.md#clearAuthCache) - [contents.executeJavaScript(code[, userGesture, callback])](https://github.com/electron/electron/blob/master/docs/api/web-contents.md#executeJavaScript) - [contents.print([options], [callback])](https://github.com/electron/electron/blob/master/docs/api/web-contents.md#print) -- [contents.printToPDF(options, callback)](https://github.com/electron/electron/blob/master/docs/api/web-contents.md#printToPDF) - [contents.savePage(fullPath, saveType, callback)](https://github.com/electron/electron/blob/master/docs/api/web-contents.md#savePage) - [webFrame.executeJavaScript(code[, userGesture, callback])](https://github.com/electron/electron/blob/master/docs/api/web-frame.md#executeJavaScript) - [webFrame.executeJavaScriptInIsolatedWorld(worldId, scripts[, userGesture, callback])](https://github.com/electron/electron/blob/master/docs/api/web-frame.md#executeJavaScriptInIsolatedWorld) - [webviewTag.executeJavaScript(code[, userGesture, callback])](https://github.com/electron/electron/blob/master/docs/api/webview-tag.md#executeJavaScript) -- [webviewTag.printToPDF(options, callback)](https://github.com/electron/electron/blob/master/docs/api/webview-tag.md#printToPDF) ### Converted Functions - [app.getFileIcon(path[, options], callback)](https://github.com/electron/electron/blob/master/docs/api/app.md#getFileIcon) - [contents.capturePage([rect, ]callback)](https://github.com/electron/electron/blob/master/docs/api/web-contents.md#capturePage) +- [contents.printToPDF(options, callback)](https://github.com/electron/electron/blob/master/docs/api/web-contents.md#printToPDF) - [contentTracing.getCategories(callback)](https://github.com/electron/electron/blob/master/docs/api/content-tracing.md#getCategories) - [contentTracing.startRecording(options, callback)](https://github.com/electron/electron/blob/master/docs/api/content-tracing.md#startRecording) - [contentTracing.stopRecording(resultFilePath, callback)](https://github.com/electron/electron/blob/master/docs/api/content-tracing.md#stopRecording) @@ -50,5 +49,6 @@ When a majority of affected functions are migrated, this flag will be enabled by - [protocol.isProtocolHandled(scheme, callback)](https://github.com/electron/electron/blob/master/docs/api/protocol.md#isProtocolHandled) - [shell.openExternal(url[, options, callback])](https://github.com/electron/electron/blob/master/docs/api/shell.md#openExternal) - [webviewTag.capturePage([rect, ]callback)](https://github.com/electron/electron/blob/master/docs/api/webview-tag.md#capturePage) +- [webviewTag.printToPDF(options, callback)](https://github.com/electron/electron/blob/master/docs/api/webview-tag.md#printToPDF) - [win.capturePage([rect, ]callback)](https://github.com/electron/electron/blob/master/docs/api/browser-window.md#capturePage) - [desktopCapturer.getSources(options, callback)](https://github.com/electron/electron/blob/master/docs/api/desktop-capturer.md#getSources) diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index 9ccc7b1b5d949..f5c43de91fe1c 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -1202,6 +1202,25 @@ settings. The `callback` will be called with `callback(error, data)` on completion. The `data` is a `Buffer` that contains the generated PDF data. +**[Deprecated Soon](promisification.md)** + +#### `contents.printToPDF(options)` + +* `options` Object + * `marginsType` Integer (optional) - Specifies the type of margins to use. Uses 0 for + default margin, 1 for no margin, and 2 for minimum margin. + * `pageSize` String | Size (optional) - Specify page size of the generated PDF. Can be `A3`, + `A4`, `A5`, `Legal`, `Letter`, `Tabloid` or an Object containing `height` + and `width` in microns. + * `printBackground` Boolean (optional) - Whether to print CSS backgrounds. + * `printSelectionOnly` Boolean (optional) - Whether to print selection only. + * `landscape` Boolean (optional) - `true` for landscape, `false` for portrait. + +* Returns `Promise` - Resolves with the generated PDF data. + +Prints window's web page as PDF with Chromium's preview printing custom +settings. + The `landscape` will be ignored if `@page` CSS at-rule is used in the web page. By default, an empty `options` will be regarded as: diff --git a/docs/api/webview-tag.md b/docs/api/webview-tag.md index 0c17200eb2363..e9fdcde68ae40 100644 --- a/docs/api/webview-tag.md +++ b/docs/api/webview-tag.md @@ -542,6 +542,24 @@ Prints `webview`'s web page. Same as `webContents.print([options])`. Prints `webview`'s web page as PDF, Same as `webContents.printToPDF(options, callback)`. +**[Deprecated Soon](promisification.md)** + +### `.printToPDF(options)` + +* `options` Object + * `marginsType` Integer (optional) - Specifies the type of margins to use. Uses 0 for + default margin, 1 for no margin, and 2 for minimum margin. + * `pageSize` String | Size (optional) - Specify page size of the generated PDF. Can be `A3`, + `A4`, `A5`, `Legal`, `Letter`, `Tabloid` or an Object containing `height` + and `width` in microns. + * `printBackground` Boolean (optional) - Whether to print CSS backgrounds. + * `printSelectionOnly` Boolean (optional) - Whether to print selection only. + * `landscape` Boolean (optional) - `true` for landscape, `false` for portrait. + +* Returns `Promise` - Resolves with the generated PDF data. + +Prints `webview`'s web page as PDF, Same as `webContents.printToPDF(options)`. + ### `.capturePage([rect, ]callback)` * `rect` [Rectangle](structures/rectangle.md) (optional) - The bounds to capture diff --git a/lib/browser/api/web-contents.js b/lib/browser/api/web-contents.js index ebf5d65b6dc8b..12e180715a7fc 100644 --- a/lib/browser/api/web-contents.js +++ b/lib/browser/api/web-contents.js @@ -263,7 +263,7 @@ WebContents.prototype.takeHeapSnapshot = function (filePath) { } // Translate the options of printToPDF. -WebContents.prototype.printToPDF = function (options, callback) { +WebContents.prototype.printToPDF = function (options) { const printingSetting = Object.assign({}, defaultPrintingSetting) if (options.landscape) { printingSetting.landscape = options.landscape @@ -282,7 +282,7 @@ WebContents.prototype.printToPDF = function (options, callback) { const pageSize = options.pageSize if (typeof pageSize === 'object') { if (!pageSize.height || !pageSize.width) { - return callback(new Error('Must define height and width for pageSize')) + return Promise.reject(new Error('Must define height and width for pageSize')) } // Dimensions in Microns // 1 meter = 10^6 microns @@ -295,7 +295,7 @@ WebContents.prototype.printToPDF = function (options, callback) { } else if (PDFPageSizes[pageSize]) { printingSetting.mediaSize = PDFPageSizes[pageSize] } else { - return callback(new Error(`Does not support pageSize with ${pageSize}`)) + return Promise.reject(new Error(`Does not support pageSize with ${pageSize}`)) } } else { printingSetting.mediaSize = PDFPageSizes['A4'] @@ -304,9 +304,9 @@ WebContents.prototype.printToPDF = function (options, callback) { // Chromium expects this in a 0-100 range number, not as float printingSetting.scaleFactor *= 100 if (features.isPrintingEnabled()) { - this._printToPDF(printingSetting, callback) + return this._printToPDF(printingSetting) } else { - console.error('Error: Printing feature is disabled.') + return Promise.reject(new Error('Printing feature is disabled')) } } @@ -342,6 +342,9 @@ WebContents.prototype.loadFile = function (filePath, options = {}) { })) } +WebContents.prototype.capturePage = deprecate.promisify(WebContents.prototype.capturePage) +WebContents.prototype.printToPDF = deprecate.promisify(WebContents.prototype.printToPDF) + const addReplyToEvent = (event) => { event.reply = (...args) => { event.sender.sendToFrame(event.frameId, ...args) @@ -385,8 +388,6 @@ WebContents.prototype._init = function () { // render-view-deleted event, so ignore the listeners warning. this.setMaxListeners(0) - this.capturePage = deprecate.promisify(this.capturePage) - // Dispatch IPC messages to the ipc module. this.on('-ipc-message', function (event, internal, channel, args) { if (internal) { diff --git a/lib/common/web-view-methods.js b/lib/common/web-view-methods.js index 63f5ce1b6dffa..3ee1d3211cfa5 100644 --- a/lib/common/web-view-methods.js +++ b/lib/common/web-view-methods.js @@ -60,11 +60,11 @@ exports.asyncCallbackMethods = new Set([ 'sendInputEvent', 'setLayoutZoomLevelLimits', 'setVisualZoomLevelLimits', - 'print', - 'printToPDF' + 'print' ]) exports.asyncPromiseMethods = new Set([ 'capturePage', - 'executeJavaScript' + 'executeJavaScript', + 'printToPDF' ]) diff --git a/lib/renderer/web-view/web-view-impl.js b/lib/renderer/web-view/web-view-impl.js index 6579c45f6bf3e..66caf2716e5a4 100644 --- a/lib/renderer/web-view/web-view-impl.js +++ b/lib/renderer/web-view/web-view-impl.js @@ -1,6 +1,6 @@ 'use strict' -const { webFrame } = require('electron') +const { webFrame, deprecate } = require('electron') const v8Util = process.atomBinding('v8_util') const ipcRenderer = require('@electron/internal/renderer/ipc-renderer-internal') @@ -287,6 +287,8 @@ const setupMethods = (WebViewElement) => { for (const method of asyncPromiseMethods) { WebViewElement.prototype[method] = createPromiseHandler(method) } + + WebViewElement.prototype.printToPDF = deprecate.promisify(WebViewElement.prototype.printToPDF) } module.exports = { setupAttributes, setupMethods, guestViewInternal, webFrame, WebViewImpl } diff --git a/spec/api-web-contents-spec.js b/spec/api-web-contents-spec.js index 84730ceb007b4..4a6dd33f5b58d 100644 --- a/spec/api-web-contents-spec.js +++ b/spec/api-web-contents-spec.js @@ -1268,7 +1268,22 @@ describe('webContents module', () => { } }) - it('can print to PDF', (done) => { + it('can print to PDF', async () => { + w.destroy() + w = new BrowserWindow({ + show: false, + webPreferences: { + sandbox: true + } + }) + await w.loadURL('data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E') + const data = await w.webContents.printToPDF({}) + assert.strictEqual(data instanceof Buffer, true) + assert.notStrictEqual(data.length, 0) + }) + + // TODO(miniak): remove when promisification is complete + it('can print to PDF (callback)', (done) => { w.destroy() w = new BrowserWindow({ show: false, diff --git a/spec/webview-spec.js b/spec/webview-spec.js index 1eb02e28f604f..d00f23f1c6be8 100644 --- a/spec/webview-spec.js +++ b/spec/webview-spec.js @@ -12,6 +12,7 @@ const { emittedOnce, waitForEvent } = require('./events-helpers') const { expect } = chai chai.use(dirtyChai) +const features = process.atomBinding('features') const isCI = remote.getGlobal('isCi') const nativeModulesEnabled = remote.getGlobal('nativeModulesEnabled') @@ -1233,6 +1234,38 @@ describe(' tag', function () { }) }) + describe('.printToPDF()', () => { + before(function () { + if (!features.isPrintingEnabled()) { + this.skip() + } + }) + + it('can print to PDF', async () => { + const src = 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E' + await loadWebView(webview, { src }) + + const data = await webview.printToPDF({}) + assert.strictEqual(data instanceof Buffer, true) + assert.notStrictEqual(data.length, 0) + }) + + // TODO(miniak): remove when promisification is complete + it('can print to PDF (callback)', (done) => { + webview.addEventListener('did-finish-load', () => { + webview.printToPDF({}, function (error, data) { + assert.strictEqual(error, null) + assert.strictEqual(data instanceof Buffer, true) + assert.notStrictEqual(data.length, 0) + done() + }) + }) + + webview.src = 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E' + document.body.appendChild(webview) + }) + }) + // FIXME(deepak1556): Ch69 follow up. xdescribe('document.visibilityState/hidden', () => { afterEach(() => {