0

pdf: Add entry and exit of annotation-mode

* Adds a new viewer element to host the Ink component. For now this is
  just a dummy implementation of the expected API allowing us to test
  entry/exit.
* `ContentController` is extended with load/unload
* `InkController` is added and made the current controller when in
  annotation mode, delegating behavior to the viewer-ink-host rather
  than plugin.
* The plugin interface is extended to allow the PDF to be saved and
  sent back regardless of whether the PDF has been modified.
* The loaded state of PDFViewer is centralized and exposed via the
  `loaded` property. This allows the switching logic and tests to wait
  for the mode switch to complete.

Bug: 902646
Change-Id: I2922cb121e510e9e84c21b75faddc48fa90a195c
Reviewed-on: https://chromium-review.googlesource.com/c/1368846
Commit-Queue: dstockwell <dstockwell@chromium.org>
Reviewed-by: Lei Zhang <thestig@chromium.org>
Reviewed-by: Henrique Nakashima <hnakashima@chromium.org>
Reviewed-by: Demetrios Papadopoulos <dpapad@chromium.org>
Cr-Commit-Position: refs/heads/master@{#617686}
This commit is contained in:
dstockwell
2018-12-19 00:20:01 +00:00
committed by Commit Bot
parent 6c13fac529
commit f905e88821
17 changed files with 401 additions and 49 deletions

@@ -119,6 +119,8 @@
<include name="IDR_PDF_VIEWER_BOOKMARKS_CONTENT_JS" file="pdf/elements/viewer-bookmarks-content/viewer-bookmarks-content.js" type="BINDATA" /> <include name="IDR_PDF_VIEWER_BOOKMARKS_CONTENT_JS" file="pdf/elements/viewer-bookmarks-content/viewer-bookmarks-content.js" type="BINDATA" />
<include name="IDR_PDF_VIEWER_ERROR_SCREEN_HTML" file="pdf/elements/viewer-error-screen/viewer-error-screen.html" type="BINDATA" /> <include name="IDR_PDF_VIEWER_ERROR_SCREEN_HTML" file="pdf/elements/viewer-error-screen/viewer-error-screen.html" type="BINDATA" />
<include name="IDR_PDF_VIEWER_ERROR_SCREEN_JS" file="pdf/elements/viewer-error-screen/viewer-error-screen.js" type="BINDATA" /> <include name="IDR_PDF_VIEWER_ERROR_SCREEN_JS" file="pdf/elements/viewer-error-screen/viewer-error-screen.js" type="BINDATA" />
<include name="IDR_PDF_VIEWER_INK_HOST_HTML" file="pdf/elements/viewer-ink-host/viewer-ink-host.html" type="BINDATA" />
<include name="IDR_PDF_VIEWER_INK_HOST_JS" file="pdf/elements/viewer-ink-host/viewer-ink-host.js" type="BINDATA" />
<include name="IDR_PDF_VIEWER_PAGE_INDICATOR_HTML" file="pdf/elements/viewer-page-indicator/viewer-page-indicator.html" type="BINDATA" /> <include name="IDR_PDF_VIEWER_PAGE_INDICATOR_HTML" file="pdf/elements/viewer-page-indicator/viewer-page-indicator.html" type="BINDATA" />
<include name="IDR_PDF_VIEWER_PAGE_INDICATOR_JS" file="pdf/elements/viewer-page-indicator/viewer-page-indicator.js" type="BINDATA" flattenhtml="true" /> <include name="IDR_PDF_VIEWER_PAGE_INDICATOR_JS" file="pdf/elements/viewer-page-indicator/viewer-page-indicator.js" type="BINDATA" flattenhtml="true" />
<include name="IDR_PDF_VIEWER_PAGE_SELECTOR_HTML" file="pdf/elements/viewer-page-selector/viewer-page-selector.html" type="BINDATA" /> <include name="IDR_PDF_VIEWER_PAGE_SELECTOR_HTML" file="pdf/elements/viewer-page-selector/viewer-page-selector.html" type="BINDATA" />

@@ -10,6 +10,7 @@ group("closure_compile") {
":pdf_resources", ":pdf_resources",
"elements/viewer-bookmark:closure_compile", "elements/viewer-bookmark:closure_compile",
"elements/viewer-error-screen:closure_compile", "elements/viewer-error-screen:closure_compile",
"elements/viewer-ink-host:closure_compile",
"elements/viewer-page-indicator:closure_compile", "elements/viewer-page-indicator:closure_compile",
"elements/viewer-page-selector:closure_compile", "elements/viewer-page-selector:closure_compile",
"elements/viewer-password-screen:closure_compile", "elements/viewer-password-screen:closure_compile",

@@ -0,0 +1,14 @@
# Copyright 2018 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import("//third_party/closure_compiler/compile_js.gni")
js_type_check("closure_compile") {
deps = [
":viewer-ink-host",
]
}
js_library("viewer-ink-host") {
}

@@ -0,0 +1,13 @@
<link rel="import" href="chrome://resources/html/polymer.html">
<dom-module id="viewer-ink-host">
<template>
<style>
:host {
visibility: hidden;
}
</style>
[[dummyContent_]]
</template>
<script src="viewer-ink-host.js"></script>
</dom-module>

@@ -0,0 +1,49 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* Hosts the Ink component which is responsible for both PDF rendering and
* annotation when in annotation mode.
*/
Polymer({
is: 'viewer-ink-host',
properties: {
/** @private */
dummyContent_: String,
},
/** @private {?string} */
dummyFileName_: null,
/** @private {ArrayBuffer} */
dummyData_: null,
/**
* Begins annotation mode with the document represented by `data`.
* When the return value resolves the Ink component will be ready
* to render immediately.
*
* @param {string} fileName The name of the PDF file.
* @param {ArrayBuffer} data The contents of the PDF document.
* @return {!Promise} void value.
*/
load: async function(fileName, data) {
this.dummyContent_ = `Annotating ${data.byteLength} bytes`;
this.dummyFileName_ = fileName;
this.dummyData_ = data;
this.style.visibility = 'visible';
},
/**
* @return {!Promise<{fileName: string, dataToSave: ArrayBuffer}>}
* The serialized PDF document including any annotations that were made.
*/
saveDocument: async function() {
return {
fileName: this.dummyFileName_,
dataToSave: this.dummyData_,
};
},
});

@@ -125,6 +125,7 @@
<div id="buttons" class="invisible"> <div id="buttons" class="invisible">
<template is="dom-if" if="[[pdfAnnotationsEnabled]]"> <template is="dom-if" if="[[pdfAnnotationsEnabled]]">
<paper-icon-button id="annotate" icon="pdf:create" <paper-icon-button id="annotate" icon="pdf:create"
on-click="toggleAnnotation"
aria-label$="{{strings.tooltipAnnotate}}" aria-label$="{{strings.tooltipAnnotate}}"
title$="{{strings.tooltipAnnotate}}"> title$="{{strings.tooltipAnnotate}}">
</paper-icon-button> </paper-icon-button>
@@ -160,7 +161,7 @@
</div> </div>
</div> </div>
<div id="progress-container"> <div id="progress-container">
<paper-progress id="progress" value="{{loadProgress}}"></paper-progress> <paper-progress id="progress" value="[[loadProgress]]"></paper-progress>
</div> </div>
</div> </div>
</template> </template>

@@ -9,7 +9,7 @@ Polymer({
/** /**
* The current loading progress of the PDF document (0 - 100). * The current loading progress of the PDF document (0 - 100).
*/ */
loadProgress: {type: Number, observer: 'loadProgressChanged'}, loadProgress: {type: Number, observer: 'loadProgressChanged_'},
/** /**
* The title of the PDF document. * The title of the PDF document.
@@ -36,6 +36,14 @@ Polymer({
*/ */
opened: {type: Boolean, value: true}, opened: {type: Boolean, value: true},
/**
* Whether the viewer is currently in annotation mode.
*/
annotationMode: {
type: Boolean,
notify: true,
},
/** /**
* Whether the PDF Annotations feature is enabled. * Whether the PDF Annotations feature is enabled.
*/ */
@@ -44,11 +52,18 @@ Polymer({
strings: Object, strings: Object,
}, },
loadProgressChanged: function() { /**
if (this.loadProgress >= 100) { * @param {number} newProgress
this.$.pageselector.classList.toggle('invisible', false); * @param {number} oldProgress
this.$.buttons.classList.toggle('invisible', false); * @private
this.$.progress.style.opacity = 0; */
loadProgressChanged_: function(newProgress, oldProgress) {
const loaded = newProgress >= 100;
const progressReset = newProgress < oldProgress;
if (progressReset || loaded) {
this.$.pageselector.classList.toggle('invisible', !loaded);
this.$.buttons.classList.toggle('invisible', !loaded);
this.$.progress.style.opacity = loaded ? 0 : 1;
} }
}, },
@@ -126,6 +141,10 @@ Polymer({
print: function() { print: function() {
this.fire('print'); this.fire('print');
},
toggleAnnotation: function() {
this.annotationMode = !this.annotationMode;
} }
}); });
})(); })();

