Skip to content

[api-minor] Introduce a PrintAnnotationStorage with *frozen* serializable data #15043

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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