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:
chrome
browser
resources
component_extension_resources.grd
pdf
ui
webui
print_preview
test
pdf
@ -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_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_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_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" />
|
||||
|
@ -10,6 +10,7 @@ group("closure_compile") {
|
||||
":pdf_resources",
|
||||
"elements/viewer-bookmark:closure_compile",
|
||||
"elements/viewer-error-screen:closure_compile",
|
||||
"elements/viewer-ink-host:closure_compile",
|
||||
"elements/viewer-page-indicator:closure_compile",
|
||||
"elements/viewer-page-selector: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">
|
||||
<template is="dom-if" if="[[pdfAnnotationsEnabled]]">
|
||||
<paper-icon-button id="annotate" icon="pdf:create"
|
||||
on-click="toggleAnnotation"
|
||||
aria-label$="{{strings.tooltipAnnotate}}"
|
||||
title$="{{strings.tooltipAnnotate}}">
|
||||
</paper-icon-button>
|
||||
@ -160,7 +161,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="progress-container">
|
||||
<paper-progress id="progress" value="{{loadProgress}}"></paper-progress>
|
||||
<paper-progress id="progress" value="[[loadProgress]]"></paper-progress>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -9,7 +9,7 @@ Polymer({
|
||||
/**
|
||||
* 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.
|
||||
@ -36,6 +36,14 @@ Polymer({
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@ -44,11 +52,18 @@ Polymer({
|
||||
strings: Object,
|
||||
},
|
||||
|
||||
loadProgressChanged: function() {
|
||||
if (this.loadProgress >= 100) {
|
||||
this.$.pageselector.classList.toggle('invisible', false);
|
||||
this.$.buttons.classList.toggle('invisible', false);
|
||||
this.$.progress.style.opacity = 0;
|
||||
/**
|
||||
* @param {number} newProgress
|
||||
* @param {number} oldProgress
|
||||
* @private
|
||||
*/
|
||||
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() {
|
||||
this.fire('print');
|
||||
},
|
||||
|
||||
toggleAnnotation: function() {
|
||||
this.annotationMode = !this.annotationMode;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
@ -20,6 +20,7 @@ viewer-pdf-toolbar {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
viewer-ink-host,
|
||||
#plugin {
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
|
@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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-selector/viewer-page-selector.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="chrome://resources/js/load_time_data.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="metrics.js"></script>
|
||||
<script src="pdf_viewer.js"></script>
|
||||
|
@ -125,9 +125,7 @@ function PDFViewer(browserApi) {
|
||||
this.isFormFieldFocused_ = false;
|
||||
this.beepCount_ = 0;
|
||||
this.delayedScriptingMessages_ = [];
|
||||
|
||||
/** @private {!Set<string>} */
|
||||
this.pendingTokens_ = new Set();
|
||||
this.loaded_ = new PromiseResolver();
|
||||
|
||||
this.isPrintPreview_ = location.origin === 'chrome://print';
|
||||
this.isPrintPreviewLoadingFinished_ = false;
|
||||
@ -220,10 +218,12 @@ function PDFViewer(browserApi) {
|
||||
} else {
|
||||
this.plugin_.setAttribute('full-frame', '');
|
||||
}
|
||||
|
||||
document.body.appendChild(this.plugin_);
|
||||
|
||||
this.pluginController_ =
|
||||
new PluginController(this.plugin_, this, this.viewport_);
|
||||
this.inkController_ = new InkController(this, this.viewport_);
|
||||
this.currentController_ = this.pluginController_;
|
||||
|
||||
// Setup the button event listeners.
|
||||
@ -252,6 +252,8 @@ function PDFViewer(browserApi) {
|
||||
'print', () => this.currentController_.print());
|
||||
this.toolbar_.addEventListener(
|
||||
'rotate-right', () => this.currentController_.rotateClockwise());
|
||||
this.toolbar_.addEventListener(
|
||||
'annotation-mode-changed', e => this.annotationModeChanged_(e));
|
||||
|
||||
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.
|
||||
*
|
||||
@ -559,9 +607,41 @@ PDFViewer.prototype = {
|
||||
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
|
||||
* message being received from the plugin.
|
||||
* message being received from the content controller.
|
||||
*
|
||||
* @param {number} progress the progress as a percentage.
|
||||
*/
|
||||
@ -577,7 +657,7 @@ PDFViewer.prototype = {
|
||||
this.passwordScreen_.deny();
|
||||
this.passwordScreen_.close();
|
||||
}
|
||||
this.loadState_ = LoadState.FAILED;
|
||||
this.setLoadState_(LoadState.FAILED);
|
||||
this.isPrintPreviewLoadingFinished_ = true;
|
||||
this.sendDocumentLoadedMessage_();
|
||||
} else if (progress == 100) {
|
||||
@ -586,12 +666,14 @@ PDFViewer.prototype = {
|
||||
this.viewport_.position = this.lastViewportPosition_;
|
||||
this.paramsParser_.getViewportFromUrlParams(
|
||||
this.originalUrl_, this.handleURLParams_.bind(this));
|
||||
this.loadState_ = LoadState.SUCCESS;
|
||||
this.setLoadState_(LoadState.SUCCESS);
|
||||
this.sendDocumentLoadedMessage_();
|
||||
while (this.delayedScriptingMessages_.length > 0)
|
||||
this.handleScriptingMessage(this.delayedScriptingMessages_.shift());
|
||||
|
||||
this.toolbarManager_.hideToolbarsAfterTimeout();
|
||||
} else {
|
||||
this.setLoadState_(LoadState.LOADING);
|
||||
}
|
||||
},
|
||||
|
||||
@ -797,7 +879,7 @@ PDFViewer.prototype = {
|
||||
this.pluginController_.postMessage(message.data);
|
||||
return true;
|
||||
case 'resetPrintPreviewMode':
|
||||
this.loadState_ = LoadState.LOADING;
|
||||
this.setLoadState_(LoadState.LOADING);
|
||||
if (!this.inPrintPreviewMode_) {
|
||||
this.inPrintPreviewMode_ = true;
|
||||
this.isUserInitiatedEvent_ = false;
|
||||
@ -1003,7 +1085,8 @@ PDFViewer.prototype = {
|
||||
* Saves the current PDF document to disk.
|
||||
*/
|
||||
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) {
|
||||
// The content controller handled the save internally.
|
||||
return;
|
||||
@ -1057,10 +1140,77 @@ class ContentController {
|
||||
|
||||
/**
|
||||
* 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}}
|
||||
* @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 {
|
||||
@ -1075,7 +1225,7 @@ class PluginController extends ContentController {
|
||||
this.viewer_ = viewer;
|
||||
this.viewport_ = viewport;
|
||||
|
||||
/** @private {!Map<string, Function>} */
|
||||
/** @private {!Map<string, PromiseResolver>} */
|
||||
this.pendingTokens_ = new Map();
|
||||
this.plugin_.addEventListener(
|
||||
'message', e => this.handlePluginMessage_(e), false);
|
||||
@ -1162,13 +1312,30 @@ class PluginController extends ContentController {
|
||||
}
|
||||
|
||||
/** @override */
|
||||
save() {
|
||||
return new Promise(resolve => {
|
||||
const newToken = createToken();
|
||||
this.pendingTokens_.set(newToken, resolve);
|
||||
const force = false;
|
||||
this.postMessage({type: 'save', token: newToken});
|
||||
});
|
||||
save(requireResult) {
|
||||
const resolver = new PromiseResolver();
|
||||
const newToken = createToken();
|
||||
this.pendingTokens_.set(newToken, resolver);
|
||||
this.postMessage({type: 'save', token: newToken, force: requireResult});
|
||||
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
|
||||
*/
|
||||
saveData_(messageData) {
|
||||
if (!(loadTimeData.getBoolean('pdfFormSaveEnabled')))
|
||||
if (!(loadTimeData.getBoolean('pdfFormSaveEnabled') ||
|
||||
loadTimeData.getBoolean('pdfAnnotationsEnabled')))
|
||||
throw new Error('Internal error: save not enabled.');
|
||||
|
||||
// Verify a token that was created by this instance is included to avoid
|
||||
// being spammed.
|
||||
const resolve = this.pendingTokens_.get(messageData.token);
|
||||
const resolver = this.pendingTokens_.get(messageData.token);
|
||||
if (!this.pendingTokens_.delete(messageData.token))
|
||||
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
|
||||
// 100 MB. This cap should be kept in sync with and is also enforced in
|
||||
// pdf/out_of_process_instance.cc.
|
||||
@ -1275,6 +1448,6 @@ class PluginController extends ContentController {
|
||||
throw new Error('Not a PDF file.');
|
||||
}
|
||||
|
||||
resolve(messageData);
|
||||
resolver.resolve(messageData);
|
||||
}
|
||||
}
|
||||
|
@ -436,6 +436,10 @@ void SetupPrintPreviewPlugin(content::WebUIDataSource* source) {
|
||||
source->AddResourcePath(
|
||||
"pdf/elements/viewer-error-screen/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(
|
||||
"pdf/elements/viewer-page-indicator/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
|
||||
// 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([
|
||||
function testAnnotationsEnabled() {
|
||||
const toolbar = document.body.querySelector('#toolbar');
|
||||
@ -10,4 +24,20 @@ chrome.test.runTests([
|
||||
toolbar.shadowRoot.querySelector('#annotate') != null);
|
||||
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)
|
||||
constexpr char kJSSaveType[] = "save";
|
||||
constexpr char kJSToken[] = "token";
|
||||
constexpr char kJSForce[] = "force";
|
||||
// Save Data (Plugin -> Page)
|
||||
constexpr char kJSSaveDataType[] = "saveData";
|
||||
constexpr char kJSFileName[] = "fileName";
|
||||
@ -666,11 +667,17 @@ void OutOfProcessInstance::HandleMessage(const pp::Var& message) {
|
||||
} else if (type == kJSPrintType) {
|
||||
Print();
|
||||
} 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();
|
||||
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) {
|
||||
RotateClockwise();
|
||||
} else if (type == kJSRotateCounterclockwiseType) {
|
||||
@ -1473,37 +1480,58 @@ void OutOfProcessInstance::GetDocumentPassword(
|
||||
PostMessage(message);
|
||||
}
|
||||
|
||||
void OutOfProcessInstance::Save(const std::string& token) {
|
||||
engine_->KillFormFocus();
|
||||
bool OutOfProcessInstance::ShouldSaveEdits() const {
|
||||
return edit_mode_ &&
|
||||
base::FeatureList::IsEnabled(features::kSaveEditedPDFForm);
|
||||
}
|
||||
|
||||
if (!base::FeatureList::IsEnabled(features::kSaveEditedPDFForm) ||
|
||||
!edit_mode_) {
|
||||
ConsumeSaveToken(token);
|
||||
pp::PDF::SaveAs(this);
|
||||
return;
|
||||
}
|
||||
void OutOfProcessInstance::SaveToBuffer(const std::string& token) {
|
||||
engine_->KillFormFocus();
|
||||
|
||||
GURL url(url_);
|
||||
std::string file_name = url.ExtractFileName();
|
||||
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;
|
||||
message.Set(kType, kJSSaveDataType);
|
||||
message.Set(kJSToken, pp::Var(token));
|
||||
message.Set(kJSFileName, pp::Var(file_name));
|
||||
pp::VarArrayBuffer buffer(data.size());
|
||||
std::copy(data.begin(), data.end(), reinterpret_cast<char*>(buffer.Map()));
|
||||
message.Set(kJSDataToSave, buffer);
|
||||
// This will be overwritten if the save is successful.
|
||||
message.Set(kJSDataToSave, pp::Var(pp::Var::Null()));
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
pp::VarDictionary message;
|
||||
message.Set(kType, kJSConsumeSaveTokenType);
|
||||
|
@ -186,7 +186,9 @@ class OutOfProcessInstance : public pp::Instance,
|
||||
// frame's origin.
|
||||
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 FormDidOpen(int32_t result);
|
||||
|
@ -412,6 +412,9 @@ class PDFEngine {
|
||||
|
||||
// Remove focus from form widgets, consolidating the user input.
|
||||
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.
|
||||
|
@ -535,7 +535,6 @@ gin::IsolateHolder* g_isolate_holder = nullptr;
|
||||
void SetUpV8() {
|
||||
const char* recommended = FPDF_GetRecommendedV8Flags();
|
||||
v8::V8::SetFlagsFromString(recommended, strlen(recommended));
|
||||
|
||||
gin::IsolateHolder::Initialize(gin::IsolateHolder::kNonStrictMode,
|
||||
gin::IsolateHolder::kStableV8Extras,
|
||||
gin::ArrayBufferAllocator::SharedInstance());
|
||||
@ -1227,6 +1226,15 @@ void PDFiumEngine::KillFormFocus() {
|
||||
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,
|
||||
FPDF_PAGE page) {
|
||||
unsigned long form_sel_text_len =
|
||||
|
@ -136,6 +136,8 @@ class PDFiumEngine : public PDFEngine,
|
||||
void OnDocumentCanceled() override;
|
||||
void CancelBrowserDownload() override;
|
||||
void KillFormFocus() override;
|
||||
uint32_t GetLoadedByteSize() override;
|
||||
bool ReadLoadedBytes(uint32_t length, void* buffer) override;
|
||||
|
||||
#if defined(PDF_ENABLE_XFA)
|
||||
void UpdatePageCount();
|
||||
|
Reference in New Issue
Block a user