@@ -20,6 +20,7 @@ viewer-pdf-toolbar {
z-index: 4; z-index: 4;
} }
viewer-ink-host,
#plugin { #plugin {
height: 100%; height: 100%;
position: fixed; position: fixed;

@@ -3,6 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="import" href="elements/viewer-error-screen/viewer-error-screen.html"> <link rel="import" href="elements/viewer-error-screen/viewer-error-screen.html">
<link rel="import" href="elements/viewer-ink-host/viewer-ink-host.html">
<link rel="import" href="elements/viewer-page-indicator/viewer-page-indicator.html"> <link rel="import" href="elements/viewer-page-indicator/viewer-page-indicator.html">
<link rel="import" href="elements/viewer-page-selector/viewer-page-selector.html"> <link rel="import" href="elements/viewer-page-selector/viewer-page-selector.html">
<link rel="import" href="elements/viewer-password-screen/viewer-password-screen.html"> <link rel="import" href="elements/viewer-password-screen/viewer-password-screen.html">
@@ -38,6 +39,7 @@
<script src="pdf_scripting_api.js"></script> <script src="pdf_scripting_api.js"></script>
<script src="chrome://resources/js/load_time_data.js"></script> <script src="chrome://resources/js/load_time_data.js"></script>
<script src="chrome://resources/js/util.js"></script> <script src="chrome://resources/js/util.js"></script>
<script src="chrome://resources/js/promise_resolver.js"></script>
<script src="browser_api.js"></script> <script src="browser_api.js"></script>
<script src="metrics.js"></script> <script src="metrics.js"></script>
<script src="pdf_viewer.js"></script> <script src="pdf_viewer.js"></script>

