Skip to content

Commit

Permalink
feat: promisify webContents.printToPDF() (#16795)
Browse files Browse the repository at this point in the history
  • Loading branch information
miniak authored and John Kleinschmidt committed Feb 11, 2019
1 parent 3effa6f commit 36ce3e9
Show file tree
Hide file tree
Showing 12 changed files with 161 additions and 57 deletions.
9 changes: 5 additions & 4 deletions atom/browser/api/atom_api_web_contents.cc
Expand Up @@ -1499,11 +1499,12 @@ std::vector<printing::PrinterBasicInfo> WebContents::GetPrinterList() {
return printers;
}

void WebContents::PrintToPDF(
const base::DictionaryValue& settings,
const PrintPreviewMessageHandler::PrintToPDFCallback& callback) {
v8::Local<v8::Promise> WebContents::PrintToPDF(
const base::DictionaryValue& settings) {
scoped_refptr<util::Promise> promise = new util::Promise(isolate());
PrintPreviewMessageHandler::FromWebContents(web_contents())
->PrintToPDF(settings, callback);
->PrintToPDF(settings, promise);
return promise->GetHandle();
}
#endif

Expand Down
4 changes: 1 addition & 3 deletions atom/browser/api/atom_api_web_contents.h
Expand Up @@ -174,9 +174,7 @@ class WebContents : public mate::TrackableObject<WebContents>,
void Print(mate::Arguments* args);
std::vector<printing::PrinterBasicInfo> GetPrinterList();
// Print current page as PDF.
void PrintToPDF(
const base::DictionaryValue& settings,
const PrintPreviewMessageHandler::PrintToPDFCallback& callback);
v8::Local<v8::Promise> PrintToPDF(const base::DictionaryValue& settings);
#endif

// DevTools workspace api.
Expand Down
71 changes: 43 additions & 28 deletions atom/browser/printing/print_preview_message_handler.cc
Expand Up @@ -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;
}

Expand All @@ -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));
}
}

Expand All @@ -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));
}
Expand All @@ -131,23 +130,23 @@ void PrintPreviewMessageHandler::OnPrintPreviewFailed(
const PrintHostMsg_PreviewIds& ids) {
StopWorker(document_cookie);

RunPrintToPDFCallback(ids.request_id, nullptr);
RejectPromise(ids.request_id);
}

void PrintPreviewMessageHandler::OnPrintPreviewCancelled(
int document_cookie,
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<atom::util::Promise> 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()
Expand All @@ -156,28 +155,44 @@ void PrintPreviewMessageHandler::PrintToPDF(
rfh->Send(new PrintMsg_PrintPreview(rfh->GetRoutingID(), options));
}

void PrintPreviewMessageHandler::RunPrintToPDFCallback(
scoped_refptr<atom::util::Promise> 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<base::RefCountedMemory> 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<v8::Value> buffer =
node::Buffer::Copy(isolate,
reinterpret_cast<const char*>(data_bytes->front()),
data_bytes->size())
.ToLocalChecked();
print_to_pdf_callback_map_[request_id].Run(v8::Null(isolate), buffer);
} else {
v8::Local<v8::String> 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<v8::Context>::New(isolate, promise->GetContext()));

v8::Local<v8::Value> buffer =
node::Buffer::Copy(isolate,
reinterpret_cast<const char*>(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
18 changes: 10 additions & 8 deletions atom/browser/printing/print_preview_message_handler.h
Expand Up @@ -7,6 +7,7 @@

#include <map>

#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"
Expand All @@ -28,13 +29,10 @@ class PrintPreviewMessageHandler
: public content::WebContentsObserver,
public content::WebContentsUserData<PrintPreviewMessageHandler> {
public:
using PrintToPDFCallback =
base::Callback<void(v8::Local<v8::Value>, v8::Local<v8::Value>)>;

~PrintPreviewMessageHandler() override;

void PrintToPDF(const base::DictionaryValue& options,
const PrintToPDFCallback& callback);
scoped_refptr<atom::util::Promise> promise);

protected:
// content::WebContentsObserver implementation.
Expand All @@ -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<base::RefCountedMemory> data_bytes);

using PrintToPDFCallbackMap = std::map<int, PrintToPDFCallback>;
PrintToPDFCallbackMap print_to_pdf_callback_map_;
scoped_refptr<atom::util::Promise> GetPromise(int request_id);

void ResolvePromise(int request_id,
scoped_refptr<base::RefCountedMemory> data_bytes);
void RejectPromise(int request_id);

using PromiseMap = std::map<int, scoped_refptr<atom::util::Promise>>;
PromiseMap promise_map_;

base::WeakPtrFactory<PrintPreviewMessageHandler> weak_ptr_factory_;

Expand Down
4 changes: 2 additions & 2 deletions docs/api/promisification.md
Expand Up @@ -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)
Expand All @@ -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)
19 changes: 19 additions & 0 deletions docs/api/web-contents.md
Expand Up @@ -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<Buffer>` - 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:
Expand Down
18 changes: 18 additions & 0 deletions docs/api/webview-tag.md
Expand Up @@ -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)**

### `<webview>.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<Buffer>` - Resolves with the generated PDF data.

Prints `webview`'s web page as PDF, Same as `webContents.printToPDF(options)`.

### `<webview>.capturePage([rect, ]callback)`

* `rect` [Rectangle](structures/rectangle.md) (optional) - The bounds to capture
Expand Down
15 changes: 8 additions & 7 deletions lib/browser/api/web-contents.js
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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']
Expand All @@ -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'))
}
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 3 additions & 3 deletions lib/common/web-view-methods.js
Expand Up @@ -60,11 +60,11 @@ exports.asyncCallbackMethods = new Set([
'sendInputEvent',
'setLayoutZoomLevelLimits',
'setVisualZoomLevelLimits',
'print',
'printToPDF'
'print'
])

exports.asyncPromiseMethods = new Set([
'capturePage',
'executeJavaScript'
'executeJavaScript',
'printToPDF'
])
4 changes: 3 additions & 1 deletion 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')
Expand Down Expand Up @@ -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 }
17 changes: 16 additions & 1 deletion spec/api-web-contents-spec.js
Expand Up @@ -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,
Expand Down

0 comments on commit 36ce3e9

Please sign in to comment.