5b9638329c
These changes will be necessary for the next patch, since we don't want to accidentally pull in the entire default viewer in the standalone viewer components.
3370 lines
103 KiB
JavaScript
3370 lines
103 KiB
JavaScript
/* Copyright 2012 Mozilla Foundation
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
/* globals PDFBug, Stats */
|
|
|
|
import {
|
|
animationStarted,
|
|
apiPageLayoutToSpreadMode,
|
|
apiPageModeToSidebarView,
|
|
AutoPrintRegExp,
|
|
DEFAULT_SCALE_VALUE,
|
|
EventBus,
|
|
getActiveOrFocusedElement,
|
|
getPDFFileNameFromURL,
|
|
isValidRotation,
|
|
isValidScrollMode,
|
|
isValidSpreadMode,
|
|
MAX_SCALE,
|
|
MIN_SCALE,
|
|
noContextMenuHandler,
|
|
normalizeWheelEventDirection,
|
|
parseQueryString,
|
|
ProgressBar,
|
|
RendererType,
|
|
ScrollMode,
|
|
SidebarView,
|
|
SpreadMode,
|
|
TextLayerMode,
|
|
} from "./ui_utils.js";
|
|
import { AppOptions, OptionKind } from "./app_options.js";
|
|
import {
|
|
build,
|
|
createPromiseCapability,
|
|
getDocument,
|
|
getFilenameFromUrl,
|
|
GlobalWorkerOptions,
|
|
InvalidPDFException,
|
|
isPdfFile,
|
|
LinkTarget,
|
|
loadScript,
|
|
MissingPDFException,
|
|
OPS,
|
|
PDFWorker,
|
|
PermissionFlag,
|
|
shadow,
|
|
UnexpectedResponseException,
|
|
UNSUPPORTED_FEATURES,
|
|
version,
|
|
} from "pdfjs-lib";
|
|
import { CursorTool, PDFCursorTools } from "./pdf_cursor_tools.js";
|
|
import { PDFRenderingQueue, RenderingStates } from "./pdf_rendering_queue.js";
|
|
import { OverlayManager } from "./overlay_manager.js";
|
|
import { PasswordPrompt } from "./password_prompt.js";
|
|
import { PDFAttachmentViewer } from "./pdf_attachment_viewer.js";
|
|
import { PDFDocumentProperties } from "./pdf_document_properties.js";
|
|
import { PDFFindBar } from "./pdf_find_bar.js";
|
|
import { PDFFindController } from "./pdf_find_controller.js";
|
|
import { PDFHistory } from "./pdf_history.js";
|
|
import { PDFLayerViewer } from "./pdf_layer_viewer.js";
|
|
import { PDFLinkService } from "./pdf_link_service.js";
|
|
import { PDFOutlineViewer } from "./pdf_outline_viewer.js";
|
|
import { PDFPresentationMode } from "./pdf_presentation_mode.js";
|
|
import { PDFSidebar } from "./pdf_sidebar.js";
|
|
import { PDFSidebarResizer } from "./pdf_sidebar_resizer.js";
|
|
import { PDFThumbnailViewer } from "./pdf_thumbnail_viewer.js";
|
|
import { PDFViewer } from "./pdf_viewer.js";
|
|
import { SecondaryToolbar } from "./secondary_toolbar.js";
|
|
import { Toolbar } from "./toolbar.js";
|
|
import { viewerCompatibilityParams } from "./viewer_compatibility.js";
|
|
import { ViewHistory } from "./view_history.js";
|
|
|
|
const DEFAULT_SCALE_DELTA = 1.1;
|
|
const DISABLE_AUTO_FETCH_LOADING_BAR_TIMEOUT = 5000; // ms
|
|
const FORCE_PAGES_LOADED_TIMEOUT = 10000; // ms
|
|
const WHEEL_ZOOM_DISABLED_TIMEOUT = 1000; // ms
|
|
const ENABLE_PERMISSIONS_CLASS = "enablePermissions";
|
|
|
|
const ViewOnLoad = {
|
|
UNKNOWN: -1,
|
|
PREVIOUS: 0, // Default value.
|
|
INITIAL: 1,
|
|
};
|
|
|
|
const ViewerCssTheme = {
|
|
AUTOMATIC: 0, // Default value.
|
|
LIGHT: 1,
|
|
DARK: 2,
|
|
};
|
|
|
|
// Keep these in sync with mozilla-central's Histograms.json.
|
|
const KNOWN_VERSIONS = [
|
|
"1.0",
|
|
"1.1",
|
|
"1.2",
|
|
"1.3",
|
|
"1.4",
|
|
"1.5",
|
|
"1.6",
|
|
"1.7",
|
|
"1.8",
|
|
"1.9",
|
|
"2.0",
|
|
"2.1",
|
|
"2.2",
|
|
"2.3",
|
|
];
|
|
// Keep these in sync with mozilla-central's Histograms.json.
|
|
const KNOWN_GENERATORS = [
|
|
"acrobat distiller",
|
|
"acrobat pdfwriter",
|
|
"adobe livecycle",
|
|
"adobe pdf library",
|
|
"adobe photoshop",
|
|
"ghostscript",
|
|
"tcpdf",
|
|
"cairo",
|
|
"dvipdfm",
|
|
"dvips",
|
|
"pdftex",
|
|
"pdfkit",
|
|
"itext",
|
|
"prince",
|
|
"quarkxpress",
|
|
"mac os x",
|
|
"microsoft",
|
|
"openoffice",
|
|
"oracle",
|
|
"luradocument",
|
|
"pdf-xchange",
|
|
"antenna house",
|
|
"aspose.cells",
|
|
"fpdf",
|
|
];
|
|
|
|
class DefaultExternalServices {
|
|
constructor() {
|
|
throw new Error("Cannot initialize DefaultExternalServices.");
|
|
}
|
|
|
|
static updateFindControlState(data) {}
|
|
|
|
static updateFindMatchesCount(data) {}
|
|
|
|
static initPassiveLoading(callbacks) {}
|
|
|
|
static async fallback(data) {}
|
|
|
|
static reportTelemetry(data) {}
|
|
|
|
static createDownloadManager(options) {
|
|
throw new Error("Not implemented: createDownloadManager");
|
|
}
|
|
|
|
static createPreferences() {
|
|
throw new Error("Not implemented: createPreferences");
|
|
}
|
|
|
|
static createL10n(options) {
|
|
throw new Error("Not implemented: createL10n");
|
|
}
|
|
|
|
static createScripting(options) {
|
|
throw new Error("Not implemented: createScripting");
|
|
}
|
|
|
|
static get supportsIntegratedFind() {
|
|
return shadow(this, "supportsIntegratedFind", false);
|
|
}
|
|
|
|
static get supportsDocumentFonts() {
|
|
return shadow(this, "supportsDocumentFonts", true);
|
|
}
|
|
|
|
static get supportedMouseWheelZoomModifierKeys() {
|
|
return shadow(this, "supportedMouseWheelZoomModifierKeys", {
|
|
ctrlKey: true,
|
|
metaKey: true,
|
|
});
|
|
}
|
|
|
|
static get isInAutomation() {
|
|
return shadow(this, "isInAutomation", false);
|
|
}
|
|
}
|
|
|
|
const PDFViewerApplication = {
|
|
initialBookmark: document.location.hash.substring(1),
|
|
_initializedCapability: createPromiseCapability(),
|
|
fellback: false,
|
|
appConfig: null,
|
|
pdfDocument: null,
|
|
pdfLoadingTask: null,
|
|
printService: null,
|
|
/** @type {PDFViewer} */
|
|
pdfViewer: null,
|
|
/** @type {PDFThumbnailViewer} */
|
|
pdfThumbnailViewer: null,
|
|
/** @type {PDFRenderingQueue} */
|
|
pdfRenderingQueue: null,
|
|
/** @type {PDFPresentationMode} */
|
|
pdfPresentationMode: null,
|
|
/** @type {PDFDocumentProperties} */
|
|
pdfDocumentProperties: null,
|
|
/** @type {PDFLinkService} */
|
|
pdfLinkService: null,
|
|
/** @type {PDFHistory} */
|
|
pdfHistory: null,
|
|
/** @type {PDFSidebar} */
|
|
pdfSidebar: null,
|
|
/** @type {PDFSidebarResizer} */
|
|
pdfSidebarResizer: null,
|
|
/** @type {PDFOutlineViewer} */
|
|
pdfOutlineViewer: null,
|
|
/** @type {PDFAttachmentViewer} */
|
|
pdfAttachmentViewer: null,
|
|
/** @type {PDFLayerViewer} */
|
|
pdfLayerViewer: null,
|
|
/** @type {PDFCursorTools} */
|
|
pdfCursorTools: null,
|
|
/** @type {ViewHistory} */
|
|
store: null,
|
|
/** @type {DownloadManager} */
|
|
downloadManager: null,
|
|
/** @type {OverlayManager} */
|
|
overlayManager: null,
|
|
/** @type {Preferences} */
|
|
preferences: null,
|
|
/** @type {Toolbar} */
|
|
toolbar: null,
|
|
/** @type {SecondaryToolbar} */
|
|
secondaryToolbar: null,
|
|
/** @type {EventBus} */
|
|
eventBus: null,
|
|
/** @type {IL10n} */
|
|
l10n: null,
|
|
isInitialViewSet: false,
|
|
downloadComplete: false,
|
|
isViewerEmbedded: window.parent !== window,
|
|
url: "",
|
|
baseUrl: "",
|
|
externalServices: DefaultExternalServices,
|
|
_boundEvents: Object.create(null),
|
|
documentInfo: null,
|
|
metadata: null,
|
|
_contentDispositionFilename: null,
|
|
_contentLength: null,
|
|
triggerDelayedFallback: null,
|
|
_saveInProgress: false,
|
|
_wheelUnusedTicks: 0,
|
|
_idleCallbacks: new Set(),
|
|
_scriptingInstance: null,
|
|
_mouseState: Object.create(null),
|
|
|
|
// Called once when the document is loaded.
|
|
async initialize(appConfig) {
|
|
this.preferences = this.externalServices.createPreferences();
|
|
this.appConfig = appConfig;
|
|
|
|
await this._readPreferences();
|
|
await this._parseHashParameters();
|
|
this._forceCssTheme();
|
|
await this._initializeL10n();
|
|
|
|
if (
|
|
this.isViewerEmbedded &&
|
|
AppOptions.get("externalLinkTarget") === LinkTarget.NONE
|
|
) {
|
|
// Prevent external links from "replacing" the viewer,
|
|
// when it's embedded in e.g. an <iframe> or an <object>.
|
|
AppOptions.set("externalLinkTarget", LinkTarget.TOP);
|
|
}
|
|
await this._initializeViewerComponents();
|
|
|
|
// Bind the various event handlers *after* the viewer has been
|
|
// initialized, to prevent errors if an event arrives too soon.
|
|
this.bindEvents();
|
|
this.bindWindowEvents();
|
|
|
|
// We can start UI localization now.
|
|
const appContainer = appConfig.appContainer || document.documentElement;
|
|
this.l10n.translate(appContainer).then(() => {
|
|
// Dispatch the 'localized' event on the `eventBus` once the viewer
|
|
// has been fully initialized and translated.
|
|
this.eventBus.dispatch("localized", { source: this });
|
|
});
|
|
|
|
this._initializedCapability.resolve();
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
async _readPreferences() {
|
|
if (
|
|
(typeof PDFJSDev === "undefined" ||
|
|
PDFJSDev.test("!PRODUCTION || GENERIC")) &&
|
|
AppOptions.get("disablePreferences")
|
|
) {
|
|
// Give custom implementations of the default viewer a simpler way to
|
|
// opt-out of having the `Preferences` override existing `AppOptions`.
|
|
return;
|
|
}
|
|
try {
|
|
AppOptions.setAll(await this.preferences.getAll());
|
|
} catch (reason) {
|
|
console.error(`_readPreferences: "${reason?.message}".`);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Potentially parse special debugging flags in the hash section of the URL.
|
|
* @private
|
|
*/
|
|
async _parseHashParameters() {
|
|
if (!AppOptions.get("pdfBugEnabled")) {
|
|
return undefined;
|
|
}
|
|
const hash = document.location.hash.substring(1);
|
|
if (!hash) {
|
|
return undefined;
|
|
}
|
|
const hashParams = parseQueryString(hash),
|
|
waitOn = [];
|
|
|
|
if ("disableworker" in hashParams && hashParams.disableworker === "true") {
|
|
waitOn.push(loadFakeWorker());
|
|
}
|
|
if ("disablerange" in hashParams) {
|
|
AppOptions.set("disableRange", hashParams.disablerange === "true");
|
|
}
|
|
if ("disablestream" in hashParams) {
|
|
AppOptions.set("disableStream", hashParams.disablestream === "true");
|
|
}
|
|
if ("disableautofetch" in hashParams) {
|
|
AppOptions.set(
|
|
"disableAutoFetch",
|
|
hashParams.disableautofetch === "true"
|
|
);
|
|
}
|
|
if ("disablefontface" in hashParams) {
|
|
AppOptions.set("disableFontFace", hashParams.disablefontface === "true");
|
|
}
|
|
if ("disablehistory" in hashParams) {
|
|
AppOptions.set("disableHistory", hashParams.disablehistory === "true");
|
|
}
|
|
if ("webgl" in hashParams) {
|
|
AppOptions.set("enableWebGL", hashParams.webgl === "true");
|
|
}
|
|
if ("verbosity" in hashParams) {
|
|
AppOptions.set("verbosity", hashParams.verbosity | 0);
|
|
}
|
|
if ("textlayer" in hashParams) {
|
|
switch (hashParams.textlayer) {
|
|
case "off":
|
|
AppOptions.set("textLayerMode", TextLayerMode.DISABLE);
|
|
break;
|
|
case "visible":
|
|
case "shadow":
|
|
case "hover":
|
|
const viewer = this.appConfig.viewerContainer;
|
|
viewer.classList.add("textLayer-" + hashParams.textlayer);
|
|
break;
|
|
}
|
|
}
|
|
if ("pdfbug" in hashParams) {
|
|
AppOptions.set("pdfBug", true);
|
|
AppOptions.set("fontExtraProperties", true);
|
|
|
|
const enabled = hashParams.pdfbug.split(",");
|
|
waitOn.push(loadAndEnablePDFBug(enabled));
|
|
}
|
|
// It is not possible to change locale for the (various) extension builds.
|
|
if (
|
|
(typeof PDFJSDev === "undefined" ||
|
|
PDFJSDev.test("!PRODUCTION || GENERIC")) &&
|
|
"locale" in hashParams
|
|
) {
|
|
AppOptions.set("locale", hashParams.locale);
|
|
}
|
|
|
|
if (waitOn.length === 0) {
|
|
return undefined;
|
|
}
|
|
return Promise.all(waitOn).catch(reason => {
|
|
console.error(`_parseHashParameters: "${reason.message}".`);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
async _initializeL10n() {
|
|
this.l10n = this.externalServices.createL10n(
|
|
typeof PDFJSDev === "undefined" || PDFJSDev.test("!PRODUCTION || GENERIC")
|
|
? { locale: AppOptions.get("locale") }
|
|
: null
|
|
);
|
|
const dir = await this.l10n.getDirection();
|
|
document.getElementsByTagName("html")[0].dir = dir;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_forceCssTheme() {
|
|
const cssTheme = AppOptions.get("viewerCssTheme");
|
|
if (
|
|
cssTheme === ViewerCssTheme.AUTOMATIC ||
|
|
!Object.values(ViewerCssTheme).includes(cssTheme)
|
|
) {
|
|
return;
|
|
}
|
|
try {
|
|
const styleSheet = document.styleSheets[0];
|
|
const cssRules = styleSheet?.cssRules || [];
|
|
for (let i = 0, ii = cssRules.length; i < ii; i++) {
|
|
const rule = cssRules[i];
|
|
if (
|
|
rule instanceof CSSMediaRule &&
|
|
rule.media?.[0] === "(prefers-color-scheme: dark)"
|
|
) {
|
|
if (cssTheme === ViewerCssTheme.LIGHT) {
|
|
styleSheet.deleteRule(i);
|
|
return;
|
|
}
|
|
// cssTheme === ViewerCssTheme.DARK
|
|
const darkRules = /^@media \(prefers-color-scheme: dark\) {\n\s*([\w\s-.,:;/\\{}()]+)\n}$/.exec(
|
|
rule.cssText
|
|
);
|
|
if (darkRules?.[1]) {
|
|
styleSheet.deleteRule(i);
|
|
styleSheet.insertRule(darkRules[1], i);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
} catch (reason) {
|
|
console.error(`_forceCssTheme: "${reason?.message}".`);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
async _initializeViewerComponents() {
|
|
const appConfig = this.appConfig;
|
|
|
|
const eventBus =
|
|
appConfig.eventBus ||
|
|
new EventBus({ isInAutomation: this.externalServices.isInAutomation });
|
|
this.eventBus = eventBus;
|
|
|
|
this.overlayManager = new OverlayManager();
|
|
|
|
const pdfRenderingQueue = new PDFRenderingQueue();
|
|
pdfRenderingQueue.onIdle = this.cleanup.bind(this);
|
|
this.pdfRenderingQueue = pdfRenderingQueue;
|
|
|
|
const pdfLinkService = new PDFLinkService({
|
|
eventBus,
|
|
externalLinkTarget: AppOptions.get("externalLinkTarget"),
|
|
externalLinkRel: AppOptions.get("externalLinkRel"),
|
|
ignoreDestinationZoom: AppOptions.get("ignoreDestinationZoom"),
|
|
});
|
|
this.pdfLinkService = pdfLinkService;
|
|
|
|
const downloadManager = this.externalServices.createDownloadManager();
|
|
this.downloadManager = downloadManager;
|
|
|
|
const findController = new PDFFindController({
|
|
linkService: pdfLinkService,
|
|
eventBus,
|
|
});
|
|
this.findController = findController;
|
|
|
|
const container = appConfig.mainContainer;
|
|
const viewer = appConfig.viewerContainer;
|
|
this.pdfViewer = new PDFViewer({
|
|
container,
|
|
viewer,
|
|
eventBus,
|
|
renderingQueue: pdfRenderingQueue,
|
|
linkService: pdfLinkService,
|
|
downloadManager,
|
|
findController,
|
|
renderer: AppOptions.get("renderer"),
|
|
enableWebGL: AppOptions.get("enableWebGL"),
|
|
l10n: this.l10n,
|
|
textLayerMode: AppOptions.get("textLayerMode"),
|
|
imageResourcesPath: AppOptions.get("imageResourcesPath"),
|
|
renderInteractiveForms: AppOptions.get("renderInteractiveForms"),
|
|
enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
|
|
useOnlyCssZoom: AppOptions.get("useOnlyCssZoom"),
|
|
maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
|
|
enableScripting: AppOptions.get("enableScripting"),
|
|
mouseState: this._mouseState,
|
|
});
|
|
pdfRenderingQueue.setViewer(this.pdfViewer);
|
|
pdfLinkService.setViewer(this.pdfViewer);
|
|
|
|
this.pdfThumbnailViewer = new PDFThumbnailViewer({
|
|
container: appConfig.sidebar.thumbnailView,
|
|
eventBus,
|
|
renderingQueue: pdfRenderingQueue,
|
|
linkService: pdfLinkService,
|
|
l10n: this.l10n,
|
|
});
|
|
pdfRenderingQueue.setThumbnailViewer(this.pdfThumbnailViewer);
|
|
|
|
this.pdfHistory = new PDFHistory({
|
|
linkService: pdfLinkService,
|
|
eventBus,
|
|
});
|
|
pdfLinkService.setHistory(this.pdfHistory);
|
|
|
|
if (!this.supportsIntegratedFind) {
|
|
this.findBar = new PDFFindBar(appConfig.findBar, eventBus, this.l10n);
|
|
}
|
|
|
|
this.pdfDocumentProperties = new PDFDocumentProperties(
|
|
appConfig.documentProperties,
|
|
this.overlayManager,
|
|
eventBus,
|
|
this.l10n
|
|
);
|
|
|
|
this.pdfCursorTools = new PDFCursorTools({
|
|
container,
|
|
eventBus,
|
|
cursorToolOnLoad: AppOptions.get("cursorToolOnLoad"),
|
|
});
|
|
|
|
this.toolbar = new Toolbar(appConfig.toolbar, eventBus, this.l10n);
|
|
|
|
this.secondaryToolbar = new SecondaryToolbar(
|
|
appConfig.secondaryToolbar,
|
|
container,
|
|
eventBus
|
|
);
|
|
|
|
if (this.supportsFullscreen) {
|
|
this.pdfPresentationMode = new PDFPresentationMode({
|
|
container,
|
|
pdfViewer: this.pdfViewer,
|
|
eventBus,
|
|
});
|
|
}
|
|
|
|
this.passwordPrompt = new PasswordPrompt(
|
|
appConfig.passwordOverlay,
|
|
this.overlayManager,
|
|
this.l10n,
|
|
this.isViewerEmbedded
|
|
);
|
|
|
|
this.pdfOutlineViewer = new PDFOutlineViewer({
|
|
container: appConfig.sidebar.outlineView,
|
|
eventBus,
|
|
linkService: pdfLinkService,
|
|
});
|
|
|
|
this.pdfAttachmentViewer = new PDFAttachmentViewer({
|
|
container: appConfig.sidebar.attachmentsView,
|
|
eventBus,
|
|
downloadManager,
|
|
});
|
|
|
|
this.pdfLayerViewer = new PDFLayerViewer({
|
|
container: appConfig.sidebar.layersView,
|
|
eventBus,
|
|
l10n: this.l10n,
|
|
});
|
|
|
|
this.pdfSidebar = new PDFSidebar({
|
|
elements: appConfig.sidebar,
|
|
pdfViewer: this.pdfViewer,
|
|
pdfThumbnailViewer: this.pdfThumbnailViewer,
|
|
eventBus,
|
|
l10n: this.l10n,
|
|
});
|
|
this.pdfSidebar.onToggled = this.forceRendering.bind(this);
|
|
|
|
this.pdfSidebarResizer = new PDFSidebarResizer(
|
|
appConfig.sidebarResizer,
|
|
eventBus,
|
|
this.l10n
|
|
);
|
|
},
|
|
|
|
run(config) {
|
|
this.initialize(config).then(webViewerInitialized);
|
|
},
|
|
|
|
get initialized() {
|
|
return this._initializedCapability.settled;
|
|
},
|
|
|
|
get initializedPromise() {
|
|
return this._initializedCapability.promise;
|
|
},
|
|
|
|
zoomIn(ticks) {
|
|
if (this.pdfViewer.isInPresentationMode) {
|
|
return;
|
|
}
|
|
let newScale = this.pdfViewer.currentScale;
|
|
do {
|
|
newScale = (newScale * DEFAULT_SCALE_DELTA).toFixed(2);
|
|
newScale = Math.ceil(newScale * 10) / 10;
|
|
newScale = Math.min(MAX_SCALE, newScale);
|
|
} while (--ticks > 0 && newScale < MAX_SCALE);
|
|
this.pdfViewer.currentScaleValue = newScale;
|
|
},
|
|
|
|
zoomOut(ticks) {
|
|
if (this.pdfViewer.isInPresentationMode) {
|
|
return;
|
|
}
|
|
let newScale = this.pdfViewer.currentScale;
|
|
do {
|
|
newScale = (newScale / DEFAULT_SCALE_DELTA).toFixed(2);
|
|
newScale = Math.floor(newScale * 10) / 10;
|
|
newScale = Math.max(MIN_SCALE, newScale);
|
|
} while (--ticks > 0 && newScale > MIN_SCALE);
|
|
this.pdfViewer.currentScaleValue = newScale;
|
|
},
|
|
|
|
zoomReset() {
|
|
if (this.pdfViewer.isInPresentationMode) {
|
|
return;
|
|
}
|
|
this.pdfViewer.currentScaleValue = DEFAULT_SCALE_VALUE;
|
|
},
|
|
|
|
get pagesCount() {
|
|
return this.pdfDocument ? this.pdfDocument.numPages : 0;
|
|
},
|
|
|
|
get page() {
|
|
return this.pdfViewer.currentPageNumber;
|
|
},
|
|
|
|
set page(val) {
|
|
this.pdfViewer.currentPageNumber = val;
|
|
},
|
|
|
|
get supportsPrinting() {
|
|
return PDFPrintServiceFactory.instance.supportsPrinting;
|
|
},
|
|
|
|
get supportsFullscreen() {
|
|
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
|
|
return shadow(this, "supportsFullscreen", document.fullscreenEnabled);
|
|
}
|
|
const doc = document.documentElement;
|
|
let support = !!(
|
|
doc.requestFullscreen ||
|
|
doc.mozRequestFullScreen ||
|
|
doc.webkitRequestFullScreen
|
|
);
|
|
|
|
if (
|
|
document.fullscreenEnabled === false ||
|
|
document.mozFullScreenEnabled === false ||
|
|
document.webkitFullscreenEnabled === false
|
|
) {
|
|
support = false;
|
|
}
|
|
return shadow(this, "supportsFullscreen", support);
|
|
},
|
|
|
|
get supportsIntegratedFind() {
|
|
return this.externalServices.supportsIntegratedFind;
|
|
},
|
|
|
|
get supportsDocumentFonts() {
|
|
return this.externalServices.supportsDocumentFonts;
|
|
},
|
|
|
|
get loadingBar() {
|
|
const bar = new ProgressBar("#loadingBar");
|
|
return shadow(this, "loadingBar", bar);
|
|
},
|
|
|
|
get supportedMouseWheelZoomModifierKeys() {
|
|
return this.externalServices.supportedMouseWheelZoomModifierKeys;
|
|
},
|
|
|
|
initPassiveLoading() {
|
|
if (
|
|
typeof PDFJSDev === "undefined" ||
|
|
!PDFJSDev.test("MOZCENTRAL || CHROME")
|
|
) {
|
|
throw new Error("Not implemented: initPassiveLoading");
|
|
}
|
|
this.externalServices.initPassiveLoading({
|
|
onOpenWithTransport: (url, length, transport) => {
|
|
this.open(url, { length, range: transport });
|
|
},
|
|
onOpenWithData: (data, contentDispositionFilename) => {
|
|
if (isPdfFile(contentDispositionFilename)) {
|
|
this._contentDispositionFilename = contentDispositionFilename;
|
|
}
|
|
this.open(data);
|
|
},
|
|
onOpenWithURL: (url, length, originalUrl) => {
|
|
const file = originalUrl !== undefined ? { url, originalUrl } : url;
|
|
const args = length !== undefined ? { length } : null;
|
|
|
|
this.open(file, args);
|
|
},
|
|
onError: err => {
|
|
this.l10n.get("loading_error").then(msg => {
|
|
this._documentError(msg, err);
|
|
});
|
|
},
|
|
onProgress: (loaded, total) => {
|
|
this.progress(loaded / total);
|
|
},
|
|
});
|
|
},
|
|
|
|
setTitleUsingUrl(url = "") {
|
|
this.url = url;
|
|
this.baseUrl = url.split("#")[0];
|
|
let title = getPDFFileNameFromURL(url, "");
|
|
if (!title) {
|
|
try {
|
|
title = decodeURIComponent(getFilenameFromUrl(url)) || url;
|
|
} catch (ex) {
|
|
// decodeURIComponent may throw URIError,
|
|
// fall back to using the unprocessed url in that case
|
|
title = url;
|
|
}
|
|
}
|
|
this.setTitle(title);
|
|
},
|
|
|
|
setTitle(title) {
|
|
if (this.isViewerEmbedded) {
|
|
// Embedded PDF viewers should not be changing their parent page's title.
|
|
return;
|
|
}
|
|
document.title = title;
|
|
},
|
|
|
|
get _docFilename() {
|
|
// Use `this.url` instead of `this.baseUrl` to perform filename detection
|
|
// based on the reference fragment as ultimate fallback if needed.
|
|
return this._contentDispositionFilename || getPDFFileNameFromURL(this.url);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_cancelIdleCallbacks() {
|
|
if (!this._idleCallbacks.size) {
|
|
return;
|
|
}
|
|
for (const callback of this._idleCallbacks) {
|
|
window.cancelIdleCallback(callback);
|
|
}
|
|
this._idleCallbacks.clear();
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
async _destroyScriptingInstance() {
|
|
if (!this._scriptingInstance) {
|
|
return;
|
|
}
|
|
const { scripting, internalEvents, domEvents } = this._scriptingInstance;
|
|
try {
|
|
await scripting.destroySandbox();
|
|
} catch (ex) {}
|
|
|
|
for (const [name, listener] of internalEvents) {
|
|
this.eventBus._off(name, listener);
|
|
}
|
|
internalEvents.clear();
|
|
|
|
for (const [name, listener] of domEvents) {
|
|
window.removeEventListener(name, listener);
|
|
}
|
|
domEvents.clear();
|
|
|
|
delete this._mouseState.isDown;
|
|
this._scriptingInstance = null;
|
|
},
|
|
|
|
/**
|
|
* Closes opened PDF document.
|
|
* @returns {Promise} - Returns the promise, which is resolved when all
|
|
* destruction is completed.
|
|
*/
|
|
async close() {
|
|
this._unblockDocumentLoadEvent();
|
|
|
|
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
|
|
const { container } = this.appConfig.errorWrapper;
|
|
container.hidden = true;
|
|
}
|
|
|
|
if (!this.pdfLoadingTask) {
|
|
return undefined;
|
|
}
|
|
const promises = [];
|
|
|
|
promises.push(this.pdfLoadingTask.destroy());
|
|
this.pdfLoadingTask = null;
|
|
|
|
if (this.pdfDocument) {
|
|
this.pdfDocument = null;
|
|
|
|
this.pdfThumbnailViewer.setDocument(null);
|
|
this.pdfViewer.setDocument(null);
|
|
this.pdfLinkService.setDocument(null);
|
|
this.pdfDocumentProperties.setDocument(null);
|
|
}
|
|
webViewerResetPermissions();
|
|
this.store = null;
|
|
this.isInitialViewSet = false;
|
|
this.downloadComplete = false;
|
|
this.url = "";
|
|
this.baseUrl = "";
|
|
this.documentInfo = null;
|
|
this.metadata = null;
|
|
this._contentDispositionFilename = null;
|
|
this._contentLength = null;
|
|
this.triggerDelayedFallback = null;
|
|
this._saveInProgress = false;
|
|
|
|
this._cancelIdleCallbacks();
|
|
promises.push(this._destroyScriptingInstance());
|
|
|
|
this.pdfSidebar.reset();
|
|
this.pdfOutlineViewer.reset();
|
|
this.pdfAttachmentViewer.reset();
|
|
this.pdfLayerViewer.reset();
|
|
|
|
if (this.pdfHistory) {
|
|
this.pdfHistory.reset();
|
|
}
|
|
if (this.findBar) {
|
|
this.findBar.reset();
|
|
}
|
|
this.toolbar.reset();
|
|
this.secondaryToolbar.reset();
|
|
|
|
if (typeof PDFBug !== "undefined") {
|
|
PDFBug.cleanup();
|
|
}
|
|
await Promise.all(promises);
|
|
|
|
return undefined;
|
|
},
|
|
|
|
/**
|
|
* Opens PDF document specified by URL or array with additional arguments.
|
|
* @param {string|TypedArray|ArrayBuffer} file - PDF location or binary data.
|
|
* @param {Object} [args] - Additional arguments for the getDocument call,
|
|
* e.g. HTTP headers ('httpHeaders') or alternative
|
|
* data transport ('range').
|
|
* @returns {Promise} - Returns the promise, which is resolved when document
|
|
* is opened.
|
|
*/
|
|
async open(file, args) {
|
|
if (this.pdfLoadingTask) {
|
|
// We need to destroy already opened document.
|
|
await this.close();
|
|
}
|
|
// Set the necessary global worker parameters, using the available options.
|
|
const workerParameters = AppOptions.getAll(OptionKind.WORKER);
|
|
for (const key in workerParameters) {
|
|
GlobalWorkerOptions[key] = workerParameters[key];
|
|
}
|
|
|
|
const parameters = Object.create(null);
|
|
if (typeof file === "string") {
|
|
// URL
|
|
this.setTitleUsingUrl(file);
|
|
parameters.url = file;
|
|
} else if (file && "byteLength" in file) {
|
|
// ArrayBuffer
|
|
parameters.data = file;
|
|
} else if (file.url && file.originalUrl) {
|
|
this.setTitleUsingUrl(file.originalUrl);
|
|
parameters.url = file.url;
|
|
}
|
|
// Set the necessary API parameters, using the available options.
|
|
const apiParameters = AppOptions.getAll(OptionKind.API);
|
|
for (const key in apiParameters) {
|
|
let value = apiParameters[key];
|
|
|
|
if (key === "docBaseUrl" && !value) {
|
|
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("PRODUCTION")) {
|
|
value = document.URL.split("#")[0];
|
|
} else if (PDFJSDev.test("MOZCENTRAL || CHROME")) {
|
|
value = this.baseUrl;
|
|
}
|
|
}
|
|
parameters[key] = value;
|
|
}
|
|
// Finally, update the API parameters with the arguments (if they exist).
|
|
if (args) {
|
|
for (const key in args) {
|
|
parameters[key] = args[key];
|
|
}
|
|
}
|
|
|
|
const loadingTask = getDocument(parameters);
|
|
this.pdfLoadingTask = loadingTask;
|
|
|
|
loadingTask.onPassword = (updateCallback, reason) => {
|
|
this.pdfLinkService.externalLinkEnabled = false;
|
|
this.passwordPrompt.setUpdateCallback(updateCallback, reason);
|
|
this.passwordPrompt.open();
|
|
};
|
|
|
|
loadingTask.onProgress = ({ loaded, total }) => {
|
|
this.progress(loaded / total);
|
|
};
|
|
|
|
// Listen for unsupported features to trigger the fallback UI.
|
|
loadingTask.onUnsupportedFeature = this.fallback.bind(this);
|
|
|
|
return loadingTask.promise.then(
|
|
pdfDocument => {
|
|
this.load(pdfDocument);
|
|
},
|
|
exception => {
|
|
if (loadingTask !== this.pdfLoadingTask) {
|
|
return undefined; // Ignore errors for previously opened PDF files.
|
|
}
|
|
|
|
let key = "loading_error";
|
|
if (exception instanceof InvalidPDFException) {
|
|
key = "invalid_file_error";
|
|
} else if (exception instanceof MissingPDFException) {
|
|
key = "missing_file_error";
|
|
} else if (exception instanceof UnexpectedResponseException) {
|
|
key = "unexpected_response_error";
|
|
}
|
|
return this.l10n.get(key).then(msg => {
|
|
this._documentError(msg, { message: exception?.message });
|
|
throw exception;
|
|
});
|
|
}
|
|
);
|
|
},
|
|
|
|
download({ sourceEventType = "download" } = {}) {
|
|
function downloadByUrl() {
|
|
downloadManager.downloadUrl(url, filename);
|
|
}
|
|
|
|
const downloadManager = this.downloadManager,
|
|
url = this.baseUrl,
|
|
filename = this._docFilename;
|
|
|
|
// When the PDF document isn't ready, or the PDF file is still downloading,
|
|
// simply download using the URL.
|
|
if (!this.pdfDocument || !this.downloadComplete) {
|
|
downloadByUrl();
|
|
return;
|
|
}
|
|
|
|
this.pdfDocument
|
|
.getData()
|
|
.then(function (data) {
|
|
const blob = new Blob([data], { type: "application/pdf" });
|
|
downloadManager.download(blob, url, filename, sourceEventType);
|
|
})
|
|
.catch(downloadByUrl); // Error occurred, try downloading with the URL.
|
|
},
|
|
|
|
async save({ sourceEventType = "download" } = {}) {
|
|
if (this._saveInProgress) {
|
|
return;
|
|
}
|
|
|
|
const downloadManager = this.downloadManager,
|
|
url = this.baseUrl,
|
|
filename = this._docFilename;
|
|
|
|
// When the PDF document isn't ready, or the PDF file is still downloading,
|
|
// simply download using the URL.
|
|
if (!this.pdfDocument || !this.downloadComplete) {
|
|
this.download({ sourceEventType });
|
|
return;
|
|
}
|
|
this._saveInProgress = true;
|
|
|
|
await this._scriptingInstance?.scripting.dispatchEventInSandbox({
|
|
id: "doc",
|
|
name: "WillSave",
|
|
});
|
|
|
|
this.pdfDocument
|
|
.saveDocument(this.pdfDocument.annotationStorage)
|
|
.then(data => {
|
|
const blob = new Blob([data], { type: "application/pdf" });
|
|
downloadManager.download(blob, url, filename, sourceEventType);
|
|
})
|
|
.catch(() => {
|
|
this.download({ sourceEventType });
|
|
})
|
|
.finally(async () => {
|
|
await this._scriptingInstance?.scripting.dispatchEventInSandbox({
|
|
id: "doc",
|
|
name: "DidSave",
|
|
});
|
|
|
|
this._saveInProgress = false;
|
|
});
|
|
},
|
|
|
|
downloadOrSave(options) {
|
|
if (this.pdfDocument?.annotationStorage.size > 0) {
|
|
this.save(options);
|
|
} else {
|
|
this.download(options);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* For PDF documents that contain e.g. forms and javaScript, we should only
|
|
* trigger the fallback bar once the user has interacted with the page.
|
|
* @private
|
|
*/
|
|
_delayedFallback(featureId) {
|
|
// Ensure that telemetry is always reported, since it's not guaranteed
|
|
// that the fallback bar will be shown (depends on user interaction).
|
|
this.externalServices.reportTelemetry({
|
|
type: "unsupportedFeature",
|
|
featureId,
|
|
});
|
|
|
|
if (!this.triggerDelayedFallback) {
|
|
this.triggerDelayedFallback = () => {
|
|
this.fallback(featureId);
|
|
this.triggerDelayedFallback = null;
|
|
};
|
|
}
|
|
},
|
|
|
|
fallback(featureId) {
|
|
this.externalServices.reportTelemetry({
|
|
type: "unsupportedFeature",
|
|
featureId,
|
|
});
|
|
|
|
// Don't show the fallback bar for things that are *very* unlikely to cause
|
|
// user-visible errors, to avoid bothering the user unnecessarily.
|
|
switch (featureId) {
|
|
case UNSUPPORTED_FEATURES.errorFontLoadNative:
|
|
case UNSUPPORTED_FEATURES.errorFontMissing:
|
|
return;
|
|
}
|
|
// Only trigger the fallback once so we don't spam the user with messages
|
|
// for one PDF.
|
|
if (this.fellback) {
|
|
return;
|
|
}
|
|
this.fellback = true;
|
|
this.externalServices
|
|
.fallback({
|
|
featureId,
|
|
url: this.baseUrl,
|
|
})
|
|
.then(download => {
|
|
if (!download) {
|
|
return;
|
|
}
|
|
this.download({ sourceEventType: "download" });
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Show the error box; used for errors affecting loading and/or parsing of
|
|
* the entire PDF document.
|
|
*/
|
|
_documentError(message, moreInfo = null) {
|
|
this._unblockDocumentLoadEvent();
|
|
|
|
this._otherError(message, moreInfo);
|
|
},
|
|
|
|
/**
|
|
* Show the error box; used for errors affecting e.g. only a single page.
|
|
*
|
|
* @param {string} message - A message that is human readable.
|
|
* @param {Object} [moreInfo] - Further information about the error that is
|
|
* more technical. Should have a 'message' and
|
|
* optionally a 'stack' property.
|
|
*/
|
|
_otherError(message, moreInfo = null) {
|
|
const moreInfoText = [
|
|
this.l10n.get("error_version_info", {
|
|
version: version || "?",
|
|
build: build || "?",
|
|
}),
|
|
];
|
|
if (moreInfo) {
|
|
moreInfoText.push(
|
|
this.l10n.get("error_message", { message: moreInfo.message })
|
|
);
|
|
if (moreInfo.stack) {
|
|
moreInfoText.push(
|
|
this.l10n.get("error_stack", { stack: moreInfo.stack })
|
|
);
|
|
} else {
|
|
if (moreInfo.filename) {
|
|
moreInfoText.push(
|
|
this.l10n.get("error_file", { file: moreInfo.filename })
|
|
);
|
|
}
|
|
if (moreInfo.lineNumber) {
|
|
moreInfoText.push(
|
|
this.l10n.get("error_line", { line: moreInfo.lineNumber })
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
|
|
const errorWrapperConfig = this.appConfig.errorWrapper;
|
|
const errorWrapper = errorWrapperConfig.container;
|
|
errorWrapper.hidden = false;
|
|
|
|
const errorMessage = errorWrapperConfig.errorMessage;
|
|
errorMessage.textContent = message;
|
|
|
|
const closeButton = errorWrapperConfig.closeButton;
|
|
closeButton.onclick = function () {
|
|
errorWrapper.hidden = true;
|
|
};
|
|
|
|
const errorMoreInfo = errorWrapperConfig.errorMoreInfo;
|
|
const moreInfoButton = errorWrapperConfig.moreInfoButton;
|
|
const lessInfoButton = errorWrapperConfig.lessInfoButton;
|
|
moreInfoButton.onclick = function () {
|
|
errorMoreInfo.hidden = false;
|
|
moreInfoButton.hidden = true;
|
|
lessInfoButton.hidden = false;
|
|
errorMoreInfo.style.height = errorMoreInfo.scrollHeight + "px";
|
|
};
|
|
lessInfoButton.onclick = function () {
|
|
errorMoreInfo.hidden = true;
|
|
moreInfoButton.hidden = false;
|
|
lessInfoButton.hidden = true;
|
|
};
|
|
moreInfoButton.oncontextmenu = noContextMenuHandler;
|
|
lessInfoButton.oncontextmenu = noContextMenuHandler;
|
|
closeButton.oncontextmenu = noContextMenuHandler;
|
|
moreInfoButton.hidden = false;
|
|
lessInfoButton.hidden = true;
|
|
Promise.all(moreInfoText).then(parts => {
|
|
errorMoreInfo.value = parts.join("\n");
|
|
});
|
|
} else {
|
|
Promise.all(moreInfoText).then(parts => {
|
|
console.error(message + "\n" + parts.join("\n"));
|
|
});
|
|
this.fallback();
|
|
}
|
|
},
|
|
|
|
progress(level) {
|
|
if (this.downloadComplete) {
|
|
// Don't accidentally show the loading bar again when the entire file has
|
|
// already been fetched (only an issue when disableAutoFetch is enabled).
|
|
return;
|
|
}
|
|
const percent = Math.round(level * 100);
|
|
// When we transition from full request to range requests, it's possible
|
|
// that we discard some of the loaded data. This can cause the loading
|
|
// bar to move backwards. So prevent this by only updating the bar if it
|
|
// increases.
|
|
if (percent > this.loadingBar.percent || isNaN(percent)) {
|
|
this.loadingBar.percent = percent;
|
|
|
|
// When disableAutoFetch is enabled, it's not uncommon for the entire file
|
|
// to never be fetched (depends on e.g. the file structure). In this case
|
|
// the loading bar will not be completely filled, nor will it be hidden.
|
|
// To prevent displaying a partially filled loading bar permanently, we
|
|
// hide it when no data has been loaded during a certain amount of time.
|
|
const disableAutoFetch = this.pdfDocument
|
|
? this.pdfDocument.loadingParams.disableAutoFetch
|
|
: AppOptions.get("disableAutoFetch");
|
|
|
|
if (disableAutoFetch && percent) {
|
|
if (this.disableAutoFetchLoadingBarTimeout) {
|
|
clearTimeout(this.disableAutoFetchLoadingBarTimeout);
|
|
this.disableAutoFetchLoadingBarTimeout = null;
|
|
}
|
|
this.loadingBar.show();
|
|
|
|
this.disableAutoFetchLoadingBarTimeout = setTimeout(() => {
|
|
this.loadingBar.hide();
|
|
this.disableAutoFetchLoadingBarTimeout = null;
|
|
}, DISABLE_AUTO_FETCH_LOADING_BAR_TIMEOUT);
|
|
}
|
|
}
|
|
},
|
|
|
|
load(pdfDocument) {
|
|
this.pdfDocument = pdfDocument;
|
|
|
|
pdfDocument.getDownloadInfo().then(({ length }) => {
|
|
this._contentLength = length; // Ensure that the correct length is used.
|
|
this.downloadComplete = true;
|
|
this.loadingBar.hide();
|
|
|
|
firstPagePromise.then(() => {
|
|
this.eventBus.dispatch("documentloaded", { source: this });
|
|
});
|
|
});
|
|
|
|
// Since the `setInitialView` call below depends on this being resolved,
|
|
// fetch it early to avoid delaying initial rendering of the PDF document.
|
|
const pageLayoutPromise = pdfDocument.getPageLayout().catch(function () {
|
|
/* Avoid breaking initial rendering; ignoring errors. */
|
|
});
|
|
const pageModePromise = pdfDocument.getPageMode().catch(function () {
|
|
/* Avoid breaking initial rendering; ignoring errors. */
|
|
});
|
|
const openActionPromise = pdfDocument.getOpenAction().catch(function () {
|
|
/* Avoid breaking initial rendering; ignoring errors. */
|
|
});
|
|
|
|
this.toolbar.setPagesCount(pdfDocument.numPages, false);
|
|
this.secondaryToolbar.setPagesCount(pdfDocument.numPages);
|
|
|
|
let baseDocumentUrl;
|
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
|
baseDocumentUrl = null;
|
|
} else if (PDFJSDev.test("MOZCENTRAL")) {
|
|
baseDocumentUrl = this.baseUrl;
|
|
} else if (PDFJSDev.test("CHROME")) {
|
|
baseDocumentUrl = location.href.split("#")[0];
|
|
}
|
|
this.pdfLinkService.setDocument(pdfDocument, baseDocumentUrl);
|
|
this.pdfDocumentProperties.setDocument(pdfDocument, this.url);
|
|
|
|
const pdfViewer = this.pdfViewer;
|
|
pdfViewer.setDocument(pdfDocument);
|
|
const { firstPagePromise, onePageRendered, pagesPromise } = pdfViewer;
|
|
|
|
const pdfThumbnailViewer = this.pdfThumbnailViewer;
|
|
pdfThumbnailViewer.setDocument(pdfDocument);
|
|
|
|
const storedPromise = (this.store = new ViewHistory(
|
|
pdfDocument.fingerprint
|
|
))
|
|
.getMultiple({
|
|
page: null,
|
|
zoom: DEFAULT_SCALE_VALUE,
|
|
scrollLeft: "0",
|
|
scrollTop: "0",
|
|
rotation: null,
|
|
sidebarView: SidebarView.UNKNOWN,
|
|
scrollMode: ScrollMode.UNKNOWN,
|
|
spreadMode: SpreadMode.UNKNOWN,
|
|
})
|
|
.catch(() => {
|
|
/* Unable to read from storage; ignoring errors. */
|
|
return Object.create(null);
|
|
});
|
|
|
|
firstPagePromise.then(pdfPage => {
|
|
this.loadingBar.setWidth(this.appConfig.viewerContainer);
|
|
this._initializeAnnotationStorageCallbacks(pdfDocument);
|
|
|
|
Promise.all([
|
|
animationStarted,
|
|
storedPromise,
|
|
pageLayoutPromise,
|
|
pageModePromise,
|
|
openActionPromise,
|
|
])
|
|
.then(async ([timeStamp, stored, pageLayout, pageMode, openAction]) => {
|
|
const viewOnLoad = AppOptions.get("viewOnLoad");
|
|
|
|
this._initializePdfHistory({
|
|
fingerprint: pdfDocument.fingerprint,
|
|
viewOnLoad,
|
|
initialDest: openAction?.dest,
|
|
});
|
|
const initialBookmark = this.initialBookmark;
|
|
|
|
// Initialize the default values, from user preferences.
|
|
const zoom = AppOptions.get("defaultZoomValue");
|
|
let hash = zoom ? `zoom=${zoom}` : null;
|
|
|
|
let rotation = null;
|
|
let sidebarView = AppOptions.get("sidebarViewOnLoad");
|
|
let scrollMode = AppOptions.get("scrollModeOnLoad");
|
|
let spreadMode = AppOptions.get("spreadModeOnLoad");
|
|
|
|
if (stored.page && viewOnLoad !== ViewOnLoad.INITIAL) {
|
|
hash =
|
|
`page=${stored.page}&zoom=${zoom || stored.zoom},` +
|
|
`${stored.scrollLeft},${stored.scrollTop}`;
|
|
|
|
rotation = parseInt(stored.rotation, 10);
|
|
// Always let user preference take precedence over the view history.
|
|
if (sidebarView === SidebarView.UNKNOWN) {
|
|
sidebarView = stored.sidebarView | 0;
|
|
}
|
|
if (scrollMode === ScrollMode.UNKNOWN) {
|
|
scrollMode = stored.scrollMode | 0;
|
|
}
|
|
if (spreadMode === SpreadMode.UNKNOWN) {
|
|
spreadMode = stored.spreadMode | 0;
|
|
}
|
|
}
|
|
// Always let the user preference/view history take precedence.
|
|
if (pageMode && sidebarView === SidebarView.UNKNOWN) {
|
|
sidebarView = apiPageModeToSidebarView(pageMode);
|
|
}
|
|
if (pageLayout && spreadMode === SpreadMode.UNKNOWN) {
|
|
spreadMode = apiPageLayoutToSpreadMode(pageLayout);
|
|
}
|
|
|
|
this.setInitialView(hash, {
|
|
rotation,
|
|
sidebarView,
|
|
scrollMode,
|
|
spreadMode,
|
|
});
|
|
this.eventBus.dispatch("documentinit", { source: this });
|
|
// Make all navigation keys work on document load,
|
|
// unless the viewer is embedded in a web page.
|
|
if (!this.isViewerEmbedded) {
|
|
pdfViewer.focus();
|
|
}
|
|
|
|
// Currently only the "copy"-permission is supported, hence we delay
|
|
// the `getPermissions` API call until *after* rendering has started.
|
|
this._initializePermissions(pdfDocument);
|
|
|
|
// For documents with different page sizes, once all pages are
|
|
// resolved, ensure that the correct location becomes visible on load.
|
|
// (To reduce the risk, in very large and/or slow loading documents,
|
|
// that the location changes *after* the user has started interacting
|
|
// with the viewer, wait for either `pagesPromise` or a timeout.)
|
|
await Promise.race([
|
|
pagesPromise,
|
|
new Promise(resolve => {
|
|
setTimeout(resolve, FORCE_PAGES_LOADED_TIMEOUT);
|
|
}),
|
|
]);
|
|
if (!initialBookmark && !hash) {
|
|
return;
|
|
}
|
|
if (pdfViewer.hasEqualPageSizes) {
|
|
return;
|
|
}
|
|
this.initialBookmark = initialBookmark;
|
|
|
|
// eslint-disable-next-line no-self-assign
|
|
pdfViewer.currentScaleValue = pdfViewer.currentScaleValue;
|
|
// Re-apply the initial document location.
|
|
this.setInitialView(hash);
|
|
})
|
|
.catch(() => {
|
|
// Ensure that the document is always completely initialized,
|
|
// even if there are any errors thrown above.
|
|
this.setInitialView();
|
|
})
|
|
.then(function () {
|
|
// At this point, rendering of the initial page(s) should always have
|
|
// started (and may even have completed).
|
|
// To prevent any future issues, e.g. the document being completely
|
|
// blank on load, always trigger rendering here.
|
|
pdfViewer.update();
|
|
});
|
|
});
|
|
|
|
pagesPromise.then(() => {
|
|
this._unblockDocumentLoadEvent();
|
|
|
|
this._initializeAutoPrint(pdfDocument, openActionPromise);
|
|
});
|
|
|
|
onePageRendered.then(() => {
|
|
pdfDocument.getOutline().then(outline => {
|
|
this.pdfOutlineViewer.render({ outline, pdfDocument });
|
|
});
|
|
pdfDocument.getAttachments().then(attachments => {
|
|
this.pdfAttachmentViewer.render({ attachments });
|
|
});
|
|
// Ensure that the layers accurately reflects the current state in the
|
|
// viewer itself, rather than the default state provided by the API.
|
|
pdfViewer.optionalContentConfigPromise.then(optionalContentConfig => {
|
|
this.pdfLayerViewer.render({ optionalContentConfig, pdfDocument });
|
|
});
|
|
if (
|
|
(typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) ||
|
|
"requestIdleCallback" in window
|
|
) {
|
|
const callback = window.requestIdleCallback(
|
|
() => {
|
|
this._collectTelemetry(pdfDocument);
|
|
this._idleCallbacks.delete(callback);
|
|
},
|
|
{ timeout: 1000 }
|
|
);
|
|
this._idleCallbacks.add(callback);
|
|
}
|
|
this._initializeJavaScript(pdfDocument);
|
|
});
|
|
|
|
this._initializePageLabels(pdfDocument);
|
|
this._initializeMetadata(pdfDocument);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
async _initializeJavaScript(pdfDocument) {
|
|
if (!AppOptions.get("enableScripting")) {
|
|
return;
|
|
}
|
|
const [objects, calculationOrder, docActions] = await Promise.all([
|
|
pdfDocument.getFieldObjects(),
|
|
pdfDocument.getCalculationOrderIds(),
|
|
pdfDocument.getJSActions(),
|
|
]);
|
|
|
|
if (!objects && !docActions) {
|
|
// No FieldObjects or JavaScript actions were found in the document.
|
|
return;
|
|
}
|
|
if (pdfDocument !== this.pdfDocument) {
|
|
return; // The document was closed while the data resolved.
|
|
}
|
|
const scripting = this.externalServices.createScripting(
|
|
typeof PDFJSDev === "undefined" ||
|
|
PDFJSDev.test("!PRODUCTION || GENERIC || CHROME")
|
|
? { sandboxBundleSrc: AppOptions.get("sandboxBundleSrc") }
|
|
: null
|
|
);
|
|
// Store a reference to the current scripting-instance, to allow destruction
|
|
// of the sandbox and removal of the event listeners at document closing.
|
|
const internalEvents = new Map(),
|
|
domEvents = new Map();
|
|
this._scriptingInstance = {
|
|
scripting,
|
|
ready: false,
|
|
internalEvents,
|
|
domEvents,
|
|
};
|
|
|
|
if (!this.documentInfo) {
|
|
// It should be *extremely* rare for metadata to not have been resolved
|
|
// when this code runs, but ensure that we handle that case here.
|
|
await new Promise(resolve => {
|
|
this.eventBus._on("metadataloaded", resolve, { once: true });
|
|
});
|
|
if (pdfDocument !== this.pdfDocument) {
|
|
return; // The document was closed while the metadata resolved.
|
|
}
|
|
}
|
|
if (!this._contentLength) {
|
|
// Always waiting for the entire PDF document to be loaded will, most
|
|
// likely, delay sandbox-creation too much in the general case for all
|
|
// PDF documents which are not provided as binary data to the API.
|
|
// Hence we'll simply have to trust that the `contentLength` (as provided
|
|
// by the server), when it exists, is accurate enough here.
|
|
await new Promise(resolve => {
|
|
this.eventBus._on("documentloaded", resolve, { once: true });
|
|
});
|
|
if (pdfDocument !== this.pdfDocument) {
|
|
return; // The document was closed while the downloadInfo resolved.
|
|
}
|
|
}
|
|
|
|
const updateFromSandbox = ({ detail }) => {
|
|
const { id, command, value } = detail;
|
|
if (!id) {
|
|
switch (command) {
|
|
case "clear":
|
|
console.clear();
|
|
break;
|
|
case "error":
|
|
console.error(value);
|
|
break;
|
|
case "layout":
|
|
this.pdfViewer.spreadMode = apiPageLayoutToSpreadMode(value);
|
|
break;
|
|
case "page-num":
|
|
this.pdfViewer.currentPageNumber = value + 1;
|
|
break;
|
|
case "print":
|
|
this.pdfViewer.pagesPromise.then(() => {
|
|
this.triggerPrinting();
|
|
});
|
|
break;
|
|
case "println":
|
|
console.log(value);
|
|
break;
|
|
case "zoom":
|
|
this.pdfViewer.currentScaleValue = value;
|
|
break;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
element.dispatchEvent(new CustomEvent("updatefromsandbox", { detail }));
|
|
} else {
|
|
if (value !== undefined && value !== null) {
|
|
// The element hasn't been rendered yet, use the AnnotationStorage.
|
|
pdfDocument.annotationStorage.setValue(id, value);
|
|
}
|
|
}
|
|
};
|
|
internalEvents.set("updatefromsandbox", updateFromSandbox);
|
|
|
|
const visitedPages = new Map();
|
|
const pageOpen = ({ pageNumber, actionsPromise }) => {
|
|
visitedPages.set(
|
|
pageNumber,
|
|
(async () => {
|
|
// Avoid sending, and thus serializing, the `actions` data
|
|
// when the same page is opened several times.
|
|
let actions = null;
|
|
if (!visitedPages.has(pageNumber)) {
|
|
actions = await actionsPromise;
|
|
|
|
if (pdfDocument !== this.pdfDocument) {
|
|
return; // The document was closed while the actions resolved.
|
|
}
|
|
}
|
|
|
|
await this._scriptingInstance?.scripting.dispatchEventInSandbox({
|
|
id: "page",
|
|
name: "PageOpen",
|
|
pageNumber,
|
|
actions,
|
|
});
|
|
})()
|
|
);
|
|
};
|
|
|
|
const pageClose = async ({ pageNumber }) => {
|
|
const actionsPromise = visitedPages.get(pageNumber);
|
|
if (!actionsPromise) {
|
|
// Ensure that the "pageclose" event was preceded by a "pageopen" event.
|
|
return;
|
|
}
|
|
visitedPages.set(pageNumber, null);
|
|
|
|
// Ensure that the "pageopen" event is handled first.
|
|
await actionsPromise;
|
|
|
|
if (pdfDocument !== this.pdfDocument) {
|
|
return; // The document was closed while the actions resolved.
|
|
}
|
|
|
|
await this._scriptingInstance?.scripting.dispatchEventInSandbox({
|
|
id: "page",
|
|
name: "PageClose",
|
|
pageNumber,
|
|
});
|
|
};
|
|
internalEvents.set("pageopen", pageOpen);
|
|
internalEvents.set("pageclose", pageClose);
|
|
|
|
const dispatchEventInSandbox = ({ detail }) => {
|
|
scripting.dispatchEventInSandbox(detail);
|
|
};
|
|
internalEvents.set("dispatcheventinsandbox", dispatchEventInSandbox);
|
|
|
|
const mouseDown = event => {
|
|
this._mouseState.isDown = true;
|
|
};
|
|
domEvents.set("mousedown", mouseDown);
|
|
|
|
const mouseUp = event => {
|
|
this._mouseState.isDown = false;
|
|
};
|
|
domEvents.set("mouseup", mouseUp);
|
|
|
|
for (const [name, listener] of internalEvents) {
|
|
this.eventBus._on(name, listener);
|
|
}
|
|
for (const [name, listener] of domEvents) {
|
|
window.addEventListener(name, listener);
|
|
}
|
|
|
|
try {
|
|
await scripting.createSandbox({
|
|
objects,
|
|
calculationOrder,
|
|
appInfo: {
|
|
platform: navigator.platform,
|
|
language: navigator.language,
|
|
},
|
|
docInfo: {
|
|
...this.documentInfo,
|
|
baseURL: this.baseUrl,
|
|
filesize: this._contentLength,
|
|
filename: this._docFilename,
|
|
metadata: this.metadata?.getRaw(),
|
|
authors: this.metadata?.get("dc:creator"),
|
|
numPages: pdfDocument.numPages,
|
|
URL: this.url,
|
|
actions: docActions,
|
|
},
|
|
});
|
|
|
|
if (this.externalServices.isInAutomation) {
|
|
this.eventBus.dispatch("sandboxcreated", { source: this });
|
|
}
|
|
} catch (error) {
|
|
console.error(`_initializeJavaScript: "${error?.message}".`);
|
|
|
|
this._destroyScriptingInstance();
|
|
return;
|
|
}
|
|
|
|
await scripting.dispatchEventInSandbox({
|
|
id: "doc",
|
|
name: "Open",
|
|
});
|
|
await this.pdfViewer.initializeScriptingEvents();
|
|
|
|
// Used together with the integration-tests, see the `scriptingReady`
|
|
// getter, to enable awaiting full initialization of the scripting/sandbox.
|
|
// (Defer this slightly, to make absolutely sure that everything is done.)
|
|
Promise.resolve().then(() => {
|
|
if (this._scriptingInstance) {
|
|
this._scriptingInstance.ready = true;
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* A place to fetch data for telemetry after one page is rendered and the
|
|
* viewer is idle.
|
|
* @private
|
|
*/
|
|
async _collectTelemetry(pdfDocument) {
|
|
const markInfo = await this.pdfDocument.getMarkInfo();
|
|
if (pdfDocument !== this.pdfDocument) {
|
|
return; // Document was closed while waiting for mark info.
|
|
}
|
|
const tagged = markInfo?.Marked || false;
|
|
this.externalServices.reportTelemetry({
|
|
type: "tagged",
|
|
tagged,
|
|
});
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
async _initializeAutoPrint(pdfDocument, openActionPromise) {
|
|
const [openAction, javaScript] = await Promise.all([
|
|
openActionPromise,
|
|
!AppOptions.get("enableScripting") ? pdfDocument.getJavaScript() : null,
|
|
]);
|
|
|
|
if (pdfDocument !== this.pdfDocument) {
|
|
return; // The document was closed while the auto print data resolved.
|
|
}
|
|
let triggerAutoPrint = false;
|
|
|
|
if (openAction?.action === "Print") {
|
|
triggerAutoPrint = true;
|
|
}
|
|
if (javaScript) {
|
|
javaScript.some(js => {
|
|
if (!js) {
|
|
// Don't warn/fallback for empty JavaScript actions.
|
|
return false;
|
|
}
|
|
console.warn("Warning: JavaScript is not supported");
|
|
this._delayedFallback(UNSUPPORTED_FEATURES.javaScript);
|
|
return true;
|
|
});
|
|
|
|
if (!triggerAutoPrint) {
|
|
// Hack to support auto printing.
|
|
for (const js of javaScript) {
|
|
if (js && AutoPrintRegExp.test(js)) {
|
|
triggerAutoPrint = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (triggerAutoPrint) {
|
|
this.triggerPrinting();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
async _initializeMetadata(pdfDocument) {
|
|
const {
|
|
info,
|
|
metadata,
|
|
contentDispositionFilename,
|
|
contentLength,
|
|
} = await pdfDocument.getMetadata();
|
|
|
|
if (pdfDocument !== this.pdfDocument) {
|
|
return; // The document was closed while the metadata resolved.
|
|
}
|
|
this.documentInfo = info;
|
|
this.metadata = metadata;
|
|
this._contentDispositionFilename ??= contentDispositionFilename;
|
|
this._contentLength ??= contentLength; // See `getDownloadInfo`-call above.
|
|
|
|
// Provides some basic debug information
|
|
console.log(
|
|
`PDF ${pdfDocument.fingerprint} [${info.PDFFormatVersion} ` +
|
|
`${(info.Producer || "-").trim()} / ${(info.Creator || "-").trim()}] ` +
|
|
`(PDF.js: ${version || "-"}` +
|
|
`${this.pdfViewer.enableWebGL ? " [WebGL]" : ""})`
|
|
);
|
|
|
|
let pdfTitle = info?.Title;
|
|
|
|
const metadataTitle = metadata?.get("dc:title");
|
|
if (metadataTitle) {
|
|
// Ghostscript can produce invalid 'dc:title' Metadata entries:
|
|
// - The title may be "Untitled" (fixes bug 1031612).
|
|
// - The title may contain incorrectly encoded characters, which thus
|
|
// looks broken, hence we ignore the Metadata entry when it
|
|
// contains characters from the Specials Unicode block
|
|
// (fixes bug 1605526).
|
|
if (
|
|
metadataTitle !== "Untitled" &&
|
|
!/[\uFFF0-\uFFFF]/g.test(metadataTitle)
|
|
) {
|
|
pdfTitle = metadataTitle;
|
|
}
|
|
}
|
|
if (pdfTitle) {
|
|
this.setTitle(
|
|
`${pdfTitle} - ${contentDispositionFilename || document.title}`
|
|
);
|
|
} else if (contentDispositionFilename) {
|
|
this.setTitle(contentDispositionFilename);
|
|
}
|
|
|
|
if (info.IsXFAPresent && !info.IsAcroFormPresent) {
|
|
console.warn("Warning: XFA is not supported");
|
|
this._delayedFallback(UNSUPPORTED_FEATURES.forms);
|
|
} else if (
|
|
(info.IsAcroFormPresent || info.IsXFAPresent) &&
|
|
!this.pdfViewer.renderInteractiveForms
|
|
) {
|
|
console.warn("Warning: Interactive form support is not enabled");
|
|
this._delayedFallback(UNSUPPORTED_FEATURES.forms);
|
|
}
|
|
|
|
// Telemetry labels must be C++ variable friendly.
|
|
let versionId = "other";
|
|
if (KNOWN_VERSIONS.includes(info.PDFFormatVersion)) {
|
|
versionId = `v${info.PDFFormatVersion.replace(".", "_")}`;
|
|
}
|
|
let generatorId = "other";
|
|
if (info.Producer) {
|
|
const producer = info.Producer.toLowerCase();
|
|
KNOWN_GENERATORS.some(function (generator) {
|
|
if (!producer.includes(generator)) {
|
|
return false;
|
|
}
|
|
generatorId = generator.replace(/[ .-]/g, "_");
|
|
return true;
|
|
});
|
|
}
|
|
let formType = null;
|
|
if (info.IsXFAPresent) {
|
|
formType = "xfa";
|
|
} else if (info.IsAcroFormPresent) {
|
|
formType = "acroform";
|
|
}
|
|
this.externalServices.reportTelemetry({
|
|
type: "documentInfo",
|
|
version: versionId,
|
|
generator: generatorId,
|
|
formType,
|
|
});
|
|
|
|
this.eventBus.dispatch("metadataloaded", { source: this });
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
async _initializePageLabels(pdfDocument) {
|
|
const labels = await pdfDocument.getPageLabels();
|
|
|
|
if (pdfDocument !== this.pdfDocument) {
|
|
return; // The document was closed while the page labels resolved.
|
|
}
|
|
if (!labels || AppOptions.get("disablePageLabels")) {
|
|
return;
|
|
}
|
|
const numLabels = labels.length;
|
|
if (numLabels !== this.pagesCount) {
|
|
console.error(
|
|
"The number of Page Labels does not match the number of pages in the document."
|
|
);
|
|
return;
|
|
}
|
|
let i = 0;
|
|
// Ignore page labels that correspond to standard page numbering.
|
|
while (i < numLabels && labels[i] === (i + 1).toString()) {
|
|
i++;
|
|
}
|
|
if (i === numLabels) {
|
|
return;
|
|
}
|
|
const { pdfViewer, pdfThumbnailViewer, toolbar } = this;
|
|
|
|
pdfViewer.setPageLabels(labels);
|
|
pdfThumbnailViewer.setPageLabels(labels);
|
|
|
|
// Changing toolbar page display to use labels and we need to set
|
|
// the label of the current page.
|
|
toolbar.setPagesCount(numLabels, true);
|
|
toolbar.setPageNumber(
|
|
pdfViewer.currentPageNumber,
|
|
pdfViewer.currentPageLabel
|
|
);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_initializePdfHistory({ fingerprint, viewOnLoad, initialDest = null }) {
|
|
if (this.isViewerEmbedded || AppOptions.get("disableHistory")) {
|
|
// The browsing history is only enabled when the viewer is standalone,
|
|
// i.e. not when it is embedded in a web page.
|
|
return;
|
|
}
|
|
this.pdfHistory.initialize({
|
|
fingerprint,
|
|
resetHistory: viewOnLoad === ViewOnLoad.INITIAL,
|
|
updateUrl: AppOptions.get("historyUpdateUrl"),
|
|
});
|
|
|
|
if (this.pdfHistory.initialBookmark) {
|
|
this.initialBookmark = this.pdfHistory.initialBookmark;
|
|
|
|
this.initialRotation = this.pdfHistory.initialRotation;
|
|
}
|
|
|
|
// Always let the browser history/document hash take precedence.
|
|
if (
|
|
initialDest &&
|
|
!this.initialBookmark &&
|
|
viewOnLoad === ViewOnLoad.UNKNOWN
|
|
) {
|
|
this.initialBookmark = JSON.stringify(initialDest);
|
|
// TODO: Re-factor the `PDFHistory` initialization to remove this hack
|
|
// that's currently necessary to prevent weird initial history state.
|
|
this.pdfHistory.push({ explicitDest: initialDest, pageNumber: null });
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
async _initializePermissions(pdfDocument) {
|
|
const permissions = await pdfDocument.getPermissions();
|
|
|
|
if (pdfDocument !== this.pdfDocument) {
|
|
return; // The document was closed while the permissions resolved.
|
|
}
|
|
if (!permissions || !AppOptions.get("enablePermissions")) {
|
|
return;
|
|
}
|
|
// Currently only the "copy"-permission is supported.
|
|
if (!permissions.includes(PermissionFlag.COPY)) {
|
|
this.appConfig.viewerContainer.classList.add(ENABLE_PERMISSIONS_CLASS);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_initializeAnnotationStorageCallbacks(pdfDocument) {
|
|
if (pdfDocument !== this.pdfDocument) {
|
|
return;
|
|
}
|
|
const { annotationStorage } = pdfDocument;
|
|
|
|
annotationStorage.onSetModified = function () {
|
|
window.addEventListener("beforeunload", beforeUnload);
|
|
};
|
|
annotationStorage.onResetModified = function () {
|
|
window.removeEventListener("beforeunload", beforeUnload);
|
|
};
|
|
},
|
|
|
|
setInitialView(
|
|
storedHash,
|
|
{ rotation, sidebarView, scrollMode, spreadMode } = {}
|
|
) {
|
|
const setRotation = angle => {
|
|
if (isValidRotation(angle)) {
|
|
this.pdfViewer.pagesRotation = angle;
|
|
}
|
|
};
|
|
const setViewerModes = (scroll, spread) => {
|
|
if (isValidScrollMode(scroll)) {
|
|
this.pdfViewer.scrollMode = scroll;
|
|
}
|
|
if (isValidSpreadMode(spread)) {
|
|
this.pdfViewer.spreadMode = spread;
|
|
}
|
|
};
|
|
this.isInitialViewSet = true;
|
|
this.pdfSidebar.setInitialView(sidebarView);
|
|
|
|
setViewerModes(scrollMode, spreadMode);
|
|
|
|
if (this.initialBookmark) {
|
|
setRotation(this.initialRotation);
|
|
delete this.initialRotation;
|
|
|
|
this.pdfLinkService.setHash(this.initialBookmark);
|
|
this.initialBookmark = null;
|
|
} else if (storedHash) {
|
|
setRotation(rotation);
|
|
|
|
this.pdfLinkService.setHash(storedHash);
|
|
}
|
|
|
|
// Ensure that the correct page number is displayed in the UI,
|
|
// even if the active page didn't change during document load.
|
|
this.toolbar.setPageNumber(
|
|
this.pdfViewer.currentPageNumber,
|
|
this.pdfViewer.currentPageLabel
|
|
);
|
|
this.secondaryToolbar.setPageNumber(this.pdfViewer.currentPageNumber);
|
|
|
|
if (!this.pdfViewer.currentScaleValue) {
|
|
// Scale was not initialized: invalid bookmark or scale was not specified.
|
|
// Setting the default one.
|
|
this.pdfViewer.currentScaleValue = DEFAULT_SCALE_VALUE;
|
|
}
|
|
},
|
|
|
|
cleanup() {
|
|
if (!this.pdfDocument) {
|
|
return; // run cleanup when document is loaded
|
|
}
|
|
this.pdfViewer.cleanup();
|
|
this.pdfThumbnailViewer.cleanup();
|
|
|
|
// We don't want to remove fonts used by active page SVGs.
|
|
if (this.pdfViewer.renderer !== RendererType.SVG) {
|
|
this.pdfDocument.cleanup();
|
|
}
|
|
},
|
|
|
|
forceRendering() {
|
|
this.pdfRenderingQueue.printing = !!this.printService;
|
|
this.pdfRenderingQueue.isThumbnailViewEnabled = this.pdfSidebar.isThumbnailViewVisible;
|
|
this.pdfRenderingQueue.renderHighestPriority();
|
|
},
|
|
|
|
beforePrint() {
|
|
// Given that the "beforeprint" browser event is synchronous, we
|
|
// unfortunately cannot await the scripting event dispatching here.
|
|
this._scriptingInstance?.scripting.dispatchEventInSandbox({
|
|
id: "doc",
|
|
name: "WillPrint",
|
|
});
|
|
|
|
if (this.printService) {
|
|
// There is no way to suppress beforePrint/afterPrint events,
|
|
// but PDFPrintService may generate double events -- this will ignore
|
|
// the second event that will be coming from native window.print().
|
|
return;
|
|
}
|
|
|
|
if (!this.supportsPrinting) {
|
|
this.l10n.get("printing_not_supported").then(msg => {
|
|
this._otherError(msg);
|
|
});
|
|
return;
|
|
}
|
|
|
|
// The beforePrint is a sync method and we need to know layout before
|
|
// returning from this method. Ensure that we can get sizes of the pages.
|
|
if (!this.pdfViewer.pageViewsReady) {
|
|
this.l10n.get("printing_not_ready").then(msg => {
|
|
// eslint-disable-next-line no-alert
|
|
window.alert(msg);
|
|
});
|
|
return;
|
|
}
|
|
|
|
const pagesOverview = this.pdfViewer.getPagesOverview();
|
|
const printContainer = this.appConfig.printContainer;
|
|
const printResolution = AppOptions.get("printResolution");
|
|
const optionalContentConfigPromise = this.pdfViewer
|
|
.optionalContentConfigPromise;
|
|
|
|
const printService = PDFPrintServiceFactory.instance.createPrintService(
|
|
this.pdfDocument,
|
|
pagesOverview,
|
|
printContainer,
|
|
printResolution,
|
|
optionalContentConfigPromise,
|
|
this.l10n
|
|
);
|
|
this.printService = printService;
|
|
this.forceRendering();
|
|
|
|
printService.layout();
|
|
|
|
this.externalServices.reportTelemetry({
|
|
type: "print",
|
|
});
|
|
},
|
|
|
|
afterPrint() {
|
|
// Given that the "afterprint" browser event is synchronous, we
|
|
// unfortunately cannot await the scripting event dispatching here.
|
|
this._scriptingInstance?.scripting.dispatchEventInSandbox({
|
|
id: "doc",
|
|
name: "DidPrint",
|
|
});
|
|
|
|
if (this.printService) {
|
|
this.printService.destroy();
|
|
this.printService = null;
|
|
|
|
if (this.pdfDocument) {
|
|
this.pdfDocument.annotationStorage.resetModified();
|
|
}
|
|
}
|
|
this.forceRendering();
|
|
},
|
|
|
|
rotatePages(delta) {
|
|
if (!this.pdfDocument) {
|
|
return;
|
|
}
|
|
const newRotation = (this.pdfViewer.pagesRotation + 360 + delta) % 360;
|
|
this.pdfViewer.pagesRotation = newRotation;
|
|
// Note that the thumbnail viewer is updated, and rendering is triggered,
|
|
// in the 'rotationchanging' event handler.
|
|
},
|
|
|
|
requestPresentationMode() {
|
|
if (!this.pdfPresentationMode) {
|
|
return;
|
|
}
|
|
this.pdfPresentationMode.request();
|
|
},
|
|
|
|
triggerPrinting() {
|
|
if (!this.supportsPrinting) {
|
|
return;
|
|
}
|
|
window.print();
|
|
},
|
|
|
|
bindEvents() {
|
|
const { eventBus, _boundEvents } = this;
|
|
|
|
_boundEvents.beforePrint = this.beforePrint.bind(this);
|
|
_boundEvents.afterPrint = this.afterPrint.bind(this);
|
|
|
|
eventBus._on("resize", webViewerResize);
|
|
eventBus._on("hashchange", webViewerHashchange);
|
|
eventBus._on("beforeprint", _boundEvents.beforePrint);
|
|
eventBus._on("afterprint", _boundEvents.afterPrint);
|
|
eventBus._on("pagerendered", webViewerPageRendered);
|
|
eventBus._on("updateviewarea", webViewerUpdateViewarea);
|
|
eventBus._on("pagechanging", webViewerPageChanging);
|
|
eventBus._on("scalechanging", webViewerScaleChanging);
|
|
eventBus._on("rotationchanging", webViewerRotationChanging);
|
|
eventBus._on("sidebarviewchanged", webViewerSidebarViewChanged);
|
|
eventBus._on("pagemode", webViewerPageMode);
|
|
eventBus._on("namedaction", webViewerNamedAction);
|
|
eventBus._on("presentationmodechanged", webViewerPresentationModeChanged);
|
|
eventBus._on("presentationmode", webViewerPresentationMode);
|
|
eventBus._on("print", webViewerPrint);
|
|
eventBus._on("download", webViewerDownload);
|
|
eventBus._on("save", webViewerSave);
|
|
eventBus._on("firstpage", webViewerFirstPage);
|
|
eventBus._on("lastpage", webViewerLastPage);
|
|
eventBus._on("nextpage", webViewerNextPage);
|
|
eventBus._on("previouspage", webViewerPreviousPage);
|
|
eventBus._on("zoomin", webViewerZoomIn);
|
|
eventBus._on("zoomout", webViewerZoomOut);
|
|
eventBus._on("zoomreset", webViewerZoomReset);
|
|
eventBus._on("pagenumberchanged", webViewerPageNumberChanged);
|
|
eventBus._on("scalechanged", webViewerScaleChanged);
|
|
eventBus._on("rotatecw", webViewerRotateCw);
|
|
eventBus._on("rotateccw", webViewerRotateCcw);
|
|
eventBus._on("optionalcontentconfig", webViewerOptionalContentConfig);
|
|
eventBus._on("switchscrollmode", webViewerSwitchScrollMode);
|
|
eventBus._on("scrollmodechanged", webViewerScrollModeChanged);
|
|
eventBus._on("switchspreadmode", webViewerSwitchSpreadMode);
|
|
eventBus._on("spreadmodechanged", webViewerSpreadModeChanged);
|
|
eventBus._on("documentproperties", webViewerDocumentProperties);
|
|
eventBus._on("find", webViewerFind);
|
|
eventBus._on("findfromurlhash", webViewerFindFromUrlHash);
|
|
eventBus._on("updatefindmatchescount", webViewerUpdateFindMatchesCount);
|
|
eventBus._on("updatefindcontrolstate", webViewerUpdateFindControlState);
|
|
|
|
if (AppOptions.get("pdfBug")) {
|
|
_boundEvents.reportPageStatsPDFBug = reportPageStatsPDFBug;
|
|
|
|
eventBus._on("pagerendered", _boundEvents.reportPageStatsPDFBug);
|
|
eventBus._on("pagechanging", _boundEvents.reportPageStatsPDFBug);
|
|
}
|
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
|
eventBus._on("fileinputchange", webViewerFileInputChange);
|
|
eventBus._on("openfile", webViewerOpenFile);
|
|
}
|
|
},
|
|
|
|
bindWindowEvents() {
|
|
const { eventBus, _boundEvents } = this;
|
|
|
|
_boundEvents.windowResize = () => {
|
|
eventBus.dispatch("resize", { source: window });
|
|
};
|
|
_boundEvents.windowHashChange = () => {
|
|
eventBus.dispatch("hashchange", {
|
|
source: window,
|
|
hash: document.location.hash.substring(1),
|
|
});
|
|
};
|
|
_boundEvents.windowBeforePrint = () => {
|
|
eventBus.dispatch("beforeprint", { source: window });
|
|
};
|
|
_boundEvents.windowAfterPrint = () => {
|
|
eventBus.dispatch("afterprint", { source: window });
|
|
};
|
|
_boundEvents.windowUpdateFromSandbox = event => {
|
|
eventBus.dispatch("updatefromsandbox", {
|
|
source: window,
|
|
detail: event.detail,
|
|
});
|
|
};
|
|
|
|
window.addEventListener("visibilitychange", webViewerVisibilityChange);
|
|
window.addEventListener("wheel", webViewerWheel, { passive: false });
|
|
window.addEventListener("touchstart", webViewerTouchStart, {
|
|
passive: false,
|
|
});
|
|
window.addEventListener("click", webViewerClick);
|
|
window.addEventListener("keydown", webViewerKeyDown);
|
|
window.addEventListener("keyup", webViewerKeyUp);
|
|
window.addEventListener("resize", _boundEvents.windowResize);
|
|
window.addEventListener("hashchange", _boundEvents.windowHashChange);
|
|
window.addEventListener("beforeprint", _boundEvents.windowBeforePrint);
|
|
window.addEventListener("afterprint", _boundEvents.windowAfterPrint);
|
|
window.addEventListener(
|
|
"updatefromsandbox",
|
|
_boundEvents.windowUpdateFromSandbox
|
|
);
|
|
},
|
|
|
|
unbindEvents() {
|
|
const { eventBus, _boundEvents } = this;
|
|
|
|
eventBus._off("resize", webViewerResize);
|
|
eventBus._off("hashchange", webViewerHashchange);
|
|
eventBus._off("beforeprint", _boundEvents.beforePrint);
|
|
eventBus._off("afterprint", _boundEvents.afterPrint);
|
|
eventBus._off("pagerendered", webViewerPageRendered);
|
|
eventBus._off("updateviewarea", webViewerUpdateViewarea);
|
|
eventBus._off("pagechanging", webViewerPageChanging);
|
|
eventBus._off("scalechanging", webViewerScaleChanging);
|
|
eventBus._off("rotationchanging", webViewerRotationChanging);
|
|
eventBus._off("sidebarviewchanged", webViewerSidebarViewChanged);
|
|
eventBus._off("pagemode", webViewerPageMode);
|
|
eventBus._off("namedaction", webViewerNamedAction);
|
|
eventBus._off("presentationmodechanged", webViewerPresentationModeChanged);
|
|
eventBus._off("presentationmode", webViewerPresentationMode);
|
|
eventBus._off("print", webViewerPrint);
|
|
eventBus._off("download", webViewerDownload);
|
|
eventBus._off("save", webViewerSave);
|
|
eventBus._off("firstpage", webViewerFirstPage);
|
|
eventBus._off("lastpage", webViewerLastPage);
|
|
eventBus._off("nextpage", webViewerNextPage);
|
|
eventBus._off("previouspage", webViewerPreviousPage);
|
|
eventBus._off("zoomin", webViewerZoomIn);
|
|
eventBus._off("zoomout", webViewerZoomOut);
|
|
eventBus._off("zoomreset", webViewerZoomReset);
|
|
eventBus._off("pagenumberchanged", webViewerPageNumberChanged);
|
|
eventBus._off("scalechanged", webViewerScaleChanged);
|
|
eventBus._off("rotatecw", webViewerRotateCw);
|
|
eventBus._off("rotateccw", webViewerRotateCcw);
|
|
eventBus._off("optionalcontentconfig", webViewerOptionalContentConfig);
|
|
eventBus._off("switchscrollmode", webViewerSwitchScrollMode);
|
|
eventBus._off("scrollmodechanged", webViewerScrollModeChanged);
|
|
eventBus._off("switchspreadmode", webViewerSwitchSpreadMode);
|
|
eventBus._off("spreadmodechanged", webViewerSpreadModeChanged);
|
|
eventBus._off("documentproperties", webViewerDocumentProperties);
|
|
eventBus._off("find", webViewerFind);
|
|
eventBus._off("findfromurlhash", webViewerFindFromUrlHash);
|
|
eventBus._off("updatefindmatchescount", webViewerUpdateFindMatchesCount);
|
|
eventBus._off("updatefindcontrolstate", webViewerUpdateFindControlState);
|
|
|
|
if (_boundEvents.reportPageStatsPDFBug) {
|
|
eventBus._off("pagerendered", _boundEvents.reportPageStatsPDFBug);
|
|
eventBus._off("pagechanging", _boundEvents.reportPageStatsPDFBug);
|
|
|
|
_boundEvents.reportPageStatsPDFBug = null;
|
|
}
|
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
|
eventBus._off("fileinputchange", webViewerFileInputChange);
|
|
eventBus._off("openfile", webViewerOpenFile);
|
|
}
|
|
|
|
_boundEvents.beforePrint = null;
|
|
_boundEvents.afterPrint = null;
|
|
},
|
|
|
|
unbindWindowEvents() {
|
|
const { _boundEvents } = this;
|
|
|
|
window.removeEventListener("visibilitychange", webViewerVisibilityChange);
|
|
window.removeEventListener("wheel", webViewerWheel, { passive: false });
|
|
window.removeEventListener("touchstart", webViewerTouchStart, {
|
|
passive: false,
|
|
});
|
|
window.removeEventListener("click", webViewerClick);
|
|
window.removeEventListener("keydown", webViewerKeyDown);
|
|
window.removeEventListener("keyup", webViewerKeyUp);
|
|
window.removeEventListener("resize", _boundEvents.windowResize);
|
|
window.removeEventListener("hashchange", _boundEvents.windowHashChange);
|
|
window.removeEventListener("beforeprint", _boundEvents.windowBeforePrint);
|
|
window.removeEventListener("afterprint", _boundEvents.windowAfterPrint);
|
|
window.removeEventListener(
|
|
"updatefromsandbox",
|
|
_boundEvents.windowUpdateFromSandbox
|
|
);
|
|
|
|
_boundEvents.windowResize = null;
|
|
_boundEvents.windowHashChange = null;
|
|
_boundEvents.windowBeforePrint = null;
|
|
_boundEvents.windowAfterPrint = null;
|
|
_boundEvents.windowUpdateFromSandbox = null;
|
|
},
|
|
|
|
accumulateWheelTicks(ticks) {
|
|
// If the scroll direction changed, reset the accumulated wheel ticks.
|
|
if (
|
|
(this._wheelUnusedTicks > 0 && ticks < 0) ||
|
|
(this._wheelUnusedTicks < 0 && ticks > 0)
|
|
) {
|
|
this._wheelUnusedTicks = 0;
|
|
}
|
|
this._wheelUnusedTicks += ticks;
|
|
const wholeTicks =
|
|
Math.sign(this._wheelUnusedTicks) *
|
|
Math.floor(Math.abs(this._wheelUnusedTicks));
|
|
this._wheelUnusedTicks -= wholeTicks;
|
|
return wholeTicks;
|
|
},
|
|
|
|
/**
|
|
* Should be called *after* all pages have loaded, or if an error occurred,
|
|
* to unblock the "load" event; see https://bugzilla.mozilla.org/show_bug.cgi?id=1618553
|
|
* @private
|
|
*/
|
|
_unblockDocumentLoadEvent() {
|
|
if (document.blockUnblockOnload) {
|
|
document.blockUnblockOnload(false);
|
|
}
|
|
// Ensure that this method is only ever run once.
|
|
this._unblockDocumentLoadEvent = () => {};
|
|
},
|
|
|
|
/**
|
|
* Used together with the integration-tests, to enable awaiting full
|
|
* initialization of the scripting/sandbox.
|
|
*/
|
|
get scriptingReady() {
|
|
return this._scriptingInstance?.ready || false;
|
|
},
|
|
};
|
|
|
|
let validateFileURL;
|
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
|
const HOSTED_VIEWER_ORIGINS = [
|
|
"null",
|
|
"http://mozilla.github.io",
|
|
"https://mozilla.github.io",
|
|
];
|
|
validateFileURL = function (file) {
|
|
if (file === undefined) {
|
|
return;
|
|
}
|
|
try {
|
|
const viewerOrigin = new URL(window.location.href).origin || "null";
|
|
if (HOSTED_VIEWER_ORIGINS.includes(viewerOrigin)) {
|
|
// Hosted or local viewer, allow for any file locations
|
|
return;
|
|
}
|
|
const { origin, protocol } = new URL(file, window.location.href);
|
|
// Removing of the following line will not guarantee that the viewer will
|
|
// start accepting URLs from foreign origin -- CORS headers on the remote
|
|
// server must be properly configured.
|
|
// IE10 / IE11 does not include an origin in `blob:`-URLs. So don't block
|
|
// any blob:-URL. The browser's same-origin policy will block requests to
|
|
// blob:-URLs from other origins, so this is safe.
|
|
if (origin !== viewerOrigin && protocol !== "blob:") {
|
|
throw new Error("file origin does not match viewer's");
|
|
}
|
|
} catch (ex) {
|
|
PDFViewerApplication.l10n.get("loading_error").then(msg => {
|
|
PDFViewerApplication._documentError(msg, { message: ex?.message });
|
|
});
|
|
throw ex;
|
|
}
|
|
};
|
|
}
|
|
|
|
async function loadFakeWorker() {
|
|
if (!GlobalWorkerOptions.workerSrc) {
|
|
GlobalWorkerOptions.workerSrc = AppOptions.get("workerSrc");
|
|
}
|
|
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("PRODUCTION")) {
|
|
window.pdfjsWorker = await import("pdfjs/core/worker.js");
|
|
return undefined;
|
|
}
|
|
return loadScript(PDFWorker.getWorkerSrc());
|
|
}
|
|
|
|
function loadAndEnablePDFBug(enabledTabs) {
|
|
const appConfig = PDFViewerApplication.appConfig;
|
|
return loadScript(appConfig.debuggerScriptPath).then(function () {
|
|
PDFBug.enable(enabledTabs);
|
|
PDFBug.init({ OPS }, appConfig.mainContainer);
|
|
});
|
|
}
|
|
|
|
function reportPageStatsPDFBug({ pageNumber }) {
|
|
if (typeof Stats === "undefined" || !Stats.enabled) {
|
|
return;
|
|
}
|
|
const pageView = PDFViewerApplication.pdfViewer.getPageView(
|
|
/* index = */ pageNumber - 1
|
|
);
|
|
const pageStats = pageView?.pdfPage?.stats;
|
|
if (!pageStats) {
|
|
return;
|
|
}
|
|
Stats.add(pageNumber, pageStats);
|
|
}
|
|
|
|
function webViewerInitialized() {
|
|
const appConfig = PDFViewerApplication.appConfig;
|
|
let file;
|
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
|
const queryString = document.location.search.substring(1);
|
|
const params = parseQueryString(queryString);
|
|
file = "file" in params ? params.file : AppOptions.get("defaultUrl");
|
|
validateFileURL(file);
|
|
} else if (PDFJSDev.test("MOZCENTRAL")) {
|
|
file = window.location.href;
|
|
} else if (PDFJSDev.test("CHROME")) {
|
|
file = AppOptions.get("defaultUrl");
|
|
}
|
|
|
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
|
const fileInput = document.createElement("input");
|
|
fileInput.id = appConfig.openFileInputName;
|
|
fileInput.className = "fileInput";
|
|
fileInput.setAttribute("type", "file");
|
|
fileInput.oncontextmenu = noContextMenuHandler;
|
|
document.body.appendChild(fileInput);
|
|
|
|
if (
|
|
!window.File ||
|
|
!window.FileReader ||
|
|
!window.FileList ||
|
|
!window.Blob
|
|
) {
|
|
appConfig.toolbar.openFile.hidden = true;
|
|
appConfig.secondaryToolbar.openFileButton.hidden = true;
|
|
} else {
|
|
fileInput.value = null;
|
|
}
|
|
|
|
fileInput.addEventListener("change", function (evt) {
|
|
const files = evt.target.files;
|
|
if (!files || files.length === 0) {
|
|
return;
|
|
}
|
|
PDFViewerApplication.eventBus.dispatch("fileinputchange", {
|
|
source: this,
|
|
fileInput: evt.target,
|
|
});
|
|
});
|
|
|
|
// Enable dragging-and-dropping a new PDF file onto the viewerContainer.
|
|
appConfig.mainContainer.addEventListener("dragover", function (evt) {
|
|
evt.preventDefault();
|
|
|
|
evt.dataTransfer.dropEffect = "move";
|
|
});
|
|
appConfig.mainContainer.addEventListener("drop", function (evt) {
|
|
evt.preventDefault();
|
|
|
|
const files = evt.dataTransfer.files;
|
|
if (!files || files.length === 0) {
|
|
return;
|
|
}
|
|
PDFViewerApplication.eventBus.dispatch("fileinputchange", {
|
|
source: this,
|
|
fileInput: evt.dataTransfer,
|
|
});
|
|
});
|
|
} else {
|
|
appConfig.toolbar.openFile.hidden = true;
|
|
appConfig.secondaryToolbar.openFileButton.hidden = true;
|
|
}
|
|
|
|
if (!PDFViewerApplication.supportsDocumentFonts) {
|
|
AppOptions.set("disableFontFace", true);
|
|
PDFViewerApplication.l10n.get("web_fonts_disabled").then(msg => {
|
|
console.warn(msg);
|
|
});
|
|
}
|
|
|
|
if (!PDFViewerApplication.supportsPrinting) {
|
|
appConfig.toolbar.print.classList.add("hidden");
|
|
appConfig.secondaryToolbar.printButton.classList.add("hidden");
|
|
}
|
|
|
|
if (!PDFViewerApplication.supportsFullscreen) {
|
|
appConfig.toolbar.presentationModeButton.classList.add("hidden");
|
|
appConfig.secondaryToolbar.presentationModeButton.classList.add("hidden");
|
|
}
|
|
|
|
if (PDFViewerApplication.supportsIntegratedFind) {
|
|
appConfig.toolbar.viewFind.classList.add("hidden");
|
|
}
|
|
|
|
appConfig.mainContainer.addEventListener(
|
|
"transitionend",
|
|
function (evt) {
|
|
if (evt.target === /* mainContainer */ this) {
|
|
PDFViewerApplication.eventBus.dispatch("resize", { source: this });
|
|
}
|
|
},
|
|
true
|
|
);
|
|
|
|
try {
|
|
webViewerOpenFileViaURL(file);
|
|
} catch (reason) {
|
|
PDFViewerApplication.l10n.get("loading_error").then(msg => {
|
|
PDFViewerApplication._documentError(msg, reason);
|
|
});
|
|
}
|
|
}
|
|
|
|
let webViewerOpenFileViaURL;
|
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
|
webViewerOpenFileViaURL = function (file) {
|
|
if (file && file.lastIndexOf("file:", 0) === 0) {
|
|
// file:-scheme. Load the contents in the main thread because QtWebKit
|
|
// cannot load file:-URLs in a Web Worker. file:-URLs are usually loaded
|
|
// very quickly, so there is no need to set up progress event listeners.
|
|
PDFViewerApplication.setTitleUsingUrl(file);
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.onload = function () {
|
|
PDFViewerApplication.open(new Uint8Array(xhr.response));
|
|
};
|
|
xhr.open("GET", file);
|
|
xhr.responseType = "arraybuffer";
|
|
xhr.send();
|
|
return;
|
|
}
|
|
|
|
if (file) {
|
|
PDFViewerApplication.open(file);
|
|
}
|
|
};
|
|
} else if (PDFJSDev.test("MOZCENTRAL || CHROME")) {
|
|
webViewerOpenFileViaURL = function (file) {
|
|
PDFViewerApplication.setTitleUsingUrl(file);
|
|
PDFViewerApplication.initPassiveLoading();
|
|
};
|
|
} else {
|
|
webViewerOpenFileViaURL = function (file) {
|
|
if (file) {
|
|
throw new Error("Not implemented: webViewerOpenFileViaURL");
|
|
}
|
|
};
|
|
}
|
|
|
|
function webViewerResetPermissions() {
|
|
const { appConfig } = PDFViewerApplication;
|
|
if (!appConfig) {
|
|
return;
|
|
}
|
|
// Currently only the "copy"-permission is supported.
|
|
appConfig.viewerContainer.classList.remove(ENABLE_PERMISSIONS_CLASS);
|
|
}
|
|
|
|
function webViewerPageRendered({ pageNumber, timestamp, error }) {
|
|
// If the page is still visible when it has finished rendering,
|
|
// ensure that the page number input loading indicator is hidden.
|
|
if (pageNumber === PDFViewerApplication.page) {
|
|
PDFViewerApplication.toolbar.updateLoadingIndicatorState(false);
|
|
}
|
|
|
|
// Use the rendered page to set the corresponding thumbnail image.
|
|
if (PDFViewerApplication.pdfSidebar.isThumbnailViewVisible) {
|
|
const pageView = PDFViewerApplication.pdfViewer.getPageView(
|
|
/* index = */ pageNumber - 1
|
|
);
|
|
const thumbnailView = PDFViewerApplication.pdfThumbnailViewer.getThumbnail(
|
|
/* index = */ pageNumber - 1
|
|
);
|
|
if (pageView && thumbnailView) {
|
|
thumbnailView.setImage(pageView);
|
|
}
|
|
}
|
|
|
|
if (error) {
|
|
PDFViewerApplication.l10n.get("rendering_error").then(msg => {
|
|
PDFViewerApplication._otherError(msg, error);
|
|
});
|
|
}
|
|
|
|
PDFViewerApplication.externalServices.reportTelemetry({
|
|
type: "pageInfo",
|
|
timestamp,
|
|
});
|
|
// It is a good time to report stream and font types.
|
|
PDFViewerApplication.pdfDocument.getStats().then(function (stats) {
|
|
PDFViewerApplication.externalServices.reportTelemetry({
|
|
type: "documentStats",
|
|
stats,
|
|
});
|
|
});
|
|
}
|
|
|
|
function webViewerPageMode({ mode }) {
|
|
// Handle the 'pagemode' hash parameter, see also `PDFLinkService_setHash`.
|
|
let view;
|
|
switch (mode) {
|
|
case "thumbs":
|
|
view = SidebarView.THUMBS;
|
|
break;
|
|
case "bookmarks":
|
|
case "outline": // non-standard
|
|
view = SidebarView.OUTLINE;
|
|
break;
|
|
case "attachments": // non-standard
|
|
view = SidebarView.ATTACHMENTS;
|
|
break;
|
|
case "layers": // non-standard
|
|
view = SidebarView.LAYERS;
|
|
break;
|
|
case "none":
|
|
view = SidebarView.NONE;
|
|
break;
|
|
default:
|
|
console.error('Invalid "pagemode" hash parameter: ' + mode);
|
|
return;
|
|
}
|
|
PDFViewerApplication.pdfSidebar.switchView(view, /* forceOpen = */ true);
|
|
}
|
|
|
|
function webViewerNamedAction(evt) {
|
|
// Processing a couple of named actions that might be useful, see also
|
|
// `PDFLinkService.executeNamedAction`.
|
|
switch (evt.action) {
|
|
case "GoToPage":
|
|
PDFViewerApplication.appConfig.toolbar.pageNumber.select();
|
|
break;
|
|
|
|
case "Find":
|
|
if (!PDFViewerApplication.supportsIntegratedFind) {
|
|
PDFViewerApplication.findBar.toggle();
|
|
}
|
|
break;
|
|
|
|
case "Print":
|
|
PDFViewerApplication.triggerPrinting();
|
|
break;
|
|
|
|
case "SaveAs":
|
|
webViewerSave();
|
|
break;
|
|
}
|
|
}
|
|
|
|
function webViewerPresentationModeChanged(evt) {
|
|
PDFViewerApplication.pdfViewer.presentationModeState = evt.state;
|
|
}
|
|
|
|
function webViewerSidebarViewChanged(evt) {
|
|
PDFViewerApplication.pdfRenderingQueue.isThumbnailViewEnabled =
|
|
PDFViewerApplication.pdfSidebar.isThumbnailViewVisible;
|
|
|
|
const store = PDFViewerApplication.store;
|
|
if (store && PDFViewerApplication.isInitialViewSet) {
|
|
// Only update the storage when the document has been loaded *and* rendered.
|
|
store.set("sidebarView", evt.view).catch(function () {});
|
|
}
|
|
}
|
|
|
|
function webViewerUpdateViewarea(evt) {
|
|
const location = evt.location,
|
|
store = PDFViewerApplication.store;
|
|
|
|
if (store && PDFViewerApplication.isInitialViewSet) {
|
|
store
|
|
.setMultiple({
|
|
page: location.pageNumber,
|
|
zoom: location.scale,
|
|
scrollLeft: location.left,
|
|
scrollTop: location.top,
|
|
rotation: location.rotation,
|
|
})
|
|
.catch(function () {
|
|
/* unable to write to storage */
|
|
});
|
|
}
|
|
const href = PDFViewerApplication.pdfLinkService.getAnchorUrl(
|
|
location.pdfOpenParams
|
|
);
|
|
PDFViewerApplication.appConfig.toolbar.viewBookmark.href = href;
|
|
PDFViewerApplication.appConfig.secondaryToolbar.viewBookmarkButton.href = href;
|
|
|
|
// Show/hide the loading indicator in the page number input element.
|
|
const currentPage = PDFViewerApplication.pdfViewer.getPageView(
|
|
/* index = */ PDFViewerApplication.page - 1
|
|
);
|
|
const loading = currentPage?.renderingState !== RenderingStates.FINISHED;
|
|
PDFViewerApplication.toolbar.updateLoadingIndicatorState(loading);
|
|
}
|
|
|
|
function webViewerScrollModeChanged(evt) {
|
|
const store = PDFViewerApplication.store;
|
|
if (store && PDFViewerApplication.isInitialViewSet) {
|
|
// Only update the storage when the document has been loaded *and* rendered.
|
|
store.set("scrollMode", evt.mode).catch(function () {});
|
|
}
|
|
}
|
|
|
|
function webViewerSpreadModeChanged(evt) {
|
|
const store = PDFViewerApplication.store;
|
|
if (store && PDFViewerApplication.isInitialViewSet) {
|
|
// Only update the storage when the document has been loaded *and* rendered.
|
|
store.set("spreadMode", evt.mode).catch(function () {});
|
|
}
|
|
}
|
|
|
|
function webViewerResize() {
|
|
const { pdfDocument, pdfViewer } = PDFViewerApplication;
|
|
if (!pdfDocument) {
|
|
return;
|
|
}
|
|
const currentScaleValue = pdfViewer.currentScaleValue;
|
|
if (
|
|
currentScaleValue === "auto" ||
|
|
currentScaleValue === "page-fit" ||
|
|
currentScaleValue === "page-width"
|
|
) {
|
|
// Note: the scale is constant for 'page-actual'.
|
|
pdfViewer.currentScaleValue = currentScaleValue;
|
|
}
|
|
pdfViewer.update();
|
|
}
|
|
|
|
function webViewerHashchange(evt) {
|
|
const hash = evt.hash;
|
|
if (!hash) {
|
|
return;
|
|
}
|
|
if (!PDFViewerApplication.isInitialViewSet) {
|
|
PDFViewerApplication.initialBookmark = hash;
|
|
} else if (!PDFViewerApplication.pdfHistory.popStateInProgress) {
|
|
PDFViewerApplication.pdfLinkService.setHash(hash);
|
|
}
|
|
}
|
|
|
|
let webViewerFileInputChange, webViewerOpenFile;
|
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
|
webViewerFileInputChange = function (evt) {
|
|
if (PDFViewerApplication.pdfViewer?.isInPresentationMode) {
|
|
return; // Opening a new PDF file isn't supported in Presentation Mode.
|
|
}
|
|
const file = evt.fileInput.files[0];
|
|
|
|
if (!viewerCompatibilityParams.disableCreateObjectURL) {
|
|
let url = URL.createObjectURL(file);
|
|
if (file.name) {
|
|
url = { url, originalUrl: file.name };
|
|
}
|
|
PDFViewerApplication.open(url);
|
|
} else {
|
|
PDFViewerApplication.setTitleUsingUrl(file.name);
|
|
// Read the local file into a Uint8Array.
|
|
const fileReader = new FileReader();
|
|
fileReader.onload = function webViewerChangeFileReaderOnload(event) {
|
|
const buffer = event.target.result;
|
|
PDFViewerApplication.open(new Uint8Array(buffer));
|
|
};
|
|
fileReader.readAsArrayBuffer(file);
|
|
}
|
|
|
|
// URL does not reflect proper document location - hiding some icons.
|
|
const appConfig = PDFViewerApplication.appConfig;
|
|
appConfig.toolbar.viewBookmark.hidden = true;
|
|
appConfig.secondaryToolbar.viewBookmarkButton.hidden = true;
|
|
appConfig.toolbar.download.hidden = true;
|
|
appConfig.secondaryToolbar.downloadButton.hidden = true;
|
|
};
|
|
|
|
webViewerOpenFile = function (evt) {
|
|
const openFileInputName = PDFViewerApplication.appConfig.openFileInputName;
|
|
document.getElementById(openFileInputName).click();
|
|
};
|
|
}
|
|
|
|
function webViewerPresentationMode() {
|
|
PDFViewerApplication.requestPresentationMode();
|
|
}
|
|
function webViewerPrint() {
|
|
PDFViewerApplication.triggerPrinting();
|
|
}
|
|
function webViewerDownload() {
|
|
PDFViewerApplication.downloadOrSave({ sourceEventType: "download" });
|
|
}
|
|
function webViewerSave() {
|
|
PDFViewerApplication.downloadOrSave({ sourceEventType: "save" });
|
|
}
|
|
function webViewerFirstPage() {
|
|
if (PDFViewerApplication.pdfDocument) {
|
|
PDFViewerApplication.page = 1;
|
|
}
|
|
}
|
|
function webViewerLastPage() {
|
|
if (PDFViewerApplication.pdfDocument) {
|
|
PDFViewerApplication.page = PDFViewerApplication.pagesCount;
|
|
}
|
|
}
|
|
function webViewerNextPage() {
|
|
PDFViewerApplication.pdfViewer.nextPage();
|
|
}
|
|
function webViewerPreviousPage() {
|
|
PDFViewerApplication.pdfViewer.previousPage();
|
|
}
|
|
function webViewerZoomIn() {
|
|
PDFViewerApplication.zoomIn();
|
|
}
|
|
function webViewerZoomOut() {
|
|
PDFViewerApplication.zoomOut();
|
|
}
|
|
function webViewerZoomReset() {
|
|
PDFViewerApplication.zoomReset();
|
|
}
|
|
function webViewerPageNumberChanged(evt) {
|
|
const pdfViewer = PDFViewerApplication.pdfViewer;
|
|
// Note that for `<input type="number">` HTML elements, an empty string will
|
|
// be returned for non-number inputs; hence we simply do nothing in that case.
|
|
if (evt.value !== "") {
|
|
PDFViewerApplication.pdfLinkService.goToPage(evt.value);
|
|
}
|
|
|
|
// Ensure that the page number input displays the correct value, even if the
|
|
// value entered by the user was invalid (e.g. a floating point number).
|
|
if (
|
|
evt.value !== pdfViewer.currentPageNumber.toString() &&
|
|
evt.value !== pdfViewer.currentPageLabel
|
|
) {
|
|
PDFViewerApplication.toolbar.setPageNumber(
|
|
pdfViewer.currentPageNumber,
|
|
pdfViewer.currentPageLabel
|
|
);
|
|
}
|
|
}
|
|
function webViewerScaleChanged(evt) {
|
|
PDFViewerApplication.pdfViewer.currentScaleValue = evt.value;
|
|
}
|
|
function webViewerRotateCw() {
|
|
PDFViewerApplication.rotatePages(90);
|
|
}
|
|
function webViewerRotateCcw() {
|
|
PDFViewerApplication.rotatePages(-90);
|
|
}
|
|
function webViewerOptionalContentConfig(evt) {
|
|
PDFViewerApplication.pdfViewer.optionalContentConfigPromise = evt.promise;
|
|
}
|
|
function webViewerSwitchScrollMode(evt) {
|
|
PDFViewerApplication.pdfViewer.scrollMode = evt.mode;
|
|
}
|
|
function webViewerSwitchSpreadMode(evt) {
|
|
PDFViewerApplication.pdfViewer.spreadMode = evt.mode;
|
|
}
|
|
function webViewerDocumentProperties() {
|
|
PDFViewerApplication.pdfDocumentProperties.open();
|
|
}
|
|
|
|
function webViewerFind(evt) {
|
|
PDFViewerApplication.findController.executeCommand("find" + evt.type, {
|
|
query: evt.query,
|
|
phraseSearch: evt.phraseSearch,
|
|
caseSensitive: evt.caseSensitive,
|
|
entireWord: evt.entireWord,
|
|
highlightAll: evt.highlightAll,
|
|
findPrevious: evt.findPrevious,
|
|
});
|
|
}
|
|
|
|
function webViewerFindFromUrlHash(evt) {
|
|
PDFViewerApplication.findController.executeCommand("find", {
|
|
query: evt.query,
|
|
phraseSearch: evt.phraseSearch,
|
|
caseSensitive: false,
|
|
entireWord: false,
|
|
highlightAll: true,
|
|
findPrevious: false,
|
|
});
|
|
}
|
|
|
|
function webViewerUpdateFindMatchesCount({ matchesCount }) {
|
|
if (PDFViewerApplication.supportsIntegratedFind) {
|
|
PDFViewerApplication.externalServices.updateFindMatchesCount(matchesCount);
|
|
} else {
|
|
PDFViewerApplication.findBar.updateResultsCount(matchesCount);
|
|
}
|
|
}
|
|
|
|
function webViewerUpdateFindControlState({
|
|
state,
|
|
previous,
|
|
matchesCount,
|
|
rawQuery,
|
|
}) {
|
|
if (PDFViewerApplication.supportsIntegratedFind) {
|
|
PDFViewerApplication.externalServices.updateFindControlState({
|
|
result: state,
|
|
findPrevious: previous,
|
|
matchesCount,
|
|
rawQuery,
|
|
});
|
|
} else {
|
|
PDFViewerApplication.findBar.updateUIState(state, previous, matchesCount);
|
|
}
|
|
}
|
|
|
|
function webViewerScaleChanging(evt) {
|
|
PDFViewerApplication.toolbar.setPageScale(evt.presetValue, evt.scale);
|
|
|
|
PDFViewerApplication.pdfViewer.update();
|
|
}
|
|
|
|
function webViewerRotationChanging(evt) {
|
|
PDFViewerApplication.pdfThumbnailViewer.pagesRotation = evt.pagesRotation;
|
|
|
|
PDFViewerApplication.forceRendering();
|
|
// Ensure that the active page doesn't change during rotation.
|
|
PDFViewerApplication.pdfViewer.currentPageNumber = evt.pageNumber;
|
|
}
|
|
|
|
function webViewerPageChanging({ pageNumber, pageLabel }) {
|
|
PDFViewerApplication.toolbar.setPageNumber(pageNumber, pageLabel);
|
|
PDFViewerApplication.secondaryToolbar.setPageNumber(pageNumber);
|
|
|
|
if (PDFViewerApplication.pdfSidebar.isThumbnailViewVisible) {
|
|
PDFViewerApplication.pdfThumbnailViewer.scrollThumbnailIntoView(pageNumber);
|
|
}
|
|
}
|
|
|
|
function webViewerVisibilityChange(evt) {
|
|
if (document.visibilityState === "visible") {
|
|
// Ignore mouse wheel zooming during tab switches (bug 1503412).
|
|
setZoomDisabledTimeout();
|
|
}
|
|
}
|
|
|
|
let zoomDisabledTimeout = null;
|
|
function setZoomDisabledTimeout() {
|
|
if (zoomDisabledTimeout) {
|
|
clearTimeout(zoomDisabledTimeout);
|
|
}
|
|
zoomDisabledTimeout = setTimeout(function () {
|
|
zoomDisabledTimeout = null;
|
|
}, WHEEL_ZOOM_DISABLED_TIMEOUT);
|
|
}
|
|
|
|
function webViewerWheel(evt) {
|
|
const {
|
|
pdfViewer,
|
|
supportedMouseWheelZoomModifierKeys,
|
|
} = PDFViewerApplication;
|
|
|
|
if (pdfViewer.isInPresentationMode) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
(evt.ctrlKey && supportedMouseWheelZoomModifierKeys.ctrlKey) ||
|
|
(evt.metaKey && supportedMouseWheelZoomModifierKeys.metaKey)
|
|
) {
|
|
// Only zoom the pages, not the entire viewer.
|
|
evt.preventDefault();
|
|
// NOTE: this check must be placed *after* preventDefault.
|
|
if (zoomDisabledTimeout || document.visibilityState === "hidden") {
|
|
return;
|
|
}
|
|
|
|
const previousScale = pdfViewer.currentScale;
|
|
|
|
const delta = normalizeWheelEventDirection(evt);
|
|
let ticks = 0;
|
|
if (
|
|
evt.deltaMode === WheelEvent.DOM_DELTA_LINE ||
|
|
evt.deltaMode === WheelEvent.DOM_DELTA_PAGE
|
|
) {
|
|
// For line-based devices, use one tick per event, because different
|
|
// OSs have different defaults for the number lines. But we generally
|
|
// want one "clicky" roll of the wheel (which produces one event) to
|
|
// adjust the zoom by one step.
|
|
if (Math.abs(delta) >= 1) {
|
|
ticks = Math.sign(delta);
|
|
} else {
|
|
// If we're getting fractional lines (I can't think of a scenario
|
|
// this might actually happen), be safe and use the accumulator.
|
|
ticks = PDFViewerApplication.accumulateWheelTicks(delta);
|
|
}
|
|
} else {
|
|
// pixel-based devices
|
|
const PIXELS_PER_LINE_SCALE = 30;
|
|
ticks = PDFViewerApplication.accumulateWheelTicks(
|
|
delta / PIXELS_PER_LINE_SCALE
|
|
);
|
|
}
|
|
|
|
if (ticks < 0) {
|
|
PDFViewerApplication.zoomOut(-ticks);
|
|
} else if (ticks > 0) {
|
|
PDFViewerApplication.zoomIn(ticks);
|
|
}
|
|
|
|
const currentScale = pdfViewer.currentScale;
|
|
if (previousScale !== currentScale) {
|
|
// After scaling the page via zoomIn/zoomOut, the position of the upper-
|
|
// left corner is restored. When the mouse wheel is used, the position
|
|
// under the cursor should be restored instead.
|
|
const scaleCorrectionFactor = currentScale / previousScale - 1;
|
|
const rect = pdfViewer.container.getBoundingClientRect();
|
|
const dx = evt.clientX - rect.left;
|
|
const dy = evt.clientY - rect.top;
|
|
pdfViewer.container.scrollLeft += dx * scaleCorrectionFactor;
|
|
pdfViewer.container.scrollTop += dy * scaleCorrectionFactor;
|
|
}
|
|
} else {
|
|
setZoomDisabledTimeout();
|
|
}
|
|
}
|
|
|
|
function webViewerTouchStart(evt) {
|
|
if (evt.touches.length > 1) {
|
|
// Disable touch-based zooming, because the entire UI bits gets zoomed and
|
|
// that doesn't look great. If we do want to have a good touch-based
|
|
// zooming experience, we need to implement smooth zoom capability (probably
|
|
// using a CSS transform for faster visual response, followed by async
|
|
// re-rendering at the final zoom level) and do gesture detection on the
|
|
// touchmove events to drive it. Or if we want to settle for a less good
|
|
// experience we can make the touchmove events drive the existing step-zoom
|
|
// behaviour that the ctrl+mousewheel path takes.
|
|
evt.preventDefault();
|
|
}
|
|
}
|
|
|
|
function webViewerClick(evt) {
|
|
// Avoid triggering the fallback bar when the user clicks on the
|
|
// toolbar or sidebar.
|
|
if (
|
|
PDFViewerApplication.triggerDelayedFallback &&
|
|
PDFViewerApplication.pdfViewer.containsElement(evt.target)
|
|
) {
|
|
PDFViewerApplication.triggerDelayedFallback();
|
|
}
|
|
|
|
if (!PDFViewerApplication.secondaryToolbar.isOpen) {
|
|
return;
|
|
}
|
|
const appConfig = PDFViewerApplication.appConfig;
|
|
if (
|
|
PDFViewerApplication.pdfViewer.containsElement(evt.target) ||
|
|
(appConfig.toolbar.container.contains(evt.target) &&
|
|
evt.target !== appConfig.secondaryToolbar.toggleButton)
|
|
) {
|
|
PDFViewerApplication.secondaryToolbar.close();
|
|
}
|
|
}
|
|
|
|
function webViewerKeyUp(evt) {
|
|
if (evt.keyCode === 9) {
|
|
// The user is tabbing into the viewer. Trigger the fallback bar if it has
|
|
// not already been displayed.
|
|
if (PDFViewerApplication.triggerDelayedFallback) {
|
|
PDFViewerApplication.triggerDelayedFallback();
|
|
}
|
|
}
|
|
}
|
|
|
|
function webViewerKeyDown(evt) {
|
|
if (PDFViewerApplication.overlayManager.active) {
|
|
return;
|
|
}
|
|
|
|
let handled = false,
|
|
ensureViewerFocused = false;
|
|
const cmd =
|
|
(evt.ctrlKey ? 1 : 0) |
|
|
(evt.altKey ? 2 : 0) |
|
|
(evt.shiftKey ? 4 : 0) |
|
|
(evt.metaKey ? 8 : 0);
|
|
|
|
const pdfViewer = PDFViewerApplication.pdfViewer;
|
|
const isViewerInPresentationMode = pdfViewer?.isInPresentationMode;
|
|
|
|
// First, handle the key bindings that are independent whether an input
|
|
// control is selected or not.
|
|
if (cmd === 1 || cmd === 8 || cmd === 5 || cmd === 12) {
|
|
// either CTRL or META key with optional SHIFT.
|
|
switch (evt.keyCode) {
|
|
case 70: // f
|
|
if (!PDFViewerApplication.supportsIntegratedFind && !evt.shiftKey) {
|
|
PDFViewerApplication.findBar.open();
|
|
handled = true;
|
|
}
|
|
break;
|
|
case 71: // g
|
|
if (!PDFViewerApplication.supportsIntegratedFind) {
|
|
const findState = PDFViewerApplication.findController.state;
|
|
if (findState) {
|
|
PDFViewerApplication.findController.executeCommand("findagain", {
|
|
query: findState.query,
|
|
phraseSearch: findState.phraseSearch,
|
|
caseSensitive: findState.caseSensitive,
|
|
entireWord: findState.entireWord,
|
|
highlightAll: findState.highlightAll,
|
|
findPrevious: cmd === 5 || cmd === 12,
|
|
});
|
|
}
|
|
handled = true;
|
|
}
|
|
break;
|
|
case 61: // FF/Mac '='
|
|
case 107: // FF '+' and '='
|
|
case 187: // Chrome '+'
|
|
case 171: // FF with German keyboard
|
|
if (!isViewerInPresentationMode) {
|
|
PDFViewerApplication.zoomIn();
|
|
}
|
|
handled = true;
|
|
break;
|
|
case 173: // FF/Mac '-'
|
|
case 109: // FF '-'
|
|
case 189: // Chrome '-'
|
|
if (!isViewerInPresentationMode) {
|
|
PDFViewerApplication.zoomOut();
|
|
}
|
|
handled = true;
|
|
break;
|
|
case 48: // '0'
|
|
case 96: // '0' on Numpad of Swedish keyboard
|
|
if (!isViewerInPresentationMode) {
|
|
// keeping it unhandled (to restore page zoom to 100%)
|
|
setTimeout(function () {
|
|
// ... and resetting the scale after browser adjusts its scale
|
|
PDFViewerApplication.zoomReset();
|
|
});
|
|
handled = false;
|
|
}
|
|
break;
|
|
|
|
case 38: // up arrow
|
|
if (isViewerInPresentationMode || PDFViewerApplication.page > 1) {
|
|
PDFViewerApplication.page = 1;
|
|
handled = true;
|
|
ensureViewerFocused = true;
|
|
}
|
|
break;
|
|
case 40: // down arrow
|
|
if (
|
|
isViewerInPresentationMode ||
|
|
PDFViewerApplication.page < PDFViewerApplication.pagesCount
|
|
) {
|
|
PDFViewerApplication.page = PDFViewerApplication.pagesCount;
|
|
handled = true;
|
|
ensureViewerFocused = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC || CHROME")) {
|
|
const { eventBus } = PDFViewerApplication;
|
|
|
|
// CTRL or META without shift
|
|
if (cmd === 1 || cmd === 8) {
|
|
switch (evt.keyCode) {
|
|
case 83: // s
|
|
eventBus.dispatch("download", { source: window });
|
|
handled = true;
|
|
break;
|
|
|
|
case 79: // o
|
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
|
eventBus.dispatch("openfile", { source: window });
|
|
handled = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// CTRL+ALT or Option+Command
|
|
if (cmd === 3 || cmd === 10) {
|
|
switch (evt.keyCode) {
|
|
case 80: // p
|
|
PDFViewerApplication.requestPresentationMode();
|
|
handled = true;
|
|
break;
|
|
case 71: // g
|
|
// focuses input#pageNumber field
|
|
PDFViewerApplication.appConfig.toolbar.pageNumber.select();
|
|
handled = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (handled) {
|
|
if (ensureViewerFocused && !isViewerInPresentationMode) {
|
|
pdfViewer.focus();
|
|
}
|
|
evt.preventDefault();
|
|
return;
|
|
}
|
|
|
|
// Some shortcuts should not get handled if a control/input element
|
|
// is selected.
|
|
const curElement = getActiveOrFocusedElement();
|
|
const curElementTagName = curElement?.tagName.toUpperCase();
|
|
if (
|
|
curElementTagName === "INPUT" ||
|
|
curElementTagName === "TEXTAREA" ||
|
|
curElementTagName === "SELECT" ||
|
|
curElement?.isContentEditable
|
|
) {
|
|
// Make sure that the secondary toolbar is closed when Escape is pressed.
|
|
if (evt.keyCode !== /* Esc = */ 27) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// No control key pressed at all.
|
|
if (cmd === 0) {
|
|
let turnPage = 0,
|
|
turnOnlyIfPageFit = false;
|
|
switch (evt.keyCode) {
|
|
case 38: // up arrow
|
|
case 33: // pg up
|
|
// vertical scrolling using arrow/pg keys
|
|
if (pdfViewer.isVerticalScrollbarEnabled) {
|
|
turnOnlyIfPageFit = true;
|
|
}
|
|
turnPage = -1;
|
|
break;
|
|
case 8: // backspace
|
|
if (!isViewerInPresentationMode) {
|
|
turnOnlyIfPageFit = true;
|
|
}
|
|
turnPage = -1;
|
|
break;
|
|
case 37: // left arrow
|
|
// horizontal scrolling using arrow keys
|
|
if (pdfViewer.isHorizontalScrollbarEnabled) {
|
|
turnOnlyIfPageFit = true;
|
|
}
|
|
/* falls through */
|
|
case 75: // 'k'
|
|
case 80: // 'p'
|
|
turnPage = -1;
|
|
break;
|
|
case 27: // esc key
|
|
if (PDFViewerApplication.secondaryToolbar.isOpen) {
|
|
PDFViewerApplication.secondaryToolbar.close();
|
|
handled = true;
|
|
}
|
|
if (
|
|
!PDFViewerApplication.supportsIntegratedFind &&
|
|
PDFViewerApplication.findBar.opened
|
|
) {
|
|
PDFViewerApplication.findBar.close();
|
|
handled = true;
|
|
}
|
|
break;
|
|
case 40: // down arrow
|
|
case 34: // pg down
|
|
// vertical scrolling using arrow/pg keys
|
|
if (pdfViewer.isVerticalScrollbarEnabled) {
|
|
turnOnlyIfPageFit = true;
|
|
}
|
|
turnPage = 1;
|
|
break;
|
|
case 13: // enter key
|
|
case 32: // spacebar
|
|
if (!isViewerInPresentationMode) {
|
|
turnOnlyIfPageFit = true;
|
|
}
|
|
turnPage = 1;
|
|
break;
|
|
case 39: // right arrow
|
|
// horizontal scrolling using arrow keys
|
|
if (pdfViewer.isHorizontalScrollbarEnabled) {
|
|
turnOnlyIfPageFit = true;
|
|
}
|
|
/* falls through */
|
|
case 74: // 'j'
|
|
case 78: // 'n'
|
|
turnPage = 1;
|
|
break;
|
|
|
|
case 36: // home
|
|
if (isViewerInPresentationMode || PDFViewerApplication.page > 1) {
|
|
PDFViewerApplication.page = 1;
|
|
handled = true;
|
|
ensureViewerFocused = true;
|
|
}
|
|
break;
|
|
case 35: // end
|
|
if (
|
|
isViewerInPresentationMode ||
|
|
PDFViewerApplication.page < PDFViewerApplication.pagesCount
|
|
) {
|
|
PDFViewerApplication.page = PDFViewerApplication.pagesCount;
|
|
handled = true;
|
|
ensureViewerFocused = true;
|
|
}
|
|
break;
|
|
|
|
case 83: // 's'
|
|
PDFViewerApplication.pdfCursorTools.switchTool(CursorTool.SELECT);
|
|
break;
|
|
case 72: // 'h'
|
|
PDFViewerApplication.pdfCursorTools.switchTool(CursorTool.HAND);
|
|
break;
|
|
|
|
case 82: // 'r'
|
|
PDFViewerApplication.rotatePages(90);
|
|
break;
|
|
|
|
case 115: // F4
|
|
PDFViewerApplication.pdfSidebar.toggle();
|
|
break;
|
|
}
|
|
|
|
if (
|
|
turnPage !== 0 &&
|
|
(!turnOnlyIfPageFit || pdfViewer.currentScaleValue === "page-fit")
|
|
) {
|
|
if (turnPage > 0) {
|
|
pdfViewer.nextPage();
|
|
} else {
|
|
pdfViewer.previousPage();
|
|
}
|
|
handled = true;
|
|
}
|
|
}
|
|
|
|
// shift-key
|
|
if (cmd === 4) {
|
|
switch (evt.keyCode) {
|
|
case 13: // enter key
|
|
case 32: // spacebar
|
|
if (
|
|
!isViewerInPresentationMode &&
|
|
pdfViewer.currentScaleValue !== "page-fit"
|
|
) {
|
|
break;
|
|
}
|
|
if (PDFViewerApplication.page > 1) {
|
|
PDFViewerApplication.page--;
|
|
}
|
|
handled = true;
|
|
break;
|
|
|
|
case 82: // 'r'
|
|
PDFViewerApplication.rotatePages(-90);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!handled && !isViewerInPresentationMode) {
|
|
// 33=Page Up 34=Page Down 35=End 36=Home
|
|
// 37=Left 38=Up 39=Right 40=Down
|
|
// 32=Spacebar
|
|
if (
|
|
(evt.keyCode >= 33 && evt.keyCode <= 40) ||
|
|
(evt.keyCode === 32 && curElementTagName !== "BUTTON")
|
|
) {
|
|
ensureViewerFocused = true;
|
|
}
|
|
}
|
|
|
|
if (ensureViewerFocused && !pdfViewer.containsElement(curElement)) {
|
|
// The page container is not focused, but a page navigation key has been
|
|
// pressed. Change the focus to the viewer container to make sure that
|
|
// navigation by keyboard works as expected.
|
|
pdfViewer.focus();
|
|
}
|
|
|
|
if (handled) {
|
|
evt.preventDefault();
|
|
}
|
|
}
|
|
|
|
function beforeUnload(evt) {
|
|
evt.preventDefault();
|
|
evt.returnValue = "";
|
|
return false;
|
|
}
|
|
|
|
/* Abstract factory for the print service. */
|
|
const PDFPrintServiceFactory = {
|
|
instance: {
|
|
supportsPrinting: false,
|
|
createPrintService() {
|
|
throw new Error("Not implemented: createPrintService");
|
|
},
|
|
},
|
|
};
|
|
|
|
export {
|
|
DefaultExternalServices,
|
|
PDFPrintServiceFactory,
|
|
PDFViewerApplication,
|
|
};
|