// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
type ViewportChangedCallback =
(pageX: number, pageY: number, pageWidth: number, viewportWidth: number,
viewportHeight: number) => void;
export interface PdfPlugin extends HTMLIFrameElement {
darkModeChanged(darkMode: boolean): void;
hideToolbar(): void;
loadPreviewPage(url: string, index: number): void;
resetPrintPreviewMode(
url: string, color: boolean, pages: number[], modifiable: boolean): void;
scrollPosition(x: number, y: number): void;
sendKeyEvent(e: KeyboardEvent): void;
setKeyEventCallback(callback: (e: KeyboardEvent) => void): void;
setLoadCompleteCallback(callback: (loaded: boolean) => void): void;
setViewportChangedCallback(callback: ViewportChangedCallback): void;
}
export interface SerializedKeyEvent {
keyCode: number;
code: string;
key: string;
shiftKey: boolean;
ctrlKey: boolean;
altKey: boolean;
metaKey: boolean;
}
/**
* Turn a dictionary received from postMessage into a key event.
* @param dict A dictionary representing the key event.
*/
export function deserializeKeyEvent(dict: SerializedKeyEvent): KeyboardEvent {
const e = new KeyboardEvent('keydown', {
bubbles: true,
cancelable: true,
key: dict.key,
code: dict.code,
keyCode: dict.keyCode,
shiftKey: dict.shiftKey,
ctrlKey: dict.ctrlKey,
altKey: dict.altKey,
metaKey: dict.metaKey,
});
return e;
}
/**
* Turn a key event into a dictionary which can be sent over postMessage.
* @return A dictionary representing the key event.
*/
export function serializeKeyEvent(event: KeyboardEvent): SerializedKeyEvent {
return {
keyCode: event.keyCode,
code: event.code,
key: event.key,
shiftKey: event.shiftKey,
ctrlKey: event.ctrlKey,
altKey: event.altKey,
metaKey: event.metaKey,
};
}
/**
* An enum containing a value specifying whether the PDF is currently loading,
* has finished loading or failed to load.
*/
export enum LoadState {
LOADING = 'loading',
SUCCESS = 'success',
FAILED = 'failed',
}
// Provides a scripting interface to the PDF viewer so that it can be customized
// by things like print preview.
export class PdfScriptingApi {
private loadState_: LoadState = LoadState.LOADING;
private pendingScriptingMessages_: Array<{type: string}> = [];
private viewportChangedCallback_?: ViewportChangedCallback;
private loadCompleteCallback_?: (completed: boolean) => void;
private selectedTextCallback_?: ((text: string) => void)|null;
private keyEventCallback_?: (e: KeyboardEvent) => void;
private plugin_: Window|null = null;
/**
* @param window the window of the page containing the pdf viewer.
* @param plugin the plugin element containing the pdf viewer.
*/
constructor(window: Window, plugin: Window|null) {
this.setPlugin(plugin);
window.addEventListener('message', event => {
if (event.origin !==
'chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai' &&
event.origin !== 'chrome://print') {
console.error(
'Received message that was not from the extension: ' + event);
return;
}
switch (event.data.type) {
case 'viewport':
const viewportData = event.data;
if (this.viewportChangedCallback_) {
this.viewportChangedCallback_(
viewportData.pageX, viewportData.pageY, viewportData.pageWidth,
viewportData.viewportWidth, viewportData.viewportHeight);
}
break;
case 'documentLoaded': {
const data = event.data;
this.loadState_ = data.load_state;
if (this.loadCompleteCallback_) {
this.loadCompleteCallback_(this.loadState_ === LoadState.SUCCESS);
}
break;
}
case 'getSelectedTextReply': {
const data = event.data;
if (this.selectedTextCallback_) {
this.selectedTextCallback_(data.selectedText);
this.selectedTextCallback_ = null;
}
break;
}
case 'sendKeyEvent':
if (this.keyEventCallback_) {
this.keyEventCallback_(deserializeKeyEvent(event.data.keyEvent));
}
break;
}
}, false);
}
/**
* Send a message to the extension. If messages try to get sent before there
* is a plugin element set, then we queue them up and send them later (this
* can happen in print preview).
*/
private sendMessage_<M extends {type: string}>(message: M) {
if (this.plugin_) {
this.plugin_.postMessage(message, '*');
} else {
this.pendingScriptingMessages_.push(message);
}
}
/**
* Sets the plugin element containing the PDF viewer. The element will usually
* be passed into the PdfScriptingApi constructor but may also be set later.
* @param plugin the plugin element containing the PDF viewer.
*/
setPlugin(plugin: Window|null) {
this.plugin_ = plugin;
if (this.plugin_) {
// Send a message to ensure the postMessage channel is initialized which
// allows us to receive messages.
this.sendMessage_({type: 'initialize'});
// Flush pending messages.
while (this.pendingScriptingMessages_.length > 0) {
this.sendMessage_(this.pendingScriptingMessages_.shift()!);
}
}
}
/**
* Sets the callback which will be run when the PDF viewport changes.
*/
setViewportChangedCallback(callback: ViewportChangedCallback) {
this.viewportChangedCallback_ = callback;
}
/**
* Sets the callback which will be run when the PDF document has finished
* loading. If the document is already loaded, it will be run immediately.
*/
setLoadCompleteCallback(callback: (loaded: boolean) => void) {
this.loadCompleteCallback_ = callback;
if (this.loadState_ !== LoadState.LOADING && this.loadCompleteCallback_) {
this.loadCompleteCallback_(this.loadState_ === LoadState.SUCCESS);
}
}
/**
* Sets a callback that gets run when a key event is fired in the PDF viewer.
*/
setKeyEventCallback(callback: (e: KeyboardEvent) => void) {
this.keyEventCallback_ = callback;
}
/**
* Resets the PDF viewer into print preview mode.
* @param url the url of the PDF to load.
* @param grayscale whether or not to display the PDF in grayscale.
* @param pageNumbers an array of the page numbers.
* @param modifiable whether or not the document is modifiable.
*/
resetPrintPreviewMode(
url: string, grayscale: boolean, pageNumbers: number[],
modifiable: boolean) {
this.loadState_ = LoadState.LOADING;
this.sendMessage_({
type: 'resetPrintPreviewMode',
url: url,
grayscale: grayscale,
pageNumbers: pageNumbers,
modifiable: modifiable,
});
}
/** Hide the toolbar after a delay. */
hideToolbar() {
this.sendMessage_({type: 'hideToolbar'});
}
/**
* Load a page into the document while in print preview mode.
* @param url the url of the pdf page to load.
* @param index the index of the page to load.
*/
loadPreviewPage(url: string, index: number) {
this.sendMessage_({type: 'loadPreviewPage', url: url, index: index});
}
/** @param darkMode Whether the page is in dark mode. */
darkModeChanged(darkMode: boolean) {
this.sendMessage_({type: 'darkModeChanged', darkMode: darkMode});
}
/**
* Select all the text in the document. May only be called after document
* load.
*/
selectAll() {
this.sendMessage_({type: 'selectAll'});
}
/**
* Get the selected text in the document. The callback will be called with the
* text that is selected. May only be called after document load.
* @param callback a callback to be called with the selected text.
* @return Whether the function is successful, false if there is an
* outstanding request for selected text that has not been answered.
*/
getSelectedText(callback: (text: string) => void): boolean {
if (this.selectedTextCallback_) {
return false;
}
this.selectedTextCallback_ = callback;
this.sendMessage_({type: 'getSelectedText'});
return true;
}
/** Print the document. May only be called after document load. */
print() {
this.sendMessage_({type: 'print'});
}
/**
* Send a key event to the extension.
* @param keyEvent the key event to send to the extension.
*/
sendKeyEvent(keyEvent: KeyboardEvent) {
this.sendMessage_(
{type: 'sendKeyEvent', keyEvent: serializeKeyEvent(keyEvent)});
}
/**
* @param scrollX The amount to horizontally scroll in pixels.
* @param scrollY The amount to vertically scroll in pixels.
*/
scrollPosition(scrollX: number, scrollY: number) {
this.sendMessage_({type: 'scrollPosition', x: scrollX, y: scrollY});
}
}
/**
* Creates a PDF viewer with a scripting interface. This is basically 1) an
* iframe which is navigated to the PDF viewer extension and 2) a scripting
* interface which provides access to various features of the viewer for use
* by print preview and accessibility.
* @param src the source URL of the PDF to load initially.
* @param baseUrl the base URL of the PDF viewer
* @return The iframe element containing the PDF viewer.
*/
export function pdfCreateOutOfProcessPlugin(
src: string, baseUrl: string): PdfPlugin {
const client = new PdfScriptingApi(window, null);
const iframe = window.document.createElement('iframe') as PdfPlugin;
const url = baseUrl.endsWith('html') ? baseUrl : baseUrl + '/index.html';
iframe.setAttribute('src', `${url}?${src}`);
iframe.onload = function() {
client.setPlugin(iframe.contentWindow);
};
// Add the functions to the iframe so that they can be called directly.
iframe.darkModeChanged = client.darkModeChanged.bind(client);
iframe.hideToolbar = client.hideToolbar.bind(client);
iframe.loadPreviewPage = client.loadPreviewPage.bind(client);
iframe.resetPrintPreviewMode = client.resetPrintPreviewMode.bind(client);
iframe.scrollPosition = client.scrollPosition.bind(client);
iframe.sendKeyEvent = client.sendKeyEvent.bind(client);
iframe.setKeyEventCallback = client.setKeyEventCallback.bind(client);
iframe.setLoadCompleteCallback = client.setLoadCompleteCallback.bind(client);
iframe.setViewportChangedCallback =
client.setViewportChangedCallback.bind(client);
return iframe;
}