blob: 554f88b2b778cb56b164bb7e5f1ac43f6fec5222 [file] [log] [blame]
// Copyright 2013 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 './elements/viewer-error-screen.js';
import './elements/viewer-password-screen.js';
import './elements/viewer-pdf-toolbar.js';
import './elements/viewer-zoom-toolbar.js';
import './elements/shared-vars.js';
// <if expr="chromeos">
import './elements/viewer-ink-host.js';
import './elements/viewer-form-warning.js';
// </if>
import './pdf_viewer_shared_style.js';
import 'chrome://resources/cr_elements/hidden_style_css.m.js';
import 'chrome://resources/cr_elements/shared_vars_css.m.js';
import {assert, assertNotReached} from 'chrome://resources/js/assert.m.js';
import {FocusOutlineManager} from 'chrome://resources/js/cr/ui/focus_outline_manager.m.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {hasKeyModifiers, listenOnce} from 'chrome://resources/js/util.m.js';
import {html} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {Bookmark} from './bookmark_type.js';
import {BrowserApi} from './browser_api.js';
import {Attachment, FittingType, Point, SaveRequestType} from './constants.js';
import {ViewerPdfSidenavElement} from './elements/viewer-pdf-sidenav.js';
import {ViewerPdfToolbarNewElement} from './elements/viewer-pdf-toolbar-new.js';
import {ViewerThumbnailElement} from './elements/viewer-thumbnail.js';
// <if expr="chromeos">
import {InkController} from './ink_controller.js';
//</if>
import {LocalStorageProxyImpl} from './local_storage_proxy.js';
import {PDFMetrics, UserAction} from './metrics.js';
import {NavigatorDelegateImpl, PdfNavigator, WindowOpenDisposition} from './navigator.js';
import {OpenPdfParamsParser} from './open_pdf_params_parser.js';
import {DeserializeKeyEvent, LoadState, SerializeKeyEvent} from './pdf_scripting_api.js';
import {PDFViewerBaseElement} from './pdf_viewer_base.js';
import {DestinationMessageData, DocumentDimensionsMessageData, shouldIgnoreKeyEvents} from './pdf_viewer_utils.js';
import {ToolbarManager} from './toolbar_manager.js';
/**
* @typedef {{
* type: string,
* url: string,
* disposition: !WindowOpenDisposition,
* }}
*/
let NavigateMessageData;
/**
* @typedef {{
* type: string,
* title: string,
* attachments: !Array<!Attachment>,
* bookmarks: !Array<!Bookmark>,
* canSerializeDocument: boolean,
* }}
*/
let MetadataMessageData;
/**
* @typedef {{
* type: string,
* messageId: string,
* page: number,
* }}
*/
let GetThumbnailMessageData;
/**
* @typedef {{
* hasUnsavedChanges: (boolean|undefined),
* fileName: string,
* dataToSave: !ArrayBuffer
* }}
*/
let RequiredSaveResult;
/**
* Return the filename component of a URL, percent decoded if possible.
* Exported for tests.
* @param {string} url The URL to get the filename from.
* @return {string} The filename component.
*/
export function getFilenameFromURL(url) {
// Ignore the query and fragment.
const mainUrl = url.split(/#|\?/)[0];
const components = mainUrl.split(/\/|\\/);
const filename = components[components.length - 1];
try {
return decodeURIComponent(filename);
} catch (e) {
if (e instanceof URIError) {
return filename;
}
throw e;
}
}
/** @type {string} */
const LOCAL_STORAGE_SIDENAV_COLLAPSED_KEY = 'sidenavCollapsed';
export class PDFViewerElement extends PDFViewerBaseElement {
static get is() {
return 'pdf-viewer';
}
static get template() {
return html`{__html_template__}`;
}
static get properties() {
return {
annotationAvailable_: {
type: Boolean,
computed: 'computeAnnotationAvailable_(' +
'hadPassword_, clockwiseRotations_, canSerializeDocument_,' +
'twoUpViewEnabled_)',
},
annotationMode_: {
type: Boolean,
value: false,
},
attachments_: Array,
bookmarks_: Array,
clockwiseRotations_: Number,
documentHasFocus_: {
type: Boolean,
value: false,
},
hasEdits_: {
type: Boolean,
value: false,
},
hasEnteredAnnotationMode_: {
type: Boolean,
value: false,
},
hadPassword_: Boolean,
canSerializeDocument_: Boolean,
title_: String,
sidenavCollapsed_: Boolean,
twoUpViewEnabled_: Boolean,
isFormFieldFocused_: Boolean,
pdfViewerUpdateEnabled_: Boolean,
docLength_: Number,
// <if expr="chromeos">
inkController_: Object,
// </if>
loadProgress_: Number,
pageNo_: Number,
pdfFormSaveEnabled_: Boolean,
pdfAnnotationsEnabled_: Boolean,
printingEnabled_: Boolean,
viewportZoom_: Number,
};
}
constructor() {
super();
// Polymer properties
/** @private {boolean} */
this.annotationAvailable_;
/** @private {boolean} */
this.annotationMode_ = false;
/** @private {!Array<!Attachment>} */
this.attachments_ = [];
/** @private {!Array<!Bookmark>} */
this.bookmarks_ = [];
/** @private {number} */
this.clockwiseRotations_ = 0;
/** @private {boolean} */
this.documentHasFocus_ = false;
/** @private {boolean} */
this.hasEdits_ = false;
/** @private {boolean} */
this.hasEnteredAnnotationMode_ = false;
/** @private {boolean} */
this.hadPassword_ = false;
/** @private {boolean} */
this.canSerializeDocument_ = false;
/** @private {string} */
this.title_ = '';
/** @private {boolean} */
this.twoUpViewEnabled_ = false;
/** @private {boolean} */
this.isFormFieldFocused_ = false;
// <if expr="chromeos">
/** @private {?InkController} */
this.inkController_ = null;
// </if>
/** @private {boolean} */
this.pdfAnnotationsEnabled_ = false;
/** @private {boolean} */
this.pdfFormSaveEnabled_ = false;
/** @private {boolean} */
this.printingEnabled_ = false;
/** @private {number} */
this.viewportZoom_ = 1;
// Non-Polymer properties
/** @type {number} */
this.beepCount = 0;
/** @private {boolean} */
this.hadPassword_ = false;
/** @private {boolean} */
this.toolbarEnabled_ = false;
/** @private {?ToolbarManager} */
this.toolbarManager_ = null;
/** @private {?PdfNavigator} */
this.navigator_ = null;
/** @private {string} */
this.title_ = '';
/**
* The number of pages in the PDF document.
* @private {number}
*/
this.docLength_;
/**
* The number of the page being viewed (1-based).
* @private {number}
*/
this.pageNo_;
/**
* The current loading progress of the PDF document (0 - 100).
* @private {number}
*/
this.loadProgress_;
/** @private {boolean} */
this.pdfViewerUpdateEnabled_ =
document.documentElement.hasAttribute('pdf-viewer-update-enabled');
/** @private {boolean} */
this.sidenavCollapsed_ = false;
/**
* The state to restore sidenavCollapsed_ to after exiting annotation mode.
* @private {boolean}
*/
this.sidenavRestoreState_ = false;
if (this.pdfViewerUpdateEnabled_) {
// TODO(dpapad): Add tests after crbug.com/1111459 is fixed.
this.sidenavCollapsed_ = Boolean(Number.parseInt(
LocalStorageProxyImpl.getInstance().getItem(
LOCAL_STORAGE_SIDENAV_COLLAPSED_KEY),
10));
}
FocusOutlineManager.forDocument(document);
}
/** @override */
getToolbarHeight() {
assert(this.paramsParser);
this.toolbarEnabled_ =
this.paramsParser.shouldShowToolbar(this.originalUrl);
// The toolbar does not need to be manually accounted in the
// PDFViewerUpdate UI.
if (this.pdfViewerUpdateEnabled_) {
return 0;
}
return this.toolbarEnabled_ ? MATERIAL_TOOLBAR_HEIGHT : 0;
}
/** @override */
hasFixedToolbar() {
return this.pdfViewerUpdateEnabled_;
}
/** @override */
getContent() {
return /** @type {!HTMLDivElement} */ (this.$$('#content'));
}
/** @override */
getSizer() {
return /** @type {!HTMLDivElement} */ (this.$$('#sizer'));
}
/** @override */
getErrorScreen() {
return /** @type {!ViewerErrorScreenElement} */ (this.$$('#error-screen'));
}
/**
* @return {!ViewerPdfToolbarElement}
* @private
*/
getToolbar_() {
return /** @type {!ViewerPdfToolbarElement} */ (this.$$('#toolbar'));
}
/**
* @return {!ViewerPdfToolbarNewElement}
* @private
*/
getToolbarNew_() {
assert(this.pdfViewerUpdateEnabled_);
return /** @type {!ViewerPdfToolbarNewElement} */ (this.$$('#toolbar'));
}
/**
* @return {!ViewerZoomToolbarElement}
* @private
*/
getZoomToolbar_() {
return /** @type {!ViewerZoomToolbarElement} */ (this.$$('#zoom-toolbar'));
}
/** @override */
getBackgroundColor() {
return BACKGROUND_COLOR;
}
/** @param {!BrowserApi} browserApi */
init(browserApi) {
super.init(browserApi);
// <if expr="chromeos">
this.inkController_ = new InkController(
this.viewport, /** @type {!HTMLDivElement} */ (this.getContent()));
this.tracker.add(
this.inkController_.getEventTarget(), 'has-unsaved-changes',
() => chrome.mimeHandlerPrivate.setShowBeforeUnloadDialog(true));
// </if>
this.title_ = getFilenameFromURL(this.originalUrl);
if (this.toolbarEnabled_) {
this.getToolbar_().hidden = false;
}
if (!this.pdfViewerUpdateEnabled_) {
this.toolbarManager_ = new ToolbarManager(
window, this.getToolbar_(), this.getZoomToolbar_());
}
// Setup the keyboard event listener.
document.addEventListener(
'keydown',
e => this.handleKeyEvent_(/** @type {!KeyboardEvent} */ (e)));
const tabId = this.browserApi.getStreamInfo().tabId;
this.navigator_ = new PdfNavigator(
this.originalUrl, this.viewport,
/** @type {!OpenPdfParamsParser} */ (this.paramsParser),
new NavigatorDelegateImpl(tabId));
// Listen for save commands from the browser.
if (chrome.mimeHandlerPrivate && chrome.mimeHandlerPrivate.onSave) {
chrome.mimeHandlerPrivate.onSave.addListener(url => this.onSave_(url));
}
}
/**
* Helper for handleKeyEvent_ dealing with events that control toolbars.
* @param {!KeyboardEvent} e the event to handle.
* @private
*/
handleToolbarKeyEvent_(e) {
if (this.pdfViewerUpdateEnabled_) {
if (e.key === '\\' && e.ctrlKey) {
this.getToolbarNew_().fitToggle();
}
// TODO: Add handling for additional relevant hotkeys for the new unified
// toolbar.
return;
}
switch (e.key) {
case 'Tab':
this.toolbarManager_.showToolbarsForKeyboardNavigation();
return;
case 'Escape':
this.toolbarManager_.hideSingleToolbarLayer();
return;
case 'g':
if (this.toolbarEnabled_ && (e.ctrlKey || e.metaKey) && e.altKey) {
this.toolbarManager_.showToolbars();
this.getToolbar_().selectPageNumber();
}
return;
case '\\':
if (e.ctrlKey) {
this.getZoomToolbar_().fitToggleFromHotKey();
}
return;
}
// Show toolbars as a fallback.
if (!(e.shiftKey || e.ctrlKey || e.altKey)) {
this.toolbarManager_.showToolbars();
}
}
/**
* Handle key events. These may come from the user directly or via the
* scripting API.
* @param {!KeyboardEvent} e the event to handle.
* @private
*/
handleKeyEvent_(e) {
if (shouldIgnoreKeyEvents(document.activeElement) || e.defaultPrevented) {
return;
}
if (!this.pdfViewerUpdateEnabled_) {
this.toolbarManager_.hideToolbarsAfterTimeout();
}
// Let the viewport handle directional key events.
if (this.viewport.handleDirectionalKeyEvent(e, this.isFormFieldFocused_)) {
return;
}
switch (e.key) {
case 'a':
if (e.ctrlKey || e.metaKey) {
this.pluginController.selectAll();
// Since we do selection ourselves.
e.preventDefault();
}
return;
case '[':
if (e.ctrlKey) {
this.rotateCounterclockwise();
}
return;
case ']':
if (e.ctrlKey) {
this.rotateClockwise();
}
return;
}
// Handle toolbar related key events.
this.handleToolbarKeyEvent_(e);
}
// <if expr="chromeos">
/** @private */
onResetView_() {
if (this.twoUpViewEnabled_) {
this.currentController.setTwoUpView(false);
this.twoUpViewEnabled_ = false;
}
if (this.isRotated_()) {
const rotations = this.viewport.getClockwiseRotations();
switch (rotations) {
case 1:
super.rotateCounterclockwise();
break;
case 2:
super.rotateCounterclockwise();
super.rotateCounterclockwise();
break;
case 3:
super.rotateClockwise();
break;
}
this.clockwiseRotations_ = 0;
}
}
/**
* @return {!Promise} Resolves when the sidenav animation is complete.
* @private
*/
waitForSidenavTransition_() {
return new Promise(resolve => {
listenOnce(
/** @type {!ViewerPdfSidenavElement} */ (
this.shadowRoot.querySelector('#sidenav-container')),
'transitionend', e => resolve());
});
}
/**
* @return {!Promise} Resolves when the sidenav is restored to
* |sidenavRestoreState_|, after having been closed for annotation mode.
* @private
*/
restoreSidenav_() {
this.sidenavCollapsed_ = this.sidenavRestoreState_;
return this.sidenavCollapsed_ ? Promise.resolve() :
this.waitForSidenavTransition_();
}
/**
* Handles the annotation mode being toggled on or off.
* @param {!CustomEvent<boolean>} e
* @private
*/
async onAnnotationModeToggled_(e) {
const annotationMode = e.detail;
if (annotationMode) {
// Enter annotation mode.
assert(this.currentController === this.pluginController);
// TODO(dstockwell): set plugin read-only, begin transition
this.updateProgress(0);
if (this.pdfViewerUpdateEnabled_) {
this.sidenavRestoreState_ = this.sidenavCollapsed_;
this.sidenavCollapsed_ = true;
if (!this.sidenavRestoreState_) {
// Wait for the animation before proceeding.
await this.waitForSidenavTransition_();
}
}
// TODO(dstockwell): handle save failure
const saveResult =
await this.pluginController.save(SaveRequestType.ANNOTATION);
// Data always exists when save is called with requestType = ANNOTATION.
const result = /** @type {!RequiredSaveResult} */ (saveResult);
if (result.hasUnsavedChanges) {
assert(!loadTimeData.getBoolean('pdfFormSaveEnabled'));
try {
await this.$$('#form-warning').show();
} catch (e) {
// The user aborted entering annotation mode. Revert to the plugin.
this.getToolbar_().annotationMode = false;
this.updateProgress(100);
return;
}
}
PDFMetrics.record(UserAction.ENTER_ANNOTATION_MODE);
this.annotationMode_ = true;
this.hasEnteredAnnotationMode_ = 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.
PDFMetrics.record(UserAction.EXIT_ANNOTATION_MODE);
assert(this.currentController === this.inkController_);
// TODO(dstockwell): set ink read-only, begin transition
this.updateProgress(0);
this.annotationMode_ = false;
// 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 saveResult =
await this.inkController_.save(SaveRequestType.ANNOTATION);
// Data always exists when save is called with requestType = ANNOTATION.
const result = /** @type {!RequiredSaveResult} */ (saveResult);
if (this.pdfViewerUpdateEnabled_) {
await this.restoreSidenav_();
}
await this.pluginController.load(result.fileName, result.dataToSave);
// Ensure the plugin gets the initial viewport.
this.pluginController.afterZoom();
}
}
/**
* Exits annotation mode if active.
* @return {Promise<void>}
*/
async exitAnnotationMode_() {
if (!this.getToolbar_().annotationMode) {
return;
}
this.getToolbar_().toggleAnnotation();
this.annotationMode_ = false;
if (this.pdfViewerUpdateEnabled_) {
await this.restoreSidenav_();
}
await this.loaded;
}
// </if>
/**
* @param {!Event} e
* @private
*/
onDisplayAnnotationsChanged_(e) {
this.currentController.setDisplayAnnotations(e.detail);
}
/**
* @param {!Event} e
* @private
*/
onScroll_(e) {
if (this.currentController === this.pluginController &&
!this.annotationMode_) {
this.pluginController.updateScroll(
e.target.scrollLeft, e.target.scrollTop);
}
}
/** @override */
onFitToChanged(e) {
super.onFitToChanged(e);
if (this.pdfViewerUpdateEnabled_) {
return;
}
if (e.detail === FittingType.FIT_TO_PAGE ||
e.detail === FittingType.FIT_TO_HEIGHT) {
this.toolbarManager_.forceHideTopToolbar();
}
}
/**
* Changes two up view mode for the controller. Controller will trigger
* layout update later, which will update the viewport accordingly.
* @param {!CustomEvent<boolean>} e
* @private
*/
onTwoUpViewChanged_(e) {
this.twoUpViewEnabled_ = e.detail;
this.currentController.setTwoUpView(this.twoUpViewEnabled_);
if (!this.pdfViewerUpdateEnabled_) {
this.toolbarManager_.forceHideTopToolbar();
}
PDFMetrics.recordTwoUpViewEnabled(this.twoUpViewEnabled_);
}
/**
* Moves the viewport to a point in a page. Called back after a
* 'transformPagePointReply' is returned from the plugin.
* @param {string} origin Identifier for the caller for logging purposes.
* @param {number} page The index of the page to go to. zero-based.
* @param {Point} message Message received from the plugin containing the
* x and y to navigate to in screen coordinates.
* @private
*/
goToPageAndXY_(origin, page, message) {
this.viewport.goToPageAndXY(page, message.x, message.y);
if (origin === 'bookmark') {
PDFMetrics.record(UserAction.FOLLOW_BOOKMARK);
}
}
/** @return {!Viewport} The viewport. Used for testing. */
/** @return {!Array<!Bookmark>} The bookmarks. Used for testing. */
get bookmarks() {
return this.bookmarks_;
}
/** @override */
setLoadState(loadState) {
super.setLoadState(loadState);
if (loadState === LoadState.FAILED) {
const passwordScreen = this.$$('#password-screen');
if (passwordScreen && passwordScreen.active) {
passwordScreen.deny();
passwordScreen.close();
}
}
}
/** @override */
updateProgress(progress) {
if (this.toolbarEnabled_) {
this.loadProgress_ = progress;
}
super.updateProgress(progress);
if (progress === 100 && !this.pdfViewerUpdateEnabled_) {
this.toolbarManager_.hideToolbarsAfterTimeout();
}
}
/**
* An event handler for handling password-submitted events. These are fired
* when an event is entered into the password screen.
* @param {!CustomEvent<{password: string}>} event a password-submitted event.
* @private
*/
onPasswordSubmitted_(event) {
this.pluginController.getPasswordComplete(event.detail.password);
}
/** @override */
updateUIForViewportChange() {
if (!this.pdfViewerUpdateEnabled_) {
this.getZoomToolbar_().shiftForScrollbars(
this.viewport.documentHasScrollbars(), this.viewport.scrollbarWidth);
}
// Update the page indicator.
if (this.toolbarEnabled_) {
const visiblePage = this.viewport.getMostVisiblePage();
this.pageNo_ = visiblePage + 1;
}
this.currentController.viewportChanged();
}
/** @override */
handleStrings(strings) {
super.handleStrings(strings);
this.pdfAnnotationsEnabled_ =
loadTimeData.getBoolean('pdfAnnotationsEnabled');
this.pdfFormSaveEnabled_ = loadTimeData.getBoolean('pdfFormSaveEnabled');
this.printingEnabled_ = loadTimeData.getBoolean('printingEnabled');
}
/** @override */
handleScriptingMessage(message) {
super.handleScriptingMessage(message);
if (this.delayScriptingMessage(message)) {
return;
}
switch (message.data.type.toString()) {
case 'getSelectedText':
this.pluginController.getSelectedText().then(
this.handleSelectedTextReply.bind(this));
break;
case 'getThumbnail':
const getThumbnailData =
/** @type {GetThumbnailMessageData} */ (message.data);
const page = getThumbnailData.page;
this.pluginController.requestThumbnail(page).then(
this.sendScriptingMessage.bind(this));
break;
case 'print':
this.pluginController.print();
break;
case 'selectAll':
this.pluginController.selectAll();
break;
}
}
/** @override */
handlePluginMessage(e) {
const data = e.detail;
switch (data.type.toString()) {
case 'beep':
this.handleBeep_();
return;
case 'documentDimensions':
this.setDocumentDimensions(
/** @type {!DocumentDimensionsMessageData} */ (data));
return;
case 'getPassword':
this.handlePasswordRequest_();
return;
case 'loadProgress':
this.updateProgress(
/** @type {{ progress: number }} */ (data).progress);
return;
case 'navigate':
const navigateData = /** @type {!NavigateMessageData} */ (data);
this.handleNavigate_(navigateData.url, navigateData.disposition);
return;
case 'navigateToDestination':
const destinationData = /** @type {!DestinationMessageData} */ (data);
this.viewport.handleNavigateToDestination(
destinationData.page, destinationData.x, destinationData.y,
destinationData.zoom);
return;
case 'metadata':
this.setDocumentMetadata_(/** @type {!MetadataMessageData} */ (data));
return;
case 'setIsEditing':
// Editing mode can only be entered once, and cannot be exited.
this.hasEdits_ = true;
return;
case 'setIsSelecting':
this.viewportScroller.setEnableScrolling(
/** @type {{ isSelecting: boolean }} */ (data).isSelecting);
return;
case 'formFocusChange':
this.isFormFieldFocused_ =
/** @type {{ focused: boolean }} */ (data).focused;
return;
case 'touchSelectionOccurred':
this.sendScriptingMessage({
type: 'touchSelectionOccurred',
});
return;
case 'documentFocusChanged':
this.documentHasFocus_ =
/** @type {{ hasFocus: boolean }} */ (data).hasFocus;
return;
}
assertNotReached('Unknown message type received: ' + data.type);
}
/** @override */
forceFit(view) {
if (!this.pdfViewerUpdateEnabled_) {
if (view === FittingType.FIT_TO_PAGE ||
view === FittingType.FIT_TO_HEIGHT) {
this.toolbarManager_.forceHideTopToolbar();
}
this.getZoomToolbar_().forceFit(view);
} else {
this.getToolbarNew_().forceFit(view);
}
}
/** @override */
afterZoom(viewportZoom) {
this.viewportZoom_ = viewportZoom;
}
/** @override */
setDocumentDimensions(documentDimensions) {
super.setDocumentDimensions(documentDimensions);
// If we received the document dimensions, the password was good so we
// can dismiss the password screen.
const passwordScreen = this.$$('#password-screen');
if (passwordScreen && passwordScreen.active) {
passwordScreen.close();
}
if (this.toolbarEnabled_) {
this.docLength_ = this.documentDimensions.pageDimensions.length;
}
}
/**
* Handles a beep request from the current controller.
* @private
*/
handleBeep_() {
// Beeps are annoying, so just track count for now.
this.beepCount += 1;
}
/**
* Handles a password request from the current controller.
* @private
*/
handlePasswordRequest_() {
// If the password screen isn't up, put it up. Otherwise we're
// responding to an incorrect password so deny it.
const passwordScreen = this.$$('#password-screen');
assert(passwordScreen);
if (!passwordScreen.active) {
this.hadPassword_ = true;
passwordScreen.show();
} else {
passwordScreen.deny();
}
}
/**
* Handles a navigation request from the current controller.
* @param {string} url
* @param {!WindowOpenDisposition} disposition
* @private
*/
handleNavigate_(url, disposition) {
this.navigator_.navigate(url, disposition);
}
/**
* Sets document metadata from the current controller.
* @param {!MetadataMessageData} metadata
* @private
*/
setDocumentMetadata_(metadata) {
this.title_ = metadata.title || getFilenameFromURL(this.originalUrl);
document.title = this.title_;
this.attachments_ = metadata.attachments;
this.bookmarks_ = metadata.bookmarks;
this.canSerializeDocument_ = metadata.canSerializeDocument;
}
/**
* An event handler for when the browser tells the PDF Viewer to perform a
* save on the attachment at a certain index. Callers of this function must
* be responsible for checking whether the attachment size is valid for
* downloading.
* @param {!CustomEvent<number>} e The event which contains the index of
* attachment to be downloaded.
* @private
*/
async onSaveAttachment_(e) {
const index = e.detail;
const size = this.attachments_[index].size;
assert(size !== -1);
let dataArray = [];
// If the attachment size is 0, skip requesting the backend to fetch the
// attachment data.
if (size !== 0) {
const result = await this.currentController.saveAttachment(index);
// Cap the PDF attachment size at 100 MB. This cap should be kept in sync
// with and is also enforced in pdf/out_of_process_instance.cc.
const MAX_FILE_SIZE = 100 * 1000 * 1000;
const bufView = new Uint8Array(result.dataToSave);
assert(
bufView.length <= MAX_FILE_SIZE,
`File too large to be saved: ${bufView.length} bytes.`);
assert(
bufView.length === size,
`Received attachment size does not match its expected value: ${
size} bytes.`);
dataArray = [result.dataToSave];
}
const blob = new Blob(dataArray);
const fileName = this.attachments_[index].name;
chrome.fileSystem.chooseEntry(
{type: 'saveFile', suggestedName: fileName}, entry => {
if (chrome.runtime.lastError) {
if (chrome.runtime.lastError.message !== 'User cancelled') {
console.error(
'chrome.fileSystem.chooseEntry failed: ' +
chrome.runtime.lastError.message);
}
return;
}
entry.createWriter(writer => {
writer.write(blob);
// Unblock closing the window now that the user has saved
// successfully.
chrome.mimeHandlerPrivate.setShowBeforeUnloadDialog(false);
});
});
}
/**
* An event handler for when the browser tells the PDF Viewer to perform a
* save.
* @param {string} streamUrl unique identifier for a PDF Viewer instance.
* @private
*/
async onSave_(streamUrl) {
if (streamUrl !== this.browserApi.getStreamInfo().streamUrl) {
return;
}
let saveMode;
if (this.hasEnteredAnnotationMode_) {
saveMode = SaveRequestType.ANNOTATION;
} else if (
loadTimeData.getBoolean('pdfFormSaveEnabled') && this.hasEdits_) {
saveMode = SaveRequestType.EDITED;
} else {
saveMode = SaveRequestType.ORIGINAL;
}
this.save_(saveMode);
}
/**
* @param {!CustomEvent<!SaveRequestType>} e
* @private
*/
onToolbarSave_(e) {
this.save_(e.detail);
}
/**
* @param {!CustomEvent<!{page: number, origin: string}>} e
* @private
*/
onChangePage_(e) {
this.viewport.goToPage(e.detail.page);
if (e.detail.origin === 'bookmark') {
PDFMetrics.record(UserAction.FOLLOW_BOOKMARK);
} else if (e.detail.origin === 'pageselector') {
PDFMetrics.record(UserAction.PAGE_SELECTOR_NAVIGATE);
} else if (e.detail.origin === 'thumbnail') {
PDFMetrics.record(UserAction.THUMBNAIL_NAVIGATE);
}
}
/**
* @param {!CustomEvent<!{
* page: number, origin: string, x: number, y: number}>} e
* @private
*/
onChangePageAndXy_(e) {
const point = this.viewport.convertPageToScreen(e.detail.page, e.detail);
this.goToPageAndXY_(e.detail.origin, e.detail.page, point);
}
/**
* @param {!CustomEvent<string>} e
* @private
*/
onDropdownOpened_(e) {
if (e.detail === 'bookmarks') {
PDFMetrics.record(UserAction.OPEN_BOOKMARKS_PANEL);
}
}
/**
* @param {!CustomEvent<!{newtab: boolean, uri: string}>} e
* @private
*/
onNavigate_(e) {
const disposition = e.detail.newtab ?
WindowOpenDisposition.NEW_BACKGROUND_TAB :
WindowOpenDisposition.CURRENT_TAB;
this.navigator_.navigate(e.detail.uri, disposition);
}
/**
* @param {!CustomEvent<!ViewerThumbnailElement>} e
* @private
*/
onPaintThumbnail_(e) {
assert(this.currentController === this.pluginController);
assert(!this.annotationMode_);
const thumbnail = e.detail;
this.pluginController.requestThumbnail(thumbnail.pageNumber)
.then(response => {
const array = new Uint8ClampedArray(response.imageData);
const imageData = new ImageData(array, response.width);
thumbnail.image = imageData;
});
}
/** @private */
onSidenavToggleClick_() {
assert(this.pdfViewerUpdateEnabled_);
this.sidenavCollapsed_ = !this.sidenavCollapsed_;
LocalStorageProxyImpl.getInstance().setItem(
LOCAL_STORAGE_SIDENAV_COLLAPSED_KEY, this.sidenavCollapsed_ ? 1 : 0);
}
/**
* Saves the current PDF document to disk.
* @param {SaveRequestType} requestType The type of save request.
* @private
*/
async save_(requestType) {
this.recordSaveMetrics_(requestType);
// If we have entered annotation mode we must require the local
// contents to ensure annotations are saved, unless the user specifically
// requested the original document. Otherwise we would save the cached
// remote copy without annotations.
//
// Always send requests of type ORIGINAL to the plugin controller, not the
// ink controller. The ink controller always saves the edited document.
// TODO(dstockwell): Report an error to user if this fails.
let result;
if (requestType !== SaveRequestType.ORIGINAL || !this.annotationMode_) {
result = await this.currentController.save(requestType);
} else {
// <if expr="chromeos">
// Request type original in annotation mode --> need to exit annotation
// mode before saving. See https://crbug.com/919364.
await this.exitAnnotationMode_();
assert(!this.annotationMode_);
result = await this.currentController.save(SaveRequestType.ORIGINAL);
// </if>
}
if (result == null) {
// The content controller handled the save internally.
return;
}
// Make sure file extension is .pdf, avoids dangerous extensions.
let fileName = result.fileName;
if (!fileName.toLowerCase().endsWith('.pdf')) {
fileName = fileName + '.pdf';
}
chrome.fileSystem.chooseEntry(
{
type: 'saveFile',
accepts: [{description: '*.pdf', extensions: ['pdf']}],
suggestedName: fileName
},
entry => {
if (chrome.runtime.lastError) {
if (chrome.runtime.lastError.message !== 'User cancelled') {
console.error(
'chrome.fileSystem.chooseEntry failed: ' +
chrome.runtime.lastError.message);
}
return;
}
entry.createWriter(writer => {
writer.write(
new Blob([result.dataToSave], {type: 'application/pdf'}));
// Unblock closing the window now that the user has saved
// successfully.
chrome.mimeHandlerPrivate.setShowBeforeUnloadDialog(false);
});
});
// <if expr="chromeos">
// Saving in Annotation mode is destructive: crbug.com/919364
this.exitAnnotationMode_();
// </if>
}
/**
* Records metrics for saving PDFs.
* @param {SaveRequestType} requestType The type of save request.
* @private
*/
recordSaveMetrics_(requestType) {
PDFMetrics.record(UserAction.SAVE);
switch (requestType) {
case SaveRequestType.ANNOTATION:
PDFMetrics.record(UserAction.SAVE_WITH_ANNOTATION);
break;
case SaveRequestType.ORIGINAL:
PDFMetrics.record(
this.hasEdits_ ? UserAction.SAVE_ORIGINAL :
UserAction.SAVE_ORIGINAL_ONLY);
break;
case SaveRequestType.EDITED:
PDFMetrics.record(UserAction.SAVE_EDITED);
break;
}
}
/** @private */
async onPrint_() {
PDFMetrics.record(UserAction.PRINT);
// <if expr="chromeos">
await this.exitAnnotationMode_();
// </if>
this.currentController.print();
}
/**
* Updates the toolbar's annotation available flag depending on current
* conditions.
* @return {boolean} Whether annotations are available.
* @private
*/
computeAnnotationAvailable_() {
return this.canSerializeDocument_ && !this.hadPassword_;
}
/**
* @return {boolean} Whether the PDF contents are rotated.
* @private
*/
isRotated_() {
return this.clockwiseRotations_ !== 0;
}
/** @override */
rotateClockwise() {
super.rotateClockwise();
this.clockwiseRotations_ = this.viewport.getClockwiseRotations();
}
/** @override */
rotateCounterclockwise() {
super.rotateCounterclockwise();
this.clockwiseRotations_ = this.viewport.getClockwiseRotations();
}
}
/**
* The height of the toolbar along the top of the page. The document will be
* shifted down by this much in the viewport.
* @type {number}
*/
const MATERIAL_TOOLBAR_HEIGHT = 56;
/**
* Minimum height for the material toolbar to show (px). Should match the media
* query in index-material.css. If the window is smaller than this at load,
* leave no space for the toolbar.
* @type {number}
*/
const TOOLBAR_WINDOW_MIN_HEIGHT = 250;
/**
* The background color used for the regular viewer.
* @type {string}
*/
const BACKGROUND_COLOR = '0xFF525659';
customElements.define(PDFViewerElement.is, PDFViewerElement);