diff --git a/src/core/annotation.js b/src/core/annotation.js index fe48f13872d52f..365503cfe8b207 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -68,7 +68,6 @@ import { FileSpec } from "./file_spec.js"; import { JpegStream } from "./jpeg_stream.js"; import { ObjectLoader } from "./object_loader.js"; import { OperatorList } from "./operator_list.js"; -import { writeObject } from "./writer.js"; import { XFAFactory } from "./xfa/factory.js"; class AnnotationFactory { @@ -332,10 +331,15 @@ class AnnotationFactory { return imagePromises; } - static async saveNewAnnotations(evaluator, task, annotations, imagePromises) { + static async saveNewAnnotations( + evaluator, + task, + annotations, + imagePromises, + changes + ) { const xref = evaluator.xref; let baseFontRef; - const dependencies = []; const promises = []; const { isOffscreenCanvasSupported } = evaluator.options; @@ -351,38 +355,33 @@ class AnnotationFactory { baseFont.set("Type", Name.get("Font")); baseFont.set("Subtype", Name.get("Type1")); baseFont.set("Encoding", Name.get("WinAnsiEncoding")); - const buffer = []; baseFontRef = xref.getNewTemporaryRef(); - await writeObject(baseFontRef, baseFont, buffer, xref); - dependencies.push({ ref: baseFontRef, data: buffer.join("") }); + changes.put(baseFontRef, { + data: baseFont, + }); } promises.push( - FreeTextAnnotation.createNewAnnotation( - xref, - annotation, - dependencies, - { evaluator, task, baseFontRef } - ) + FreeTextAnnotation.createNewAnnotation(xref, annotation, changes, { + evaluator, + task, + baseFontRef, + }) ); break; case AnnotationEditorType.HIGHLIGHT: if (annotation.quadPoints) { promises.push( - HighlightAnnotation.createNewAnnotation( - xref, - annotation, - dependencies - ) + HighlightAnnotation.createNewAnnotation(xref, annotation, changes) ); } else { promises.push( - InkAnnotation.createNewAnnotation(xref, annotation, dependencies) + InkAnnotation.createNewAnnotation(xref, annotation, changes) ); } break; case AnnotationEditorType.INK: promises.push( - InkAnnotation.createNewAnnotation(xref, annotation, dependencies) + InkAnnotation.createNewAnnotation(xref, annotation, changes) ); break; case AnnotationEditorType.STAMP: @@ -391,26 +390,23 @@ class AnnotationFactory { : null; if (image?.imageStream) { const { imageStream, smaskStream } = image; - const buffer = []; if (smaskStream) { const smaskRef = xref.getNewTemporaryRef(); - await writeObject(smaskRef, smaskStream, buffer, xref); - dependencies.push({ ref: smaskRef, data: buffer.join("") }); + changes.put(smaskRef, { + data: smaskStream, + }); imageStream.dict.set("SMask", smaskRef); - buffer.length = 0; } const imageRef = (image.imageRef = xref.getNewTemporaryRef()); - await writeObject(imageRef, imageStream, buffer, xref); - dependencies.push({ ref: imageRef, data: buffer.join("") }); + changes.put(imageRef, { + data: imageStream, + }); image.imageStream = image.smaskStream = null; } promises.push( - StampAnnotation.createNewAnnotation( - xref, - annotation, - dependencies, - { image } - ) + StampAnnotation.createNewAnnotation(xref, annotation, changes, { + image, + }) ); break; } @@ -418,7 +414,6 @@ class AnnotationFactory { return { annotations: await Promise.all(promises), - dependencies, }; } @@ -1227,7 +1222,7 @@ class Annotation { return { opList, separateForm: false, separateCanvas: isUsingOwnCanvas }; } - async save(evaluator, task, annotationStorage) { + async save(evaluator, task, annotationStorage, changes) { return null; } @@ -1758,14 +1753,13 @@ class MarkupAnnotation extends Annotation { this._streams.push(this.appearance, appearanceStream); } - static async createNewAnnotation(xref, annotation, dependencies, params) { + static async createNewAnnotation(xref, annotation, changes, params) { if (!annotation.ref) { annotation.ref = xref.getNewTemporaryRef(); } const annotationRef = annotation.ref; const ap = await this.createNewAppearanceStream(annotation, xref, params); - const buffer = []; let annotationDict; if (ap) { @@ -1773,8 +1767,9 @@ class MarkupAnnotation extends Annotation { annotationDict = this.createNewDict(annotation, xref, { apRef, }); - await writeObject(apRef, ap, buffer, xref); - dependencies.push({ ref: apRef, data: buffer.join("") }); + changes.put(apRef, { + data: ap, + }); } else { annotationDict = this.createNewDict(annotation, xref, {}); } @@ -1782,10 +1777,11 @@ class MarkupAnnotation extends Annotation { annotationDict.set("StructParent", annotation.parentTreeId); } - buffer.length = 0; - await writeObject(annotationRef, annotationDict, buffer, xref); + changes.put(annotationRef, { + data: annotationDict, + }); - return { ref: annotationRef, data: buffer.join("") }; + return { ref: annotationRef }; } static async createNewPrintAnnotation( @@ -2112,7 +2108,7 @@ class WidgetAnnotation extends Annotation { amendSavedDict(annotationStorage, dict) {} - async save(evaluator, task, annotationStorage) { + async save(evaluator, task, annotationStorage, changes) { const storageEntry = annotationStorage?.get(this.data.id); const flags = this._buildFlags(storageEntry?.noView, storageEntry?.noPrint); let value = storageEntry?.value, @@ -2123,7 +2119,7 @@ class WidgetAnnotation extends Annotation { rotation === undefined && flags === undefined ) { - return null; + return; } value ||= this.data.fieldValue; } @@ -2137,7 +2133,7 @@ class WidgetAnnotation extends Annotation { isArrayEqual(value, this.data.fieldValue) && flags === undefined ) { - return null; + return; } if (rotation === undefined) { @@ -2154,7 +2150,7 @@ class WidgetAnnotation extends Annotation { ); if (appearance === null && flags === undefined) { // Appearance didn't change. - return null; + return; } } else { // No need to create an appearance: the pdf has the flag /NeedAppearances @@ -2171,7 +2167,7 @@ class WidgetAnnotation extends Annotation { const originalDict = xref.fetchIfRef(this.ref); if (!(originalDict instanceof Dict)) { - return null; + return; } const dict = new Dict(xref); @@ -2208,12 +2204,11 @@ class WidgetAnnotation extends Annotation { dict.set("MK", maybeMK); } - const buffer = []; - const changes = [ - // data for the original object - // V field changed + reference for new AP - { ref: this.ref, data: "", xfa, needAppearances }, - ]; + changes.put(this.ref, { + data: dict, + xfa, + needAppearances, + }); if (appearance !== null) { const newRef = xref.getNewTemporaryRef(); const AP = new Dict(xref); @@ -2238,26 +2233,14 @@ class WidgetAnnotation extends Annotation { appearanceDict.set("Matrix", rotationMatrix); } - await writeObject(newRef, appearanceStream, buffer, xref); - - changes.push( - // data for the new AP - { - ref: newRef, - data: buffer.join(""), - xfa: null, - needAppearances: false, - } - ); - buffer.length = 0; + changes.put(newRef, { + data: appearanceStream, + xfa: null, + needAppearances: false, + }); } dict.set("M", `D:${getModificationDate()}`); - await writeObject(this.ref, dict, buffer, xref); - - changes[0].data = buffer.join(""); - - return changes; } async _getAppearance(evaluator, task, intent, annotationStorage) { @@ -3078,22 +3061,20 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { }; } - async save(evaluator, task, annotationStorage) { + async save(evaluator, task, annotationStorage, changes) { if (this.data.checkBox) { - return this._saveCheckbox(evaluator, task, annotationStorage); + this._saveCheckbox(evaluator, task, annotationStorage, changes); + return; } if (this.data.radioButton) { - return this._saveRadioButton(evaluator, task, annotationStorage); + this._saveRadioButton(evaluator, task, annotationStorage, changes); } - - // Nothing to save - return null; } - async _saveCheckbox(evaluator, task, annotationStorage) { + async _saveCheckbox(evaluator, task, annotationStorage, changes) { if (!annotationStorage) { - return null; + return; } const storageEntry = annotationStorage.get(this.data.id); const flags = this._buildFlags(storageEntry?.noView, storageEntry?.noPrint); @@ -3102,18 +3083,18 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { if (rotation === undefined && flags === undefined) { if (value === undefined) { - return null; + return; } const defaultValue = this.data.fieldValue === this.data.exportValue; if (defaultValue === value) { - return null; + return; } } let dict = evaluator.xref.fetchIfRef(this.ref); if (!(dict instanceof Dict)) { - return null; + return; } dict = dict.clone(); @@ -3142,15 +3123,16 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { dict.set("MK", maybeMK); } - const buffer = []; - await writeObject(this.ref, dict, buffer, evaluator.xref); - - return [{ ref: this.ref, data: buffer.join(""), xfa }]; + changes.put(this.ref, { + data: dict, + xfa, + needAppearances: false, + }); } - async _saveRadioButton(evaluator, task, annotationStorage) { + async _saveRadioButton(evaluator, task, annotationStorage, changes) { if (!annotationStorage) { - return null; + return; } const storageEntry = annotationStorage.get(this.data.id); const flags = this._buildFlags(storageEntry?.noView, storageEntry?.noPrint); @@ -3159,18 +3141,18 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { if (rotation === undefined && flags === undefined) { if (value === undefined) { - return null; + return; } const defaultValue = this.data.fieldValue === this.data.buttonValue; if (defaultValue === value) { - return null; + return; } } let dict = evaluator.xref.fetchIfRef(this.ref); if (!(dict instanceof Dict)) { - return null; + return; } dict = dict.clone(); @@ -3188,16 +3170,16 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { }; const name = Name.get(value ? this.data.buttonValue : "Off"); - const buffer = []; - let parentData = null; if (value) { if (this.parent instanceof Ref) { - const parent = evaluator.xref.fetch(this.parent); + const parent = evaluator.xref.fetch(this.parent).clone(); parent.set("V", name); - await writeObject(this.parent, parent, buffer, evaluator.xref); - parentData = buffer.join(""); - buffer.length = 0; + changes.put(this.parent, { + data: parent, + xfa: null, + needAppearances: false, + }); } else if (this.parent instanceof Dict) { this.parent.set("V", name); } @@ -3219,13 +3201,11 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { dict.set("MK", maybeMK); } - await writeObject(this.ref, dict, buffer, evaluator.xref); - const newRefs = [{ ref: this.ref, data: buffer.join(""), xfa }]; - if (parentData) { - newRefs.push({ ref: this.parent, data: parentData, xfa: null }); - } - - return newRefs; + changes.put(this.ref, { + data: dict, + xfa, + needAppearances: false, + }); } _getDefaultCheckedAppearance(params, type) { diff --git a/src/core/document.js b/src/core/document.js index ba17fdc2ab0827..3c259923347fa8 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -70,7 +70,6 @@ import { OperatorList } from "./operator_list.js"; import { PartialEvaluator } from "./evaluator.js"; import { StreamsSequenceStream } from "./decode_stream.js"; import { StructTreePage } from "./struct_tree.js"; -import { writeObject } from "./writer.js"; import { XFAFactory } from "./xfa/factory.js"; import { XRef } from "./xref.js"; @@ -314,7 +313,7 @@ class Page { await Promise.all(promises); } - async saveNewAnnotations(handler, task, annotations, imagePromises) { + async saveNewAnnotations(handler, task, annotations, imagePromises, changes) { if (this.xfaFactory) { throw new Error("XFA: Cannot save new annotations."); } @@ -348,7 +347,8 @@ class Page { partialEvaluator, task, annotations, - imagePromises + imagePromises, + changes ); for (const { ref } of newData.annotations) { @@ -358,27 +358,20 @@ class Page { } } - const savedDict = pageDict.get("Annots"); - pageDict.set("Annots", annotationsArray); - const buffer = []; - await writeObject(this.ref, pageDict, buffer, this.xref); - if (savedDict) { - pageDict.set("Annots", savedDict); - } + const dict = pageDict.clone(); + dict.set("Annots", annotationsArray); + changes.put(this.ref, { + data: dict, + }); - const objects = newData.dependencies; - objects.push( - { ref: this.ref, data: buffer.join("") }, - ...newData.annotations - ); for (const deletedRef of deletedAnnotations) { - objects.push({ ref: deletedRef, data: null }); + changes.put(deletedRef, { + data: null, + }); } - - return objects; } - save(handler, task, annotationStorage) { + save(handler, task, annotationStorage, changes) { const partialEvaluator = new PartialEvaluator({ xref: this.xref, handler, @@ -395,11 +388,11 @@ class Page { // Fetch the page's annotations and save the content // in case of interactive form fields. return this._parsedAnnotations.then(function (annotations) { - const newRefsPromises = []; + const promises = []; for (const annotation of annotations) { - newRefsPromises.push( + promises.push( annotation - .save(partialEvaluator, task, annotationStorage) + .save(partialEvaluator, task, annotationStorage, changes) .catch(function (reason) { warn( "save - ignoring annotation data during " + @@ -410,9 +403,7 @@ class Page { ); } - return Promise.all(newRefsPromises).then(function (newRefs) { - return newRefs.filter(newRef => !!newRef); - }); + return Promise.all(promises); }); } diff --git a/src/core/primitives.js b/src/core/primitives.js index a6801935de2fe7..ac8df8b838d987 100644 --- a/src/core/primitives.js +++ b/src/core/primitives.js @@ -383,6 +383,14 @@ class RefSetCache { this._map.clear(); } + isEmpty() { + return this._map.size === 0; + } + + *values() { + yield* this._map.values(); + } + *items() { for (const [ref, value] of this._map) { yield [Ref.fromString(ref), value]; diff --git a/src/core/struct_tree.js b/src/core/struct_tree.js index 44e7e5b9ca5b4e..7f5c7b45e97a40 100644 --- a/src/core/struct_tree.js +++ b/src/core/struct_tree.js @@ -17,7 +17,6 @@ import { AnnotationPrefix, stringToPDFString, warn } from "../shared/util.js"; import { Dict, isName, Name, Ref, RefSetCache } from "./primitives.js"; import { lookupNormalRect, stringToAsciiOrUTF16BE } from "./core_utils.js"; import { NumberTree } from "./name_number_tree.js"; -import { writeObject } from "./writer.js"; const MAX_DEPTH = 40; @@ -117,7 +116,7 @@ class StructTreeRoot { xref, catalogRef, pdfManager, - newRefs, + changes, }) { const root = pdfManager.catalog.cloneDict(); const cache = new RefSetCache(); @@ -146,18 +145,17 @@ class StructTreeRoot { nums, xref, pdfManager, - newRefs, + changes, cache, }); structTreeRoot.set("ParentTreeNextKey", nextKey); cache.put(parentTreeRef, parentTree); - const buffer = []; for (const [ref, obj] of cache.items()) { - buffer.length = 0; - await writeObject(ref, obj, buffer, xref); - newRefs.push({ ref, data: buffer.join("") }); + changes.put(ref, { + data: obj, + }); } } @@ -235,7 +233,7 @@ class StructTreeRoot { return true; } - async updateStructureTree({ newAnnotationsByPage, pdfManager, newRefs }) { + async updateStructureTree({ newAnnotationsByPage, pdfManager, changes }) { const xref = this.dict.xref; const structTreeRoot = this.dict.clone(); const structTreeRootRef = this.ref; @@ -273,7 +271,7 @@ class StructTreeRoot { nums, xref, pdfManager, - newRefs, + changes, cache, }); @@ -288,11 +286,10 @@ class StructTreeRoot { cache.put(numsRef, nums); } - const buffer = []; for (const [ref, obj] of cache.items()) { - buffer.length = 0; - await writeObject(ref, obj, buffer, xref); - newRefs.push({ ref, data: buffer.join("") }); + changes.put(ref, { + data: obj, + }); } } @@ -304,13 +301,12 @@ class StructTreeRoot { nums, xref, pdfManager, - newRefs, + changes, cache, }) { const objr = Name.get("OBJR"); let nextKey = -1; let structTreePageObjs; - const buffer = []; for (const [pageIndex, elements] of newAnnotationsByPage) { const page = await pdfManager.getPage(pageIndex); @@ -350,9 +346,9 @@ class StructTreeRoot { // We update the existing tag. const tagDict = xref.fetch(objRef).clone(); StructTreeRoot.#writeProperties(tagDict, accessibilityData); - buffer.length = 0; - await writeObject(objRef, tagDict, buffer, xref); - newRefs.push({ ref: objRef, data: buffer.join("") }); + changes.put(objRef, { + data: tagDict, + }); continue; } } diff --git a/src/core/worker.js b/src/core/worker.js index 2b6c9ac85222c5..5c6c799e89794c 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -34,7 +34,7 @@ import { getNewAnnotationsMap, XRefParseException, } from "./core_utils.js"; -import { Dict, isDict, Ref } from "./primitives.js"; +import { Dict, isDict, Ref, RefSetCache } from "./primitives.js"; import { LocalPdfManager, NetworkPdfManager } from "./pdf_manager.js"; import { AnnotationFactory } from "./annotation.js"; import { clearGlobalCaches } from "./cleanup_helper.js"; @@ -540,6 +540,7 @@ class WorkerMessageHandler { pdfManager.ensureDoc("linearization"), pdfManager.ensureCatalog("structTreeRoot"), ]; + const changes = new RefSetCache(); const promises = []; const newAnnotationsByPage = !isPureXfa @@ -590,7 +591,13 @@ class WorkerMessageHandler { pdfManager.getPage(pageIndex).then(page => { const task = new WorkerTask(`Save (editor): page ${pageIndex}`); return page - .saveNewAnnotations(handler, task, annotations, imagePromises) + .saveNewAnnotations( + handler, + task, + annotations, + imagePromises, + changes + ) .finally(function () { finishWorkerTask(task); }); @@ -600,26 +607,24 @@ class WorkerMessageHandler { if (structTreeRoot === null) { // No structTreeRoot exists, so we need to create one. promises.push( - Promise.all(newAnnotationPromises).then(async newRefs => { + Promise.all(newAnnotationPromises).then(async () => { await StructTreeRoot.createStructureTree({ newAnnotationsByPage, xref, catalogRef, pdfManager, - newRefs, + changes, }); - return newRefs; }) ); } else if (structTreeRoot) { promises.push( - Promise.all(newAnnotationPromises).then(async newRefs => { + Promise.all(newAnnotationPromises).then(async () => { await structTreeRoot.updateStructureTree({ newAnnotationsByPage, pdfManager, - newRefs, + changes, }); - return newRefs; }) ); } @@ -633,7 +638,7 @@ class WorkerMessageHandler { pdfManager.getPage(pageIndex).then(function (page) { const task = new WorkerTask(`Save: page ${pageIndex}`); return page - .save(handler, task, annotationStorage) + .save(handler, task, annotationStorage, changes) .finally(function () { finishWorkerTask(task); }); @@ -643,26 +648,21 @@ class WorkerMessageHandler { } const refs = await Promise.all(promises); - let newRefs = []; let xfaData = null; if (isPureXfa) { xfaData = refs[0]; if (!xfaData) { return stream.bytes; } - } else { - newRefs = refs.flat(2); - - if (newRefs.length === 0) { - // No new refs so just return the initial bytes - return stream.bytes; - } + } else if (changes.isEmpty()) { + // No new refs so just return the initial bytes + return stream.bytes; } const needAppearances = acroFormRef && acroForm instanceof Dict && - newRefs.some(ref => ref.needAppearances); + changes.values().some(ref => ref.needAppearances); const xfa = (acroForm instanceof Dict && acroForm.get("XFA")) || null; let xfaDatasetsRef = null; @@ -712,7 +712,7 @@ class WorkerMessageHandler { return incrementalUpdate({ originalData: stream.bytes, xrefInfo: newXrefInfo, - newRefs, + changes, xref, hasXfa: !!xfa, xfaDatasetsRef, diff --git a/src/core/writer.js b/src/core/writer.js index 54af13b4920aab..a9095d97b4b82a 100644 --- a/src/core/writer.js +++ b/src/core/writer.js @@ -23,9 +23,9 @@ import { parseXFAPath, } from "./core_utils.js"; import { SimpleDOMNode, SimpleXMLParser } from "./xml_parser.js"; +import { Stream, StringStream } from "./stream.js"; import { BaseStream } from "./base_stream.js"; import { calculateMD5 } from "./crypto.js"; -import { Stream } from "./stream.js"; async function writeObject(ref, obj, buffer, { encrypt = null }) { const transform = encrypt?.createCipherTransform(ref.num, ref.gen); @@ -192,10 +192,10 @@ function computeMD5(filesize, xrefInfo) { return bytesToString(calculateMD5(array)); } -function writeXFADataForAcroform(str, newRefs) { +function writeXFADataForAcroform(str, changes) { const xml = new SimpleXMLParser({ hasAttributes: true }).parseFromString(str); - for (const { xfa } of newRefs) { + for (const { xfa } of changes) { if (!xfa) { continue; } @@ -230,7 +230,7 @@ async function updateAcroform({ hasXfaDatasetsEntry, xfaDatasetsRef, needAppearances, - newRefs, + changes, }) { if (hasXfa && !hasXfaDatasetsEntry && !xfaDatasetsRef) { warn("XFA - Cannot save it"); @@ -257,33 +257,23 @@ async function updateAcroform({ dict.set("NeedAppearances", true); } - const buffer = []; - await writeObject(acroFormRef, dict, buffer, xref); - - newRefs.push({ ref: acroFormRef, data: buffer.join("") }); + changes.put(acroFormRef, { + data: dict, + }); } -function updateXFA({ xfaData, xfaDatasetsRef, newRefs, xref }) { +function updateXFA({ xfaData, xfaDatasetsRef, changes, xref }) { if (xfaData === null) { const datasets = xref.fetchIfRef(xfaDatasetsRef); - xfaData = writeXFADataForAcroform(datasets.getString(), newRefs); + xfaData = writeXFADataForAcroform(datasets.getString(), changes); } + const xfaDataStream = new StringStream(xfaData); + xfaDataStream.dict = new Dict(xref); + xfaDataStream.dict.set("Type", Name.get("EmbeddedFile")); - const encrypt = xref.encrypt; - if (encrypt) { - const transform = encrypt.createCipherTransform( - xfaDatasetsRef.num, - xfaDatasetsRef.gen - ); - xfaData = transform.encryptString(xfaData); - } - const data = - `${xfaDatasetsRef.num} ${xfaDatasetsRef.gen} obj\n` + - `<< /Type /EmbeddedFile /Length ${xfaData.length}>>\nstream\n` + - xfaData + - "\nendstream\nendobj\n"; - - newRefs.push({ ref: xfaDatasetsRef, data }); + changes.put(xfaDatasetsRef, { + data: xfaDataStream, + }); } async function getXRefTable(xrefInfo, baseOffset, newRefs, newXref, buffer) { @@ -383,12 +373,12 @@ function computeIDs(baseOffset, xrefInfo, newXref) { } } -function getTrailerDict(xrefInfo, newRefs, useXrefStream) { +function getTrailerDict(xrefInfo, changes, useXrefStream) { const newXref = new Dict(null); newXref.set("Prev", xrefInfo.startXRef); const refForXrefTable = xrefInfo.newRef; if (useXrefStream) { - newRefs.push({ ref: refForXrefTable, data: "" }); + changes.put(refForXrefTable, { data: "" }); newXref.set("Size", refForXrefTable.num + 1); newXref.set("Type", Name.get("XRef")); } else { @@ -406,10 +396,24 @@ function getTrailerDict(xrefInfo, newRefs, useXrefStream) { return newXref; } +async function writeChanges(changes, xref, buffer = []) { + const newRefs = []; + for (const [ref, { data }] of changes.items()) { + if (data === null || typeof data === "string") { + newRefs.push({ ref, data }); + continue; + } + await writeObject(ref, data, buffer, xref); + newRefs.push({ ref, data: buffer.join("") }); + buffer.length = 0; + } + return newRefs.sort((a, b) => /* compare the refs */ a.ref.num - b.ref.num); +} + async function incrementalUpdate({ originalData, xrefInfo, - newRefs, + changes, xref = null, hasXfa = false, xfaDatasetsRef = null, @@ -428,19 +432,21 @@ async function incrementalUpdate({ hasXfaDatasetsEntry, xfaDatasetsRef, needAppearances, - newRefs, + changes, }); if (hasXfa) { updateXFA({ xfaData, xfaDatasetsRef, - newRefs, + changes, xref, }); } + const newXref = getTrailerDict(xrefInfo, changes, useXrefStream); const buffer = []; + const newRefs = await writeChanges(changes, xref, buffer); let baseOffset = originalData.length; const lastByte = originalData.at(-1); if (lastByte !== /* \n */ 0x0a && lastByte !== /* \r */ 0x0d) { @@ -449,10 +455,6 @@ async function incrementalUpdate({ baseOffset += 1; } - const newXref = getTrailerDict(xrefInfo, newRefs, useXrefStream); - newRefs = newRefs.sort( - (a, b) => /* compare the refs */ a.ref.num - b.ref.num - ); for (const { data } of newRefs) { if (data !== null) { buffer.push(data); @@ -482,4 +484,4 @@ async function incrementalUpdate({ return array; } -export { incrementalUpdate, writeDict, writeObject }; +export { incrementalUpdate, writeChanges, writeDict, writeObject }; diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 9ef611407352fd..f443e9310d543b 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -47,6 +47,7 @@ import { FlateStream } from "../../src/core/flate_stream.js"; import { PartialEvaluator } from "../../src/core/evaluator.js"; import { StringStream } from "../../src/core/stream.js"; import { WorkerTask } from "../../src/core/worker.js"; +import { writeChanges } from "../../src/core/writer.js"; describe("annotation", function () { class PDFManagerMock { @@ -2120,14 +2121,12 @@ describe("annotation", function () { ); const annotationStorage = new Map(); annotationStorage.set(annotation.data.id, { value: "hello world" }); + const changes = new RefSetCache(); - const data = await annotation.save( - partialEvaluator, - task, - annotationStorage - ); + await annotation.save(partialEvaluator, task, annotationStorage, changes); + const data = await writeChanges(changes, xref); expect(data.length).toEqual(2); - const [oldData, newData] = data; + const [newData, oldData] = data; expect(oldData.ref).toEqual(Ref.get(123, 0)); expect(newData.ref).toEqual(Ref.get(2, 0)); @@ -2166,14 +2165,12 @@ describe("annotation", function () { value: "hello world", rotation: 90, }); + const changes = new RefSetCache(); - const data = await annotation.save( - partialEvaluator, - task, - annotationStorage - ); + await annotation.save(partialEvaluator, task, annotationStorage, changes); + const data = await writeChanges(changes, xref); expect(data.length).toEqual(2); - const [oldData, newData] = data; + const [newData, oldData] = data; expect(oldData.ref).toEqual(Ref.get(123, 0)); expect(newData.ref).toEqual(Ref.get(2, 0)); @@ -2210,14 +2207,12 @@ describe("annotation", function () { const annotationStorage = new Map(); const value = "a".repeat(256); annotationStorage.set(annotation.data.id, { value }); + const changes = new RefSetCache(); - const data = await annotation.save( - partialEvaluator, - task, - annotationStorage - ); + await annotation.save(partialEvaluator, task, annotationStorage, changes); + const data = await writeChanges(changes, xref); expect(data.length).toEqual(2); - const [oldData, newData] = data; + const [newData, oldData] = data; expect(oldData.ref).toEqual(Ref.get(123, 0)); expect(newData.ref).toEqual(Ref.get(2, 0)); @@ -2356,16 +2351,14 @@ describe("annotation", function () { annotationStorage.set(annotation.data.id, { value: "こんにちは世界の", }); + const changes = new RefSetCache(); - const data = await annotation.save( - partialEvaluator, - task, - annotationStorage - ); + await annotation.save(partialEvaluator, task, annotationStorage, changes); + const data = await writeChanges(changes, xref); const utf16String = "\x30\x53\x30\x93\x30\x6b\x30\x61\x30\x6f\x4e\x16\x75\x4c\x30\x6e"; expect(data.length).toEqual(2); - const [oldData, newData] = data; + const [newData, oldData] = data; expect(oldData.ref).toEqual(Ref.get(123, 0)); expect(newData.ref).toEqual(Ref.get(2, 0)); @@ -2771,12 +2764,10 @@ describe("annotation", function () { ); const annotationStorage = new Map(); annotationStorage.set(annotation.data.id, { value: true }); + const changes = new RefSetCache(); - const [oldData] = await annotation.save( - partialEvaluator, - task, - annotationStorage - ); + await annotation.save(partialEvaluator, task, annotationStorage, changes); + const [oldData] = await writeChanges(changes, xref); oldData.data = oldData.data.replace(/\(D:\d+\)/, "(date)"); expect(oldData.ref).toEqual(Ref.get(123, 0)); expect(oldData.data).toEqual( @@ -2788,12 +2779,9 @@ describe("annotation", function () { annotationStorage.set(annotation.data.id, { value: false }); - const data = await annotation.save( - partialEvaluator, - task, - annotationStorage - ); - expect(data).toEqual(null); + changes.clear(); + await annotation.save(partialEvaluator, task, annotationStorage, changes); + expect(changes.isEmpty()).toBe(true); }); it("should save rotated checkboxes", async function () { @@ -2822,12 +2810,10 @@ describe("annotation", function () { ); const annotationStorage = new Map(); annotationStorage.set(annotation.data.id, { value: true, rotation: 180 }); + const changes = new RefSetCache(); - const [oldData] = await annotation.save( - partialEvaluator, - task, - annotationStorage - ); + await annotation.save(partialEvaluator, task, annotationStorage, changes); + const [oldData] = await writeChanges(changes, xref); oldData.data = oldData.data.replace(/\(D:\d+\)/, "(date)"); expect(oldData.ref).toEqual(Ref.get(123, 0)); expect(oldData.data).toEqual( @@ -2839,12 +2825,9 @@ describe("annotation", function () { annotationStorage.set(annotation.data.id, { value: false }); - const data = await annotation.save( - partialEvaluator, - task, - annotationStorage - ); - expect(data).toEqual(null); + changes.clear(); + await annotation.save(partialEvaluator, task, annotationStorage); + expect(changes.isEmpty()).toBe(true); }); it("should handle radio buttons with a field value", async function () { @@ -3117,12 +3100,10 @@ describe("annotation", function () { ); const annotationStorage = new Map(); annotationStorage.set(annotation.data.id, { value: true }); + const changes = new RefSetCache(); - let data = await annotation.save( - partialEvaluator, - task, - annotationStorage - ); + await annotation.save(partialEvaluator, task, annotationStorage, changes); + const data = await writeChanges(changes, xref); expect(data.length).toEqual(2); const [radioData, parentData] = data; radioData.data = radioData.data.replace(/\(D:\d+\)/, "(date)"); @@ -3140,8 +3121,9 @@ describe("annotation", function () { annotationStorage.set(annotation.data.id, { value: false }); - data = await annotation.save(partialEvaluator, task, annotationStorage); - expect(data).toEqual(null); + changes.clear(); + await annotation.save(partialEvaluator, task, annotationStorage, changes); + expect(changes.isEmpty()).toBe(true); }); it("should save radio buttons without a field value", async function () { @@ -3180,12 +3162,10 @@ describe("annotation", function () { ); const annotationStorage = new Map(); annotationStorage.set(annotation.data.id, { value: true }); + const changes = new RefSetCache(); - const data = await annotation.save( - partialEvaluator, - task, - annotationStorage - ); + await annotation.save(partialEvaluator, task, annotationStorage, changes); + const data = await writeChanges(changes, xref); expect(data.length).toEqual(2); const [radioData, parentData] = data; radioData.data = radioData.data.replace(/\(D:\d+\)/, "(date)"); @@ -3216,13 +3196,10 @@ describe("annotation", function () { idFactoryMock ); const annotationStorage = new Map(); + const changes = new RefSetCache(); - const data = await annotation.save( - partialEvaluator, - task, - annotationStorage - ); - expect(data).toEqual(null); + await annotation.save(partialEvaluator, task, annotationStorage, changes); + expect(changes.isEmpty()).toBe(true); }); it("should handle push buttons", async function () { @@ -3734,14 +3711,12 @@ describe("annotation", function () { ); const annotationStorage = new Map(); annotationStorage.set(annotation.data.id, { value: "C", rotation: 270 }); + const changes = new RefSetCache(); - const data = await annotation.save( - partialEvaluator, - task, - annotationStorage - ); + await annotation.save(partialEvaluator, task, annotationStorage, changes); + const data = await writeChanges(changes, xref); expect(data.length).toEqual(2); - const [oldData, newData] = data; + const [newData, oldData] = data; expect(oldData.ref).toEqual(Ref.get(123, 0)); expect(newData.ref).toEqual(Ref.get(2, 0)); @@ -3795,14 +3770,12 @@ describe("annotation", function () { ); const annotationStorage = new Map(); annotationStorage.set(annotation.data.id, { value: "C" }); + const changes = new RefSetCache(); - const data = await annotation.save( - partialEvaluator, - task, - annotationStorage - ); + await annotation.save(partialEvaluator, task, annotationStorage, changes); + const data = await writeChanges(changes, xref); expect(data.length).toEqual(2); - const [oldData, newData] = data; + const [newData, oldData] = data; expect(oldData.ref).toEqual(Ref.get(123, 0)); expect(newData.ref).toEqual(Ref.get(2, 0)); @@ -3860,15 +3833,12 @@ describe("annotation", function () { ); const annotationStorage = new Map(); annotationStorage.set(annotation.data.id, { value: ["B", "C"] }); + const changes = new RefSetCache(); - const data = await annotation.save( - partialEvaluator, - task, - annotationStorage - ); - + await annotation.save(partialEvaluator, task, annotationStorage, changes); + const data = await writeChanges(changes, xref); expect(data.length).toEqual(2); - const [oldData, newData] = data; + const [newData, oldData] = data; expect(oldData.ref).toEqual(Ref.get(123, 0)); expect(newData.ref).toEqual(Ref.get(2, 0)); @@ -4154,9 +4124,10 @@ describe("annotation", function () { describe("FreeTextAnnotation", () => { it("should create a new FreeText annotation", async () => { - partialEvaluator.xref = new XRefMock(); + const xref = (partialEvaluator.xref = new XRefMock()); const task = new WorkerTask("test FreeText creation"); - const data = await AnnotationFactory.saveNewAnnotations( + const changes = new RefSetCache(); + await AnnotationFactory.saveNewAnnotations( partialEvaluator, task, [ @@ -4168,10 +4139,13 @@ describe("annotation", function () { color: [0, 0, 0], value: "Hello PDF.js World!", }, - ] + ], + null, + changes ); + const data = await writeChanges(changes, xref); - const base = data.annotations[0].data.replace(/\(D:\d+\)/, "(date)"); + const base = data[1].data.replace(/\(D:\d+\)/, "(date)"); expect(base).toEqual( "2 0 obj\n" + "<< /Type /Annot /Subtype /FreeText /CreationDate (date) " + @@ -4180,7 +4154,7 @@ describe("annotation", function () { "endobj\n" ); - const font = data.dependencies[0].data; + const font = data[0].data; expect(font).toEqual( "1 0 obj\n" + "<< /BaseFont /Helvetica /Type /Font /Subtype /Type1 /Encoding " + @@ -4188,7 +4162,7 @@ describe("annotation", function () { "endobj\n" ); - const appearance = data.dependencies[1].data; + const appearance = data[2].data; expect(appearance).toEqual( "3 0 obj\n" + "<< /FormType 1 /Subtype /Form /Type /XObject /BBox [12 34 56 78] " + @@ -4265,12 +4239,13 @@ describe("annotation", function () { freeTextDict.set("Foo", Name.get("Bar")); const freeTextRef = Ref.get(143, 0); - partialEvaluator.xref = new XRefMock([ + const xref = (partialEvaluator.xref = new XRefMock([ { ref: freeTextRef, data: freeTextDict }, - ]); + ])); + const changes = new RefSetCache(); const task = new WorkerTask("test FreeText update"); - const data = await AnnotationFactory.saveNewAnnotations( + await AnnotationFactory.saveNewAnnotations( partialEvaluator, task, [ @@ -4285,10 +4260,13 @@ describe("annotation", function () { ref: freeTextRef, oldAnnotation: freeTextDict, }, - ] + ], + null, + changes ); + const data = await writeChanges(changes, xref); - const base = data.annotations[0].data.replaceAll(/\(D:\d+\)/g, "(date)"); + const base = data[2].data.replaceAll(/\(D:\d+\)/g, "(date)"); expect(base).toEqual( "143 0 obj\n" + "<< /Type /Annot /Subtype /FreeText /CreationDate (date) /Foo /Bar /M (date) " + @@ -4379,9 +4357,10 @@ describe("annotation", function () { }); it("should create a new Ink annotation", async function () { - partialEvaluator.xref = new XRefMock(); + const xref = (partialEvaluator.xref = new XRefMock()); + const changes = new RefSetCache(); const task = new WorkerTask("test Ink creation"); - const data = await AnnotationFactory.saveNewAnnotations( + await AnnotationFactory.saveNewAnnotations( partialEvaluator, task, [ @@ -4408,10 +4387,13 @@ describe("annotation", function () { }, ], }, - ] + ], + null, + changes ); + const data = await writeChanges(changes, xref); - const base = data.annotations[0].data.replace(/\(D:\d+\)/, "(date)"); + const base = data[0].data.replace(/\(D:\d+\)/, "(date)"); expect(base).toEqual( "1 0 obj\n" + "<< /Type /Annot /Subtype /Ink /CreationDate (date) /Rect [12 34 56 78] " + @@ -4420,7 +4402,7 @@ describe("annotation", function () { "endobj\n" ); - const appearance = data.dependencies[0].data; + const appearance = data[1].data; expect(appearance).toEqual( "2 0 obj\n" + "<< /FormType 1 /Subtype /Form /Type /XObject /BBox [12 34 56 78] /Length 129>> stream\n" + @@ -4440,9 +4422,10 @@ describe("annotation", function () { }); it("should create a new Ink annotation with some transparency", async function () { - partialEvaluator.xref = new XRefMock(); + const xref = (partialEvaluator.xref = new XRefMock()); + const changes = new RefSetCache(); const task = new WorkerTask("test Ink creation"); - const data = await AnnotationFactory.saveNewAnnotations( + await AnnotationFactory.saveNewAnnotations( partialEvaluator, task, [ @@ -4469,10 +4452,13 @@ describe("annotation", function () { }, ], }, - ] + ], + null, + changes ); + const data = await writeChanges(changes, xref); - const base = data.annotations[0].data.replace(/\(D:\d+\)/, "(date)"); + const base = data[0].data.replace(/\(D:\d+\)/, "(date)"); expect(base).toEqual( "1 0 obj\n" + "<< /Type /Annot /Subtype /Ink /CreationDate (date) /Rect [12 34 56 78] " + @@ -4481,7 +4467,7 @@ describe("annotation", function () { "endobj\n" ); - const appearance = data.dependencies[0].data; + const appearance = data[1].data; expect(appearance).toEqual( "2 0 obj\n" + "<< /FormType 1 /Subtype /Form /Type /XObject /BBox [12 34 56 78] /Length 136 /Resources " + @@ -4627,9 +4613,10 @@ describe("annotation", function () { }); it("should create a new Highlight annotation", async function () { - partialEvaluator.xref = new XRefMock(); + const xref = (partialEvaluator.xref = new XRefMock()); + const changes = new RefSetCache(); const task = new WorkerTask("test Highlight creation"); - const data = await AnnotationFactory.saveNewAnnotations( + await AnnotationFactory.saveNewAnnotations( partialEvaluator, task, [ @@ -4645,10 +4632,13 @@ describe("annotation", function () { [12, 13, 14, 15], ], }, - ] + ], + null, + changes ); + const data = await writeChanges(changes, xref); - const base = data.annotations[0].data.replace(/\(D:\d+\)/, "(date)"); + const base = data[0].data.replace(/\(D:\d+\)/, "(date)"); expect(base).toEqual( "1 0 obj\n" + "<< /Type /Annot /Subtype /Highlight /CreationDate (date) /Rect [12 34 56 78] " + @@ -4657,7 +4647,7 @@ describe("annotation", function () { "endobj\n" ); - const appearance = data.dependencies[0].data; + const appearance = data[1].data; expect(appearance).toEqual( "2 0 obj\n" + "<< /FormType 1 /Subtype /Form /Type /XObject /BBox [12 34 56 78] " + @@ -4717,9 +4707,10 @@ describe("annotation", function () { }); it("should create a new free Highlight annotation", async function () { - partialEvaluator.xref = new XRefMock(); + const xref = (partialEvaluator.xref = new XRefMock()); + const changes = new RefSetCache(); const task = new WorkerTask("test free Highlight creation"); - const data = await AnnotationFactory.saveNewAnnotations( + await AnnotationFactory.saveNewAnnotations( partialEvaluator, task, [ @@ -4749,10 +4740,13 @@ describe("annotation", function () { points: [Float32Array.from([16, 17, 18, 19])], }, }, - ] + ], + null, + changes ); + const data = await writeChanges(changes, xref); - const base = data.annotations[0].data.replace(/\(D:\d+\)/, "(date)"); + const base = data[0].data.replace(/\(D:\d+\)/, "(date)"); expect(base).toEqual( "1 0 obj\n" + "<< /Type /Annot /Subtype /Ink /CreationDate (date) /Rect [12 34 56 78] " + @@ -4761,7 +4755,7 @@ describe("annotation", function () { "endobj\n" ); - const appearance = data.dependencies[0].data; + const appearance = data[1].data; expect(appearance).toEqual( "2 0 obj\n" + "<< /FormType 1 /Subtype /Form /Type /XObject /BBox [12 34 56 78] " + diff --git a/test/unit/writer_spec.js b/test/unit/writer_spec.js index 4298e0ba8a87bb..5ebb37a9933f34 100644 --- a/test/unit/writer_spec.js +++ b/test/unit/writer_spec.js @@ -13,7 +13,7 @@ * limitations under the License. */ -import { Dict, Name, Ref } from "../../src/core/primitives.js"; +import { Dict, Name, Ref, RefSetCache } from "../../src/core/primitives.js"; import { incrementalUpdate, writeDict } from "../../src/core/writer.js"; import { bytesToString } from "../../src/shared/util.js"; import { StringStream } from "../../src/core/stream.js"; @@ -22,10 +22,9 @@ describe("Writer", function () { describe("Incremental update", function () { it("should update a file with new objects", async function () { const originalData = new Uint8Array(); - const newRefs = [ - { ref: Ref.get(123, 0x2d), data: "abc\n" }, - { ref: Ref.get(456, 0x4e), data: "defg\n" }, - ]; + const changes = new RefSetCache(); + changes.put(Ref.get(123, 0x2d), { data: "abc\n" }); + changes.put(Ref.get(456, 0x4e), { data: "defg\n" }); const xrefInfo = { newRef: Ref.get(789, 0), startXRef: 314, @@ -40,7 +39,8 @@ describe("Writer", function () { let data = await incrementalUpdate({ originalData, xrefInfo, - newRefs, + changes, + xref: {}, useXrefStream: true, }); data = bytesToString(data); @@ -65,7 +65,8 @@ describe("Writer", function () { data = await incrementalUpdate({ originalData, xrefInfo, - newRefs, + changes, + xref: {}, useXrefStream: false, }); data = bytesToString(data); @@ -91,7 +92,8 @@ describe("Writer", function () { it("should update a file, missing the /ID-entry, with new objects", async function () { const originalData = new Uint8Array(); - const newRefs = [{ ref: Ref.get(123, 0x2d), data: "abc\n" }]; + const changes = new RefSetCache(); + changes.put(Ref.get(123, 0x2d), { data: "abc\n" }); const xrefInfo = { newRef: Ref.get(789, 0), startXRef: 314, @@ -106,7 +108,8 @@ describe("Writer", function () { let data = await incrementalUpdate({ originalData, xrefInfo, - newRefs, + changes, + xref: {}, useXrefStream: true, }); data = bytesToString(data); @@ -185,7 +188,7 @@ describe("Writer", function () { describe("XFA", function () { it("should update AcroForm when no datasets in XFA array", async function () { const originalData = new Uint8Array(); - const newRefs = []; + const changes = new RefSetCache(); const acroForm = new Dict(null); acroForm.set("XFA", [ @@ -212,7 +215,7 @@ describe("Writer", function () { let data = await incrementalUpdate({ originalData, xrefInfo, - newRefs, + changes, hasXfa: true, xfaDatasetsRef, hasXfaDatasetsEntry: false, @@ -230,8 +233,7 @@ describe("Writer", function () { "<< /XFA [(preamble) 123 0 R (datasets) 101112 0 R (postamble) 456 0 R]>>\n" + "endobj\n" + "101112 0 obj\n" + - "<< /Type /EmbeddedFile /Length 20>>\n" + - "stream\n" + + "<< /Type /EmbeddedFile /Length 20>> stream\n" + "world\n" + "endstream\n" + "endobj\n" + @@ -250,10 +252,9 @@ describe("Writer", function () { it("should update a file with a deleted object", async function () { const originalData = new Uint8Array(); - const newRefs = [ - { ref: Ref.get(123, 0x2d), data: null }, - { ref: Ref.get(456, 0x4e), data: "abc\n" }, - ]; + const changes = new RefSetCache(); + changes.put(Ref.get(123, 0x2d), { data: null }); + changes.put(Ref.get(456, 0x4e), { data: "abc\n" }); const xrefInfo = { newRef: Ref.get(789, 0), startXRef: 314, @@ -268,7 +269,8 @@ describe("Writer", function () { let data = await incrementalUpdate({ originalData, xrefInfo, - newRefs, + changes, + xref: {}, useXrefStream: true, }); data = bytesToString(data); @@ -292,7 +294,8 @@ describe("Writer", function () { data = await incrementalUpdate({ originalData, xrefInfo, - newRefs, + changes, + xref: {}, useXrefStream: false, }); data = bytesToString(data);