@@ -125,9 +125,7 @@ function PDFViewer(browserApi) {
this.isFormFieldFocused_ = false; this.isFormFieldFocused_ = false;
this.beepCount_ = 0; this.beepCount_ = 0;
this.delayedScriptingMessages_ = []; this.delayedScriptingMessages_ = [];
this.loaded_ = new PromiseResolver();
/** @private {!Set<string>} */
this.pendingTokens_ = new Set();
this.isPrintPreview_ = location.origin === 'chrome://print'; this.isPrintPreview_ = location.origin === 'chrome://print';
this.isPrintPreviewLoadingFinished_ = false; this.isPrintPreviewLoadingFinished_ = false;
@@ -220,10 +218,12 @@ function PDFViewer(browserApi) {
} else { } else {
this.plugin_.setAttribute('full-frame', ''); this.plugin_.setAttribute('full-frame', '');
} }
document.body.appendChild(this.plugin_); document.body.appendChild(this.plugin_);
this.pluginController_ = this.pluginController_ =
new PluginController(this.plugin_, this, this.viewport_); new PluginController(this.plugin_, this, this.viewport_);
this.inkController_ = new InkController(this, this.viewport_);
this.currentController_ = this.pluginController_; this.currentController_ = this.pluginController_;
// Setup the button event listeners. // Setup the button event listeners.
@@ -252,6 +252,8 @@ function PDFViewer(browserApi) {
'print', () => this.currentController_.print()); 'print', () => this.currentController_.print());
this.toolbar_.addEventListener( this.toolbar_.addEventListener(
'rotate-right', () => this.currentController_.rotateClockwise()); 'rotate-right', () => this.currentController_.rotateClockwise());
this.toolbar_.addEventListener(
'annotation-mode-changed', e => this.annotationModeChanged_(e));
this.toolbar_.docTitle = getFilenameFromURL(this.originalUrl_); this.toolbar_.docTitle = getFilenameFromURL(this.originalUrl_);
} }
@@ -472,6 +474,52 @@ PDFViewer.prototype = {
} }
}, },
/**
* Handles the annotation mode being toggled on or off.
*
* @param {CustomEvent} e
* @private
*/
annotationModeChanged_: async function(e) {
const annotationMode = e.detail.value;
if (annotationMode) {
// TODO(dstockwell): add assert lib and replace this with assert
if (this.currentController_ != this.pluginController_) {
throw new Error(
'Plugin controller is not current, cannot enter annotation mode');
}
// Enter annotation mode.
// TODO(dstockwell): set plugin read-only, begin transition
this.updateProgress(0);
// TODO(dstockwell): handle save failure
const result = await this.pluginController_.save(true);
// TODO(dstockwell): feed real progress data from the Ink component
this.updateProgress(50);
await this.inkController_.load(result.fileName, result.dataToSave);
this.currentController_ = this.inkController_;
this.pluginController_.unload();
this.updateProgress(100);
} else {
// Exit annotation mode.
// TODO(dstockwell): add assert lib and replace this with assert
if (this.currentController_ != this.inkController_) {
throw new Error(
'Ink controller is not current, cannot exit annotation mode');
}
// TODO(dstockwell): set ink read-only, begin transition
this.updateProgress(0);
// This runs separately to allow other consumers of `loaded` to queue
// up after this task.
this.loaded.then(() => {
this.currentController_ = this.pluginController_;
this.inkController_.unload();
});
// TODO(dstockwell): handle save failure
const result = await this.inkController_.save(true);
await this.pluginController_.load(result.fileName, result.dataToSave);
}
},
/** /**
* Request to change the viewport fitting type. * Request to change the viewport fitting type.
* *
@@ -559,9 +607,41 @@ PDFViewer.prototype = {
this.metrics.onFollowBookmark(); this.metrics.onFollowBookmark();
}, },
/**
* @return {Promise} Resolved when the load state reaches LOADED,
* rejects on FAILED.
*/
get loaded() {
return this.loaded_.promise;
},
/**
* Updates the load state and triggers completion of the `loaded`
* promise if necessary.
* @param {!LoadState} loadState
* @private
*/
setLoadState_(loadState) {
if (this.loadState_ == loadState) {
return;
}
if ((loadState == LoadState.SUCCESS || loadState == LoadState.FAILURE) &&
this.loadState_ != LoadState.LOADING) {
throw new Error('Internal error: invalid loadState transition.');
}
this.loadState_ = loadState;
if (loadState == LoadState.SUCCESS) {
this.loaded_.resolve();
} else if (loadState == LoadState.FAILED) {
this.loaded_.reject();
} else {
this.loaded_ = new PromiseResolver();
}
},
/** /**
* Update the loading progress of the document in response to a progress * Update the loading progress of the document in response to a progress
* message being received from the plugin. * message being received from the content controller.
* *
* @param {number} progress the progress as a percentage. * @param {number} progress the progress as a percentage.
*/ */
@@ -577,7 +657,7 @@ PDFViewer.prototype = {
this.passwordScreen_.deny(); this.passwordScreen_.deny();
this.passwordScreen_.close(); this.passwordScreen_.close();
} }
this.loadState_ = LoadState.FAILED; this.setLoadState_(LoadState.FAILED);
this.isPrintPreviewLoadingFinished_ = true; this.isPrintPreviewLoadingFinished_ = true;
this.sendDocumentLoadedMessage_(); this.sendDocumentLoadedMessage_();
} else if (progress == 100) { } else if (progress == 100) {
@@ -586,12 +666,14 @@ PDFViewer.prototype = {
this.viewport_.position = this.lastViewportPosition_; this.viewport_.position = this.lastViewportPosition_;
this.paramsParser_.getViewportFromUrlParams( this.paramsParser_.getViewportFromUrlParams(
this.originalUrl_, this.handleURLParams_.bind(this)); this.originalUrl_, this.handleURLParams_.bind(this));
this.loadState_ = LoadState.SUCCESS; this.setLoadState_(LoadState.SUCCESS);
this.sendDocumentLoadedMessage_(); this.sendDocumentLoadedMessage_();
while (this.delayedScriptingMessages_.length > 0) while (this.delayedScriptingMessages_.length > 0)
this.handleScriptingMessage(this.delayedScriptingMessages_.shift()); this.handleScriptingMessage(this.delayedScriptingMessages_.shift());
this.toolbarManager_.hideToolbarsAfterTimeout(); this.toolbarManager_.hideToolbarsAfterTimeout();
} else {
this.setLoadState_(LoadState.LOADING);
} }
}, },
@@ -797,7 +879,7 @@ PDFViewer.prototype = {
this.pluginController_.postMessage(message.data); this.pluginController_.postMessage(message.data);
return true; return true;
case 'resetPrintPreviewMode': case 'resetPrintPreviewMode':
this.loadState_ = LoadState.LOADING; this.setLoadState_(LoadState.LOADING);
if (!this.inPrintPreviewMode_) { if (!this.inPrintPreviewMode_) {
this.inPrintPreviewMode_ = true; this.inPrintPreviewMode_ = true;
this.isUserInitiatedEvent_ = false; this.isUserInitiatedEvent_ = false;
@@ -1003,7 +1085,8 @@ PDFViewer.prototype = {
* Saves the current PDF document to disk. * Saves the current PDF document to disk.
*/ */
save: async function() { save: async function() {
const result = await this.currentController_.save(); // TODO(dstockwell): Report an error to user if this fails.
const result = await this.currentController_.save(false);
if (result == null) { if (result == null) {
// The content controller handled the save internally. // The content controller handled the save internally.
return; return;
@@ -1057,10 +1140,77 @@ class ContentController {
/** /**
* Requests that the current document be saved. * Requests that the current document be saved.
* @param {boolean} requireResult whether a response is required, otherwise
* the controller may save the document to disk internally.
* @return {Promise<{fileName: string, dataToSave: ArrayBuffer}} * @return {Promise<{fileName: string, dataToSave: ArrayBuffer}}
* @abstract * @abstract
*/ */
save() {} save(requireResult) {}
/**
* Loads PDF document from `data` activates UI.
* @param {string} fileName
* @param {ArrayBuffer} data
* @return {Promise<void>}
* @abstract
*/
load(fileName, data) {}
/**
* Unloads the current document and removes the UI.
* @abstract
*/
unload() {}
}
class InkController extends ContentController {
/**
* @param {PDFViewer} viewer
* @param {Viewport} viewport
*/
constructor(viewer, viewport) {
super();
this.viewer_ = viewer;
this.viewport_ = viewport;
/** @type {ViewerInkHost} */
this.inkHost_ = null;
}
/** @override */
rotateClockwise() {
// TODO(dstockwell): implement rotation
}
/** @override */
rotateCounterClockwise() {
// TODO(dstockwell): implement rotation
}
/** @override */
print() {
// TODO(dstockwell): implement printing
}
/** @override */
save(requireResult) {
return this.inkHost_.saveDocument();
}
/** @override */
load(filename, data) {
if (!this.inkHost_) {
this.inkHost_ = document.createElement('viewer-ink-host');
document.body.appendChild(this.inkHost_);
}
return this.inkHost_.load(filename, data);
}
/** @override */
unload() {
this.inkHost_.remove();
this.inkHost_ = null;
}
} }
class PluginController extends ContentController { class PluginController extends ContentController {
@@ -1075,7 +1225,7 @@ class PluginController extends ContentController {
this.viewer_ = viewer; this.viewer_ = viewer;
this.viewport_ = viewport; this.viewport_ = viewport;
/** @private {!Map<string, Function>} */ /** @private {!Map<string, PromiseResolver>} */
this.pendingTokens_ = new Map(); this.pendingTokens_ = new Map();
this.plugin_.addEventListener( this.plugin_.addEventListener(
'message', e => this.handlePluginMessage_(e), false); 'message', e => this.handlePluginMessage_(e), false);
@@ -1162,13 +1312,30 @@ class PluginController extends ContentController {
} }
/** @override */ /** @override */
save() { save(requireResult) {
return new Promise(resolve => { const resolver = new PromiseResolver();
const newToken = createToken(); const newToken = createToken();
this.pendingTokens_.set(newToken, resolve); this.pendingTokens_.set(newToken, resolver);
const force = false; this.postMessage({type: 'save', token: newToken, force: requireResult});
this.postMessage({type: 'save', token: newToken}); return resolver.promise;
}); }
/** @override */
async load(fileName, data) {
const url = URL.createObjectURL(new Blob([data]));
this.plugin_.removeAttribute('headers');
this.plugin_.setAttribute('stream-url', url);
this.plugin_.style.display = 'block';
try {
await this.viewer_.loaded;
} finally {
URL.revokeObjectURL(url);
}
}
/** @override */
unload() {
this.plugin_.style.display = 'none';
} }
/** /**
@@ -1250,15 +1417,21 @@ class PluginController extends ContentController {
* @private * @private
*/ */
saveData_(messageData) { saveData_(messageData) {
if (!(loadTimeData.getBoolean('pdfFormSaveEnabled'))) if (!(loadTimeData.getBoolean('pdfFormSaveEnabled') ||
loadTimeData.getBoolean('pdfAnnotationsEnabled')))
throw new Error('Internal error: save not enabled.'); throw new Error('Internal error: save not enabled.');
// Verify a token that was created by this instance is included to avoid // Verify a token that was created by this instance is included to avoid
// being spammed. // being spammed.
const resolve = this.pendingTokens_.get(messageData.token); const resolver = this.pendingTokens_.get(messageData.token);
if (!this.pendingTokens_.delete(messageData.token)) if (!this.pendingTokens_.delete(messageData.token))
throw new Error('Internal error: save token not found, abort save.'); throw new Error('Internal error: save token not found, abort save.');
if (!messageData.dataToSave) {
resolver.reject();
return;
}
// Verify the file size and the first bytes to make sure it's a PDF. Cap at // Verify the file size and the first bytes to make sure it's a PDF. Cap at
// 100 MB. This cap should be kept in sync with and is also enforced in // 100 MB. This cap should be kept in sync with and is also enforced in
// pdf/out_of_process_instance.cc. // pdf/out_of_process_instance.cc.
@@ -1275,6 +1448,6 @@ class PluginController extends ContentController {
throw new Error('Not a PDF file.'); throw new Error('Not a PDF file.');
} }
resolve(messageData); resolver.resolve(messageData);
} }
} }

@@ -436,6 +436,10 @@ void SetupPrintPreviewPlugin(content::WebUIDataSource* source) {
source->AddResourcePath( source->AddResourcePath(
"pdf/elements/viewer-error-screen/viewer-error-screen.js", "pdf/elements/viewer-error-screen/viewer-error-screen.js",
IDR_PDF_VIEWER_ERROR_SCREEN_JS); IDR_PDF_VIEWER_ERROR_SCREEN_JS);
source->AddResourcePath("pdf/elements/viewer-ink-host/viewer-ink-host.html",
IDR_PDF_VIEWER_INK_HOST_HTML);
source->AddResourcePath("pdf/elements/viewer-ink-host/viewer-ink-host.js",
IDR_PDF_VIEWER_INK_HOST_JS);
source->AddResourcePath( source->AddResourcePath(
"pdf/elements/viewer-page-indicator/viewer-page-indicator.html", "pdf/elements/viewer-page-indicator/viewer-page-indicator.html",
IDR_PDF_VIEWER_PAGE_INDICATOR_HTML); IDR_PDF_VIEWER_PAGE_INDICATOR_HTML);

@@ -2,6 +2,20 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
function contentElement() {
return document.elementFromPoint(innerWidth / 2, innerHeight / 2);
}
async function testAsync(f) {
try {
await f();
chrome.test.succeed();
} catch (e) {
chrome.test.fail(e);
}
}
chrome.test.runTests([ chrome.test.runTests([
function testAnnotationsEnabled() { function testAnnotationsEnabled() {
const toolbar = document.body.querySelector('#toolbar'); const toolbar = document.body.querySelector('#toolbar');
@@ -10,4 +24,20 @@ chrome.test.runTests([
toolbar.shadowRoot.querySelector('#annotate') != null); toolbar.shadowRoot.querySelector('#annotate') != null);
chrome.test.succeed(); chrome.test.succeed();
}, },
function testEnterAndExitAnnotationMode() {
testAsync(async () => {
chrome.test.assertEq('EMBED', contentElement().tagName);
// Enter annotation mode.
$('toolbar').toggleAnnotation();
await viewer.loaded;
chrome.test.assertEq(
'VIEWER-INK-HOST', contentElement().tagName);
// Exit annotation mode.
$('toolbar').toggleAnnotation();
await viewer.loaded;
chrome.test.assertEq('EMBED', contentElement().tagName);
});
}
]); ]);

@@ -102,6 +102,7 @@ constexpr char kJSPrintType[] = "print";
// Save (Page -> Plugin) // Save (Page -> Plugin)
constexpr char kJSSaveType[] = "save"; constexpr char kJSSaveType[] = "save";
constexpr char kJSToken[] = "token"; constexpr char kJSToken[] = "token";
constexpr char kJSForce[] = "force";
// Save Data (Plugin -> Page) // Save Data (Plugin -> Page)
constexpr char kJSSaveDataType[] = "saveData"; constexpr char kJSSaveDataType[] = "saveData";
constexpr char kJSFileName[] = "fileName"; constexpr char kJSFileName[] = "fileName";
@@ -666,11 +667,17 @@ void OutOfProcessInstance::HandleMessage(const pp::Var& message) {
} else if (type == kJSPrintType) { } else if (type == kJSPrintType) {
Print(); Print();
} else if (type == kJSSaveType) { } else if (type == kJSSaveType) {
if (!dict.Get(pp::Var(kJSToken)).is_string()) { if (!(dict.Get(pp::Var(kJSToken)).is_string() &&
dict.Get(pp::Var(kJSForce)).is_bool())) {
NOTREACHED(); NOTREACHED();
return; return;
} }
Save(dict.Get(pp::Var(kJSToken)).AsString()); const bool force = dict.Get(pp::Var(kJSForce)).AsBool();
if (force) {
SaveToBuffer(dict.Get(pp::Var(kJSToken)).AsString());
} else {
SaveToFile(dict.Get(pp::Var(kJSToken)).AsString());
}
} else if (type == kJSRotateClockwiseType) { } else if (type == kJSRotateClockwiseType) {
RotateClockwise(); RotateClockwise();
} else if (type == kJSRotateCounterclockwiseType) { } else if (type == kJSRotateCounterclockwiseType) {
@@ -1473,37 +1480,58 @@ void OutOfProcessInstance::GetDocumentPassword(
PostMessage(message); PostMessage(message);
} }
void OutOfProcessInstance::Save(const std::string& token) { bool OutOfProcessInstance::ShouldSaveEdits() const {
engine_->KillFormFocus(); return edit_mode_ &&
base::FeatureList::IsEnabled(features::kSaveEditedPDFForm);
}
if (!base::FeatureList::IsEnabled(features::kSaveEditedPDFForm) || void OutOfProcessInstance::SaveToBuffer(const std::string& token) {
!edit_mode_) { engine_->KillFormFocus();
ConsumeSaveToken(token);
pp::PDF::SaveAs(this);
return;
}
GURL url(url_); GURL url(url_);
std::string file_name = url.ExtractFileName(); std::string file_name = url.ExtractFileName();
file_name = net::UnescapeURLComponent(file_name, net::UnescapeRule::SPACES); file_name = net::UnescapeURLComponent(file_name, net::UnescapeRule::SPACES);
std::vector<uint8_t> data = engine_->GetSaveData();
if (data.size() == 0u || data.size() > kMaximumSavedFileSize) {
// TODO(thestig): Add feedback to the user that a failure occurred.
ConsumeSaveToken(token);
return;
}
pp::VarDictionary message; pp::VarDictionary message;
message.Set(kType, kJSSaveDataType); message.Set(kType, kJSSaveDataType);
message.Set(kJSToken, pp::Var(token)); message.Set(kJSToken, pp::Var(token));
message.Set(kJSFileName, pp::Var(file_name)); message.Set(kJSFileName, pp::Var(file_name));
pp::VarArrayBuffer buffer(data.size()); // This will be overwritten if the save is successful.
std::copy(data.begin(), data.end(), reinterpret_cast<char*>(buffer.Map())); message.Set(kJSDataToSave, pp::Var(pp::Var::Null()));
message.Set(kJSDataToSave, buffer);
if (ShouldSaveEdits()) {
std::vector<uint8_t> data = engine_->GetSaveData();
if (data.size() > 0 && data.size() <= kMaximumSavedFileSize) {
pp::VarArrayBuffer buffer(data.size());
std::copy(data.begin(), data.end(),
reinterpret_cast<char*>(buffer.Map()));
message.Set(kJSDataToSave, buffer);
}
} else {
DCHECK(base::FeatureList::IsEnabled(features::kPDFAnnotations));
uint32_t length = engine_->GetLoadedByteSize();
if (length > 0 && length <= kMaximumSavedFileSize) {
pp::VarArrayBuffer buffer(length);
if (engine_->ReadLoadedBytes(length, buffer.Map())) {
message.Set(kJSDataToSave, buffer);
}
}
}
PostMessage(message); PostMessage(message);
} }
void OutOfProcessInstance::SaveToFile(const std::string& token) {
if (!ShouldSaveEdits()) {
engine_->KillFormFocus();
ConsumeSaveToken(token);
pp::PDF::SaveAs(this);
return;
}
SaveToBuffer(token);
}
void OutOfProcessInstance::ConsumeSaveToken(const std::string& token) { void OutOfProcessInstance::ConsumeSaveToken(const std::string& token) {
pp::VarDictionary message; pp::VarDictionary message;
message.Set(kType, kJSConsumeSaveTokenType); message.Set(kType, kJSConsumeSaveTokenType);

@@ -186,7 +186,9 @@ class OutOfProcessInstance : public pp::Instance,
// frame's origin. // frame's origin.
pp::URLLoader CreateURLLoaderInternal(); pp::URLLoader CreateURLLoaderInternal();
void Save(const std::string& token); bool ShouldSaveEdits() const;
void SaveToFile(const std::string& token);
void SaveToBuffer(const std::string& token);
void ConsumeSaveToken(const std::string& token); void ConsumeSaveToken(const std::string& token);
void FormDidOpen(int32_t result); void FormDidOpen(int32_t result);

@@ -412,6 +412,9 @@ class PDFEngine {
// Remove focus from form widgets, consolidating the user input. // Remove focus from form widgets, consolidating the user input.
virtual void KillFormFocus() = 0; virtual void KillFormFocus() = 0;
virtual uint32_t GetLoadedByteSize() = 0;
virtual bool ReadLoadedBytes(uint32_t length, void* buffer) = 0;
}; };
// Interface for exports that wrap the PDF engine. // Interface for exports that wrap the PDF engine.

@@ -535,7 +535,6 @@ gin::IsolateHolder* g_isolate_holder = nullptr;
void SetUpV8() { void SetUpV8() {
const char* recommended = FPDF_GetRecommendedV8Flags(); const char* recommended = FPDF_GetRecommendedV8Flags();
v8::V8::SetFlagsFromString(recommended, strlen(recommended)); v8::V8::SetFlagsFromString(recommended, strlen(recommended));
gin::IsolateHolder::Initialize(gin::IsolateHolder::kNonStrictMode, gin::IsolateHolder::Initialize(gin::IsolateHolder::kNonStrictMode,
gin::IsolateHolder::kStableV8Extras, gin::IsolateHolder::kStableV8Extras,
gin::ArrayBufferAllocator::SharedInstance()); gin::ArrayBufferAllocator::SharedInstance());
@@ -1227,6 +1226,15 @@ void PDFiumEngine::KillFormFocus() {
SetInFormTextArea(false); SetInFormTextArea(false);
} }
uint32_t PDFiumEngine::GetLoadedByteSize() {
return doc_loader_->GetDocumentSize();
}
bool PDFiumEngine::ReadLoadedBytes(uint32_t length, void* buffer) {
DCHECK_LE(length, GetLoadedByteSize());
return doc_loader_->GetBlock(0, length, buffer);
}
void PDFiumEngine::SetFormSelectedText(FPDF_FORMHANDLE form_handle, void PDFiumEngine::SetFormSelectedText(FPDF_FORMHANDLE form_handle,
FPDF_PAGE page) { FPDF_PAGE page) {
unsigned long form_sel_text_len = unsigned long form_sel_text_len =

@@ -136,6 +136,8 @@ class PDFiumEngine : public PDFEngine,
void OnDocumentCanceled() override; void OnDocumentCanceled() override;
void CancelBrowserDownload() override; void CancelBrowserDownload() override;
void KillFormFocus() override; void KillFormFocus() override;
uint32_t GetLoadedByteSize() override;
bool ReadLoadedBytes(uint32_t length, void* buffer) override;
#if defined(PDF_ENABLE_XFA) #if defined(PDF_ENABLE_XFA)
void UpdatePageCount(); void UpdatePageCount();