Skip to content

Commit

Permalink
[api-minor] Introduce a PrintAnnotationStorage with *frozen* serial…
Browse files Browse the repository at this point in the history
…izable data

Given that printing is triggered *synchronously* in browsers, it's thus possible for scripting (in PDF documents) to modify the Annotation-data while printing is currently ongoing.
To work-around that we add a new printing-specific `AnnotationStorage`, where the serializable data is *frozen* upon initialization, which the viewer can thus create/utilize during printing.
  • Loading branch information
Snuffleupagus committed Jun 23, 2022
1 parent c5dc082 commit 1cc7cec
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 41 deletions.
64 changes: 50 additions & 14 deletions src/display/annotation_storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
* limitations under the License.
*/

import { objectFromMap, unreachable } from "../shared/util.js";
import { AnnotationEditor } from "./editor/editor.js";
import { MurmurHash3_64 } from "../shared/murmurhash3.js";
import { objectFromMap } from "../shared/util.js";

/**
* Key/value storage for annotation data in forms.
Expand Down Expand Up @@ -98,7 +98,7 @@ class AnnotationStorage {
this._storage.set(key, value);
}
if (modified) {
this._setModified();
this.#setModified();
}
}

Expand All @@ -110,10 +110,7 @@ class AnnotationStorage {
return this._storage.size;
}

/**
* @private
*/
_setModified() {
#setModified() {
if (!this._modified) {
this._modified = true;
if (typeof this.onSetModified === "function") {
Expand All @@ -131,6 +128,13 @@ class AnnotationStorage {
}
}

/**
* @returns {PrintAnnotationStorage}
*/
get print() {
return new PrintAnnotationStorage(this);
}

/**
* PLEASE NOTE: Only intended for usage within the API itself.
* @ignore
Expand All @@ -139,11 +143,10 @@ class AnnotationStorage {
if (this._storage.size === 0) {
return null;
}

const clone = new Map();
for (const [key, value] of this._storage) {
const val = value instanceof AnnotationEditor ? value.serialize() : value;
clone.set(key, val);

for (const [key, val] of this._storage) {
clone.set(key, val instanceof AnnotationEditor ? val.serialize() : val);
}
return clone;
}
Expand All @@ -152,15 +155,48 @@ class AnnotationStorage {
* PLEASE NOTE: Only intended for usage within the API itself.
* @ignore
*/
get hash() {
static getHash(map) {
if (!map) {
return "";
}
const hash = new MurmurHash3_64();

for (const [key, value] of this._storage) {
const val = value instanceof AnnotationEditor ? value.serialize() : value;
for (const [key, val] of map) {
hash.update(`${key}:${JSON.stringify(val)}`);
}
return hash.hexdigest();
}
}

export { AnnotationStorage };
/**
* A special `AnnotationStorage` for use during printing, where the serializable
* data is *frozen* upon initialization, to prevent scripting from modifying its
* contents. (Necessary since printing is triggered synchronously in browsers.)
*/
class PrintAnnotationStorage extends AnnotationStorage {
#serializable = null;

constructor(parent) {
super();
// Create a *copy* of the data, since Objects are passed by reference in JS.
this.#serializable = structuredClone(parent.serializable);
}

/**
* @returns {PrintAnnotationStorage}
*/
// eslint-disable-next-line getter-return
get print() {
unreachable("Should not call PrintAnnotationStorage.print");
}

/**
* PLEASE NOTE: Only intended for usage within the API itself.
* @ignore
*/
get serializable() {
return this.#serializable;
}
}

export { AnnotationStorage, PrintAnnotationStorage };
36 changes: 26 additions & 10 deletions src/display/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ import {
unreachable,
warn,
} from "../shared/util.js";
import {
AnnotationStorage,
PrintAnnotationStorage,
} from "./annotation_storage.js";
import {
deprecated,
DOMCanvasFactory,
Expand All @@ -49,7 +53,6 @@ import {
StatTimer,
} from "./display_utils.js";
import { FontFaceObject, FontLoader } from "./font_loader.js";
import { AnnotationStorage } from "./annotation_storage.js";
import { CanvasGraphics } from "./canvas.js";
import { GlobalWorkerOptions } from "./worker_options.js";
import { isNodeJS } from "../shared/is_node.js";
Expand Down Expand Up @@ -1181,6 +1184,7 @@ class PDFDocumentProxy {
* states set.
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap] - Map some
* annotation ids with canvases used to render them.
* @property {PrintAnnotationStorage} [printAnnotationStorage]
*/

/**
Expand All @@ -1201,6 +1205,7 @@ class PDFDocumentProxy {
* (as above) but where interactive form elements are updated with data
* from the {@link AnnotationStorage}-instance; useful e.g. for printing.
* The default value is `AnnotationMode.ENABLE`.
* @property {PrintAnnotationStorage} [printAnnotationStorage]
*/

/**
Expand Down Expand Up @@ -1399,6 +1404,7 @@ class PDFPageProxy {
optionalContentConfigPromise = null,
annotationCanvasMap = null,
pageColors = null,
printAnnotationStorage = null,
}) {
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("GENERIC")) {
if (arguments[0]?.renderInteractiveForms !== undefined) {
Expand Down Expand Up @@ -1433,7 +1439,8 @@ class PDFPageProxy {

const intentArgs = this._transport.getRenderingIntent(
intent,
annotationMode
annotationMode,
printAnnotationStorage
);
// If there was a pending destroy, cancel it so no cleanup happens during
// this call to render.
Expand Down Expand Up @@ -1560,6 +1567,7 @@ class PDFPageProxy {
getOperatorList({
intent = "display",
annotationMode = AnnotationMode.ENABLE,
printAnnotationStorage = null,
} = {}) {
function operatorListChanged() {
if (intentState.operatorList.lastChunk) {
Expand All @@ -1572,6 +1580,7 @@ class PDFPageProxy {
const intentArgs = this._transport.getRenderingIntent(
intent,
annotationMode,
printAnnotationStorage,
/* isOpList = */ true
);
let intentState = this._intentStates.get(intentArgs.cacheKey);
Expand Down Expand Up @@ -1800,7 +1809,7 @@ class PDFPageProxy {
/**
* @private
*/
_pumpOperatorList({ renderingIntent, cacheKey }) {
_pumpOperatorList({ renderingIntent, cacheKey, annotationStorageMap }) {
if (
typeof PDFJSDev === "undefined" ||
PDFJSDev.test("!PRODUCTION || TESTING")
Expand All @@ -1817,10 +1826,7 @@ class PDFPageProxy {
pageIndex: this._pageIndex,
intent: renderingIntent,
cacheKey,
annotationStorage:
renderingIntent & RenderingIntentFlag.ANNOTATIONS_STORAGE
? this._transport.annotationStorage.serializable
: null,
annotationStorage: annotationStorageMap,
}
);
const reader = readableStream.getReader();
Expand Down Expand Up @@ -2406,10 +2412,11 @@ class WorkerTransport {
getRenderingIntent(
intent,
annotationMode = AnnotationMode.ENABLE,
printAnnotationStorage = null,
isOpList = false
) {
let renderingIntent = RenderingIntentFlag.DISPLAY; // Default value.
let annotationHash = "";
let annotationMap = null;

switch (intent) {
case "any":
Expand All @@ -2436,7 +2443,13 @@ class WorkerTransport {
case AnnotationMode.ENABLE_STORAGE:
renderingIntent += RenderingIntentFlag.ANNOTATIONS_STORAGE;

annotationHash = this.annotationStorage.hash;
const annotationStorage =
renderingIntent & RenderingIntentFlag.PRINT &&
printAnnotationStorage instanceof PrintAnnotationStorage
? printAnnotationStorage
: this.annotationStorage;

annotationMap = annotationStorage.serializable;
break;
default:
warn(`getRenderingIntent - invalid annotationMode: ${annotationMode}`);
Expand All @@ -2448,7 +2461,10 @@ class WorkerTransport {

return {
renderingIntent,
cacheKey: `${renderingIntent}_${annotationHash}`,
cacheKey: `${renderingIntent}_${AnnotationStorage.getHash(
annotationMap
)}`,
annotationStorageMap: annotationMap,
};
}

Expand Down
70 changes: 70 additions & 0 deletions test/unit/api_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
RenderingCancelledException,
StatTimer,
} from "../../src/display/display_utils.js";
import { AnnotationStorage } from "../../src/display/annotation_storage.js";
import { AutoPrintRegExp } from "../../web/ui_utils.js";
import { GlobalImageCache } from "../../src/core/image_utils.js";
import { GlobalWorkerOptions } from "../../src/display/worker_options.js";
Expand Down Expand Up @@ -2826,6 +2827,75 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`)
await loadingTask.destroy();
firstImgData = null;
});

it("render for printing, with `printAnnotationStorage` set", async function () {
async function getPrintData(printAnnotationStorage = null) {
const canvasAndCtx = CanvasFactory.create(
viewport.width,
viewport.height
);
const renderTask = pdfPage.render({
canvasContext: canvasAndCtx.context,
canvasFactory: CanvasFactory,
viewport,
intent: "print",
annotationMode: AnnotationMode.ENABLE_STORAGE,
printAnnotationStorage,
});

await renderTask.promise;
const printData = canvasAndCtx.canvas.toDataURL();
CanvasFactory.destroy(canvasAndCtx);

return printData;
}

const loadingTask = getDocument(
buildGetDocumentParams("annotation-tx.pdf")
);
const pdfDoc = await loadingTask.promise;
const pdfPage = await pdfDoc.getPage(1);
const viewport = pdfPage.getViewport({ scale: 1 });

// Update the contents of the form-field.
const { annotationStorage } = pdfDoc;
annotationStorage.setValue("22R", { value: "Hello World" });

// Render for printing, with default parameters.
const printOriginalData = await getPrintData();

// Get the *frozen* print-storage for use during printing.
const printAnnotationStorage = annotationStorage.print;
// Update the contents of the form-field again.
annotationStorage.setValue("22R", { value: "Printing again..." });

const annotationHash = AnnotationStorage.getHash(
annotationStorage.serializable
);
const printAnnotationHash = AnnotationStorage.getHash(
printAnnotationStorage.serializable
);
// Sanity check to ensure that the print-storage didn't change,
// after the form-field was updated.
expect(printAnnotationHash).not.toEqual(annotationHash);

// Render for printing again, after updating the form-field,
// with default parameters.
const printAgainData = await getPrintData();

// Render for printing again, after updating the form-field,
// with `printAnnotationStorage` set.
const printStorageData = await getPrintData(printAnnotationStorage);

// Ensure that printing again, with default parameters,
// actually uses the "new" form-field data.
expect(printAgainData).not.toEqual(printOriginalData);
// Finally ensure that printing, with `printAnnotationStorage` set,
// still uses the "previous" form-field data.
expect(printStorageData).toEqual(printOriginalData);

await loadingTask.destroy();
});
});

describe("Multiple `getDocument` instances", function () {
Expand Down
22 changes: 16 additions & 6 deletions web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ const PDFViewerApplication = {
_wheelUnusedTicks: 0,
_idleCallbacks: new Set(),
_PDFBug: null,
_printAnnotationStoragePromise: null,

// Called once when the document is loaded.
async initialize(appConfig) {
Expand Down Expand Up @@ -1790,9 +1791,14 @@ const PDFViewerApplication = {
},

beforePrint() {
// Given that the "beforeprint" browser event is synchronous, we
// unfortunately cannot await the scripting event dispatching here.
this.pdfScriptingManager.dispatchWillPrint();
this._printAnnotationStoragePromise = this.pdfScriptingManager
.dispatchWillPrint()
.catch(() => {
/* Avoid breaking printing; ignoring errors. */
})
.then(() => {
return this.pdfDocument?.annotationStorage.print;
});

if (this.printService) {
// There is no way to suppress beforePrint/afterPrint events,
Expand Down Expand Up @@ -1830,6 +1836,7 @@ const PDFViewerApplication = {
printContainer,
printResolution,
optionalContentConfigPromise,
this._printAnnotationStoragePromise,
this.l10n
);
this.printService = printService;
Expand All @@ -1843,9 +1850,12 @@ const PDFViewerApplication = {
},

afterPrint() {
// Given that the "afterprint" browser event is synchronous, we
// unfortunately cannot await the scripting event dispatching here.
this.pdfScriptingManager.dispatchDidPrint();
if (this._printAnnotationStoragePromise) {
this._printAnnotationStoragePromise.then(() => {
this.pdfScriptingManager.dispatchDidPrint();
});
this._printAnnotationStoragePromise = null;
}

if (this.printService) {
this.printService.destroy();
Expand Down
Loading

0 comments on commit 1cc7cec

Please sign in to comment.