Merge pull request #15043 from Snuffleupagus/PrintAnnotationStorage

[api-minor] Introduce a `PrintAnnotationStorage` with *frozen* serializable data
This commit is contained in:
Jonas Jenwald 2022-06-24 09:30:07 +02:00 committed by GitHub
commit 3fab4af949
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 196 additions and 41 deletions

View File

@ -13,9 +13,9 @@
* limitations under the License. * limitations under the License.
*/ */
import { objectFromMap, unreachable } from "../shared/util.js";
import { AnnotationEditor } from "./editor/editor.js"; import { AnnotationEditor } from "./editor/editor.js";
import { MurmurHash3_64 } from "../shared/murmurhash3.js"; import { MurmurHash3_64 } from "../shared/murmurhash3.js";
import { objectFromMap } from "../shared/util.js";
/** /**
* Key/value storage for annotation data in forms. * Key/value storage for annotation data in forms.
@ -98,7 +98,7 @@ class AnnotationStorage {
this._storage.set(key, value); this._storage.set(key, value);
} }
if (modified) { if (modified) {
this._setModified(); this.#setModified();
} }
} }
@ -110,10 +110,7 @@ class AnnotationStorage {
return this._storage.size; return this._storage.size;
} }
/** #setModified() {
* @private
*/
_setModified() {
if (!this._modified) { if (!this._modified) {
this._modified = true; this._modified = true;
if (typeof this.onSetModified === "function") { if (typeof this.onSetModified === "function") {
@ -131,6 +128,13 @@ class AnnotationStorage {
} }
} }
/**
* @returns {PrintAnnotationStorage}
*/
get print() {
return new PrintAnnotationStorage(this);
}
/** /**
* PLEASE NOTE: Only intended for usage within the API itself. * PLEASE NOTE: Only intended for usage within the API itself.
* @ignore * @ignore
@ -139,11 +143,10 @@ class AnnotationStorage {
if (this._storage.size === 0) { if (this._storage.size === 0) {
return null; return null;
} }
const clone = new Map(); const clone = new Map();
for (const [key, value] of this._storage) {
const val = value instanceof AnnotationEditor ? value.serialize() : value; for (const [key, val] of this._storage) {
clone.set(key, val); clone.set(key, val instanceof AnnotationEditor ? val.serialize() : val);
} }
return clone; return clone;
} }
@ -152,15 +155,48 @@ class AnnotationStorage {
* PLEASE NOTE: Only intended for usage within the API itself. * PLEASE NOTE: Only intended for usage within the API itself.
* @ignore * @ignore
*/ */
get hash() { static getHash(map) {
if (!map) {
return "";
}
const hash = new MurmurHash3_64(); const hash = new MurmurHash3_64();
for (const [key, value] of this._storage) { for (const [key, val] of map) {
const val = value instanceof AnnotationEditor ? value.serialize() : value;
hash.update(`${key}:${JSON.stringify(val)}`); hash.update(`${key}:${JSON.stringify(val)}`);
} }
return hash.hexdigest(); 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 };

View File

@ -37,6 +37,10 @@ import {
unreachable, unreachable,
warn, warn,
} from "../shared/util.js"; } from "../shared/util.js";
import {
AnnotationStorage,
PrintAnnotationStorage,
} from "./annotation_storage.js";
import { import {
deprecated, deprecated,
DOMCanvasFactory, DOMCanvasFactory,
@ -49,7 +53,6 @@ import {
StatTimer, StatTimer,
} from "./display_utils.js"; } from "./display_utils.js";
import { FontFaceObject, FontLoader } from "./font_loader.js"; import { FontFaceObject, FontLoader } from "./font_loader.js";
import { AnnotationStorage } from "./annotation_storage.js";
import { CanvasGraphics } from "./canvas.js"; import { CanvasGraphics } from "./canvas.js";
import { GlobalWorkerOptions } from "./worker_options.js"; import { GlobalWorkerOptions } from "./worker_options.js";
import { isNodeJS } from "../shared/is_node.js"; import { isNodeJS } from "../shared/is_node.js";
@ -1181,6 +1184,7 @@ class PDFDocumentProxy {
* states set. * states set.
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap] - Map some * @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap] - Map some
* annotation ids with canvases used to render them. * annotation ids with canvases used to render them.
* @property {PrintAnnotationStorage} [printAnnotationStorage]
*/ */
/** /**
@ -1201,6 +1205,7 @@ class PDFDocumentProxy {
* (as above) but where interactive form elements are updated with data * (as above) but where interactive form elements are updated with data
* from the {@link AnnotationStorage}-instance; useful e.g. for printing. * from the {@link AnnotationStorage}-instance; useful e.g. for printing.
* The default value is `AnnotationMode.ENABLE`. * The default value is `AnnotationMode.ENABLE`.
* @property {PrintAnnotationStorage} [printAnnotationStorage]
*/ */
/** /**
@ -1399,6 +1404,7 @@ class PDFPageProxy {
optionalContentConfigPromise = null, optionalContentConfigPromise = null,
annotationCanvasMap = null, annotationCanvasMap = null,
pageColors = null, pageColors = null,
printAnnotationStorage = null,
}) { }) {
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("GENERIC")) { if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("GENERIC")) {
if (arguments[0]?.renderInteractiveForms !== undefined) { if (arguments[0]?.renderInteractiveForms !== undefined) {
@ -1433,7 +1439,8 @@ class PDFPageProxy {
const intentArgs = this._transport.getRenderingIntent( const intentArgs = this._transport.getRenderingIntent(
intent, intent,
annotationMode annotationMode,
printAnnotationStorage
); );
// If there was a pending destroy, cancel it so no cleanup happens during // If there was a pending destroy, cancel it so no cleanup happens during
// this call to render. // this call to render.
@ -1560,6 +1567,7 @@ class PDFPageProxy {
getOperatorList({ getOperatorList({
intent = "display", intent = "display",
annotationMode = AnnotationMode.ENABLE, annotationMode = AnnotationMode.ENABLE,
printAnnotationStorage = null,
} = {}) { } = {}) {
function operatorListChanged() { function operatorListChanged() {
if (intentState.operatorList.lastChunk) { if (intentState.operatorList.lastChunk) {
@ -1572,6 +1580,7 @@ class PDFPageProxy {
const intentArgs = this._transport.getRenderingIntent( const intentArgs = this._transport.getRenderingIntent(
intent, intent,
annotationMode, annotationMode,
printAnnotationStorage,
/* isOpList = */ true /* isOpList = */ true
); );
let intentState = this._intentStates.get(intentArgs.cacheKey); let intentState = this._intentStates.get(intentArgs.cacheKey);
@ -1800,7 +1809,7 @@ class PDFPageProxy {
/** /**
* @private * @private
*/ */
_pumpOperatorList({ renderingIntent, cacheKey }) { _pumpOperatorList({ renderingIntent, cacheKey, annotationStorageMap }) {
if ( if (
typeof PDFJSDev === "undefined" || typeof PDFJSDev === "undefined" ||
PDFJSDev.test("!PRODUCTION || TESTING") PDFJSDev.test("!PRODUCTION || TESTING")
@ -1817,10 +1826,7 @@ class PDFPageProxy {
pageIndex: this._pageIndex, pageIndex: this._pageIndex,
intent: renderingIntent, intent: renderingIntent,
cacheKey, cacheKey,
annotationStorage: annotationStorage: annotationStorageMap,
renderingIntent & RenderingIntentFlag.ANNOTATIONS_STORAGE
? this._transport.annotationStorage.serializable
: null,
} }
); );
const reader = readableStream.getReader(); const reader = readableStream.getReader();
@ -2406,10 +2412,11 @@ class WorkerTransport {
getRenderingIntent( getRenderingIntent(
intent, intent,
annotationMode = AnnotationMode.ENABLE, annotationMode = AnnotationMode.ENABLE,
printAnnotationStorage = null,
isOpList = false isOpList = false
) { ) {
let renderingIntent = RenderingIntentFlag.DISPLAY; // Default value. let renderingIntent = RenderingIntentFlag.DISPLAY; // Default value.
let annotationHash = ""; let annotationMap = null;
switch (intent) { switch (intent) {
case "any": case "any":
@ -2436,7 +2443,13 @@ class WorkerTransport {
case AnnotationMode.ENABLE_STORAGE: case AnnotationMode.ENABLE_STORAGE:
renderingIntent += RenderingIntentFlag.ANNOTATIONS_STORAGE; renderingIntent += RenderingIntentFlag.ANNOTATIONS_STORAGE;
annotationHash = this.annotationStorage.hash; const annotationStorage =
renderingIntent & RenderingIntentFlag.PRINT &&
printAnnotationStorage instanceof PrintAnnotationStorage
? printAnnotationStorage
: this.annotationStorage;
annotationMap = annotationStorage.serializable;
break; break;
default: default:
warn(`getRenderingIntent - invalid annotationMode: ${annotationMode}`); warn(`getRenderingIntent - invalid annotationMode: ${annotationMode}`);
@ -2448,7 +2461,10 @@ class WorkerTransport {
return { return {
renderingIntent, renderingIntent,
cacheKey: `${renderingIntent}_${annotationHash}`, cacheKey: `${renderingIntent}_${AnnotationStorage.getHash(
annotationMap
)}`,
annotationStorageMap: annotationMap,
}; };
} }

View File

@ -48,6 +48,7 @@ import {
RenderingCancelledException, RenderingCancelledException,
StatTimer, StatTimer,
} from "../../src/display/display_utils.js"; } from "../../src/display/display_utils.js";
import { AnnotationStorage } from "../../src/display/annotation_storage.js";
import { AutoPrintRegExp } from "../../web/ui_utils.js"; import { AutoPrintRegExp } from "../../web/ui_utils.js";
import { GlobalImageCache } from "../../src/core/image_utils.js"; import { GlobalImageCache } from "../../src/core/image_utils.js";
import { GlobalWorkerOptions } from "../../src/display/worker_options.js"; import { GlobalWorkerOptions } from "../../src/display/worker_options.js";
@ -2826,6 +2827,75 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`)
await loadingTask.destroy(); await loadingTask.destroy();
firstImgData = null; 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 () { describe("Multiple `getDocument` instances", function () {

View File

@ -254,6 +254,7 @@ const PDFViewerApplication = {
_wheelUnusedTicks: 0, _wheelUnusedTicks: 0,
_idleCallbacks: new Set(), _idleCallbacks: new Set(),
_PDFBug: null, _PDFBug: null,
_printAnnotationStoragePromise: null,
// Called once when the document is loaded. // Called once when the document is loaded.
async initialize(appConfig) { async initialize(appConfig) {
@ -1790,9 +1791,14 @@ const PDFViewerApplication = {
}, },
beforePrint() { beforePrint() {
// Given that the "beforeprint" browser event is synchronous, we this._printAnnotationStoragePromise = this.pdfScriptingManager
// unfortunately cannot await the scripting event dispatching here. .dispatchWillPrint()
this.pdfScriptingManager.dispatchWillPrint(); .catch(() => {
/* Avoid breaking printing; ignoring errors. */
})
.then(() => {
return this.pdfDocument?.annotationStorage.print;
});
if (this.printService) { if (this.printService) {
// There is no way to suppress beforePrint/afterPrint events, // There is no way to suppress beforePrint/afterPrint events,
@ -1830,6 +1836,7 @@ const PDFViewerApplication = {
printContainer, printContainer,
printResolution, printResolution,
optionalContentConfigPromise, optionalContentConfigPromise,
this._printAnnotationStoragePromise,
this.l10n this.l10n
); );
this.printService = printService; this.printService = printService;
@ -1843,9 +1850,12 @@ const PDFViewerApplication = {
}, },
afterPrint() { afterPrint() {
// Given that the "afterprint" browser event is synchronous, we if (this._printAnnotationStoragePromise) {
// unfortunately cannot await the scripting event dispatching here. this._printAnnotationStoragePromise.then(() => {
this.pdfScriptingManager.dispatchDidPrint(); this.pdfScriptingManager.dispatchDidPrint();
});
this._printAnnotationStoragePromise = null;
}
if (this.printService) { if (this.printService) {
this.printService.destroy(); this.printService.destroy();

View File

@ -29,7 +29,8 @@ function composePage(
size, size,
printContainer, printContainer,
printResolution, printResolution,
optionalContentConfigPromise optionalContentConfigPromise,
printAnnotationStoragePromise
) { ) {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
@ -61,9 +62,12 @@ function composePage(
ctx.restore(); ctx.restore();
let thisRenderTask = null; let thisRenderTask = null;
pdfDocument
.getPage(pageNumber) Promise.all([
.then(function (pdfPage) { pdfDocument.getPage(pageNumber),
printAnnotationStoragePromise,
])
.then(function ([pdfPage, printAnnotationStorage]) {
if (currentRenderTask) { if (currentRenderTask) {
currentRenderTask.cancel(); currentRenderTask.cancel();
currentRenderTask = null; currentRenderTask = null;
@ -75,6 +79,7 @@ function composePage(
intent: "print", intent: "print",
annotationMode: AnnotationMode.ENABLE_STORAGE, annotationMode: AnnotationMode.ENABLE_STORAGE,
optionalContentConfigPromise, optionalContentConfigPromise,
printAnnotationStorage,
}; };
currentRenderTask = thisRenderTask = pdfPage.render(renderContext); currentRenderTask = thisRenderTask = pdfPage.render(renderContext);
return thisRenderTask.promise; return thisRenderTask.promise;
@ -114,7 +119,8 @@ function FirefoxPrintService(
pagesOverview, pagesOverview,
printContainer, printContainer,
printResolution, printResolution,
optionalContentConfigPromise = null optionalContentConfigPromise = null,
printAnnotationStoragePromise = null
) { ) {
this.pdfDocument = pdfDocument; this.pdfDocument = pdfDocument;
this.pagesOverview = pagesOverview; this.pagesOverview = pagesOverview;
@ -122,6 +128,8 @@ function FirefoxPrintService(
this._printResolution = printResolution || 150; this._printResolution = printResolution || 150;
this._optionalContentConfigPromise = this._optionalContentConfigPromise =
optionalContentConfigPromise || pdfDocument.getOptionalContentConfig(); optionalContentConfigPromise || pdfDocument.getOptionalContentConfig();
this._optionalContentConfigPromise =
printAnnotationStoragePromise || Promise.resolve();
} }
FirefoxPrintService.prototype = { FirefoxPrintService.prototype = {
@ -132,6 +140,7 @@ FirefoxPrintService.prototype = {
printContainer, printContainer,
_printResolution, _printResolution,
_optionalContentConfigPromise, _optionalContentConfigPromise,
_printAnnotationStoragePromise,
} = this; } = this;
const body = document.querySelector("body"); const body = document.querySelector("body");
@ -149,7 +158,8 @@ FirefoxPrintService.prototype = {
pagesOverview[i], pagesOverview[i],
printContainer, printContainer,
_printResolution, _printResolution,
_optionalContentConfigPromise _optionalContentConfigPromise,
_printAnnotationStoragePromise
); );
} }
}, },
@ -175,14 +185,16 @@ PDFPrintServiceFactory.instance = {
pagesOverview, pagesOverview,
printContainer, printContainer,
printResolution, printResolution,
optionalContentConfigPromise optionalContentConfigPromise,
printAnnotationStoragePromise
) { ) {
return new FirefoxPrintService( return new FirefoxPrintService(
pdfDocument, pdfDocument,
pagesOverview, pagesOverview,
printContainer, printContainer,
printResolution, printResolution,
optionalContentConfigPromise optionalContentConfigPromise,
printAnnotationStoragePromise
); );
}, },
}; };

View File

@ -29,7 +29,8 @@ function renderPage(
pageNumber, pageNumber,
size, size,
printResolution, printResolution,
optionalContentConfigPromise optionalContentConfigPromise,
printAnnotationStoragePromise
) { ) {
const scratchCanvas = activeService.scratchCanvas; const scratchCanvas = activeService.scratchCanvas;
@ -44,7 +45,10 @@ function renderPage(
ctx.fillRect(0, 0, scratchCanvas.width, scratchCanvas.height); ctx.fillRect(0, 0, scratchCanvas.width, scratchCanvas.height);
ctx.restore(); ctx.restore();
return pdfDocument.getPage(pageNumber).then(function (pdfPage) { return Promise.all([
pdfDocument.getPage(pageNumber),
printAnnotationStoragePromise,
]).then(function ([pdfPage, printAnnotationStorage]) {
const renderContext = { const renderContext = {
canvasContext: ctx, canvasContext: ctx,
transform: [PRINT_UNITS, 0, 0, PRINT_UNITS, 0, 0], transform: [PRINT_UNITS, 0, 0, PRINT_UNITS, 0, 0],
@ -52,6 +56,7 @@ function renderPage(
intent: "print", intent: "print",
annotationMode: AnnotationMode.ENABLE_STORAGE, annotationMode: AnnotationMode.ENABLE_STORAGE,
optionalContentConfigPromise, optionalContentConfigPromise,
printAnnotationStorage,
}; };
return pdfPage.render(renderContext).promise; return pdfPage.render(renderContext).promise;
}); });
@ -63,6 +68,7 @@ function PDFPrintService(
printContainer, printContainer,
printResolution, printResolution,
optionalContentConfigPromise = null, optionalContentConfigPromise = null,
printAnnotationStoragePromise = null,
l10n l10n
) { ) {
this.pdfDocument = pdfDocument; this.pdfDocument = pdfDocument;
@ -71,6 +77,8 @@ function PDFPrintService(
this._printResolution = printResolution || 150; this._printResolution = printResolution || 150;
this._optionalContentConfigPromise = this._optionalContentConfigPromise =
optionalContentConfigPromise || pdfDocument.getOptionalContentConfig(); optionalContentConfigPromise || pdfDocument.getOptionalContentConfig();
this._printAnnotationStoragePromise =
printAnnotationStoragePromise || Promise.resolve();
this.l10n = l10n; this.l10n = l10n;
this.currentPage = -1; this.currentPage = -1;
// The temporary canvas where renderPage paints one page at a time. // The temporary canvas where renderPage paints one page at a time.
@ -160,7 +168,8 @@ PDFPrintService.prototype = {
/* pageNumber = */ index + 1, /* pageNumber = */ index + 1,
this.pagesOverview[index], this.pagesOverview[index],
this._printResolution, this._printResolution,
this._optionalContentConfigPromise this._optionalContentConfigPromise,
this._printAnnotationStoragePromise
) )
.then(this.useRenderedPage.bind(this)) .then(this.useRenderedPage.bind(this))
.then(function () { .then(function () {
@ -359,6 +368,7 @@ PDFPrintServiceFactory.instance = {
printContainer, printContainer,
printResolution, printResolution,
optionalContentConfigPromise, optionalContentConfigPromise,
printAnnotationStoragePromise,
l10n l10n
) { ) {
if (activeService) { if (activeService) {
@ -370,6 +380,7 @@ PDFPrintServiceFactory.instance = {
printContainer, printContainer,
printResolution, printResolution,
optionalContentConfigPromise, optionalContentConfigPromise,
printAnnotationStoragePromise,
l10n l10n
); );
return activeService; return activeService;