Merge pull request #19026 from calixteman/refactor_newrefs_saving

Simplify saving added/modified annotations.
This commit is contained in:
calixteman 2024-11-12 13:52:20 +01:00 committed by GitHub
commit 13a231cd3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 301 additions and 331 deletions

View File

@ -68,7 +68,6 @@ import { FileSpec } from "./file_spec.js";
import { JpegStream } from "./jpeg_stream.js"; import { JpegStream } from "./jpeg_stream.js";
import { ObjectLoader } from "./object_loader.js"; import { ObjectLoader } from "./object_loader.js";
import { OperatorList } from "./operator_list.js"; import { OperatorList } from "./operator_list.js";
import { writeObject } from "./writer.js";
import { XFAFactory } from "./xfa/factory.js"; import { XFAFactory } from "./xfa/factory.js";
class AnnotationFactory { class AnnotationFactory {
@ -332,10 +331,15 @@ class AnnotationFactory {
return imagePromises; return imagePromises;
} }
static async saveNewAnnotations(evaluator, task, annotations, imagePromises) { static async saveNewAnnotations(
evaluator,
task,
annotations,
imagePromises,
changes
) {
const xref = evaluator.xref; const xref = evaluator.xref;
let baseFontRef; let baseFontRef;
const dependencies = [];
const promises = []; const promises = [];
const { isOffscreenCanvasSupported } = evaluator.options; const { isOffscreenCanvasSupported } = evaluator.options;
@ -351,38 +355,33 @@ class AnnotationFactory {
baseFont.set("Type", Name.get("Font")); baseFont.set("Type", Name.get("Font"));
baseFont.set("Subtype", Name.get("Type1")); baseFont.set("Subtype", Name.get("Type1"));
baseFont.set("Encoding", Name.get("WinAnsiEncoding")); baseFont.set("Encoding", Name.get("WinAnsiEncoding"));
const buffer = [];
baseFontRef = xref.getNewTemporaryRef(); baseFontRef = xref.getNewTemporaryRef();
await writeObject(baseFontRef, baseFont, buffer, xref); changes.put(baseFontRef, {
dependencies.push({ ref: baseFontRef, data: buffer.join("") }); data: baseFont,
});
} }
promises.push( promises.push(
FreeTextAnnotation.createNewAnnotation( FreeTextAnnotation.createNewAnnotation(xref, annotation, changes, {
xref, evaluator,
annotation, task,
dependencies, baseFontRef,
{ evaluator, task, baseFontRef } })
)
); );
break; break;
case AnnotationEditorType.HIGHLIGHT: case AnnotationEditorType.HIGHLIGHT:
if (annotation.quadPoints) { if (annotation.quadPoints) {
promises.push( promises.push(
HighlightAnnotation.createNewAnnotation( HighlightAnnotation.createNewAnnotation(xref, annotation, changes)
xref,
annotation,
dependencies
)
); );
} else { } else {
promises.push( promises.push(
InkAnnotation.createNewAnnotation(xref, annotation, dependencies) InkAnnotation.createNewAnnotation(xref, annotation, changes)
); );
} }
break; break;
case AnnotationEditorType.INK: case AnnotationEditorType.INK:
promises.push( promises.push(
InkAnnotation.createNewAnnotation(xref, annotation, dependencies) InkAnnotation.createNewAnnotation(xref, annotation, changes)
); );
break; break;
case AnnotationEditorType.STAMP: case AnnotationEditorType.STAMP:
@ -391,26 +390,23 @@ class AnnotationFactory {
: null; : null;
if (image?.imageStream) { if (image?.imageStream) {
const { imageStream, smaskStream } = image; const { imageStream, smaskStream } = image;
const buffer = [];
if (smaskStream) { if (smaskStream) {
const smaskRef = xref.getNewTemporaryRef(); const smaskRef = xref.getNewTemporaryRef();
await writeObject(smaskRef, smaskStream, buffer, xref); changes.put(smaskRef, {
dependencies.push({ ref: smaskRef, data: buffer.join("") }); data: smaskStream,
});
imageStream.dict.set("SMask", smaskRef); imageStream.dict.set("SMask", smaskRef);
buffer.length = 0;
} }
const imageRef = (image.imageRef = xref.getNewTemporaryRef()); const imageRef = (image.imageRef = xref.getNewTemporaryRef());
await writeObject(imageRef, imageStream, buffer, xref); changes.put(imageRef, {
dependencies.push({ ref: imageRef, data: buffer.join("") }); data: imageStream,
});
image.imageStream = image.smaskStream = null; image.imageStream = image.smaskStream = null;
} }
promises.push( promises.push(
StampAnnotation.createNewAnnotation( StampAnnotation.createNewAnnotation(xref, annotation, changes, {
xref, image,
annotation, })
dependencies,
{ image }
)
); );
break; break;
} }
@ -418,7 +414,6 @@ class AnnotationFactory {
return { return {
annotations: await Promise.all(promises), annotations: await Promise.all(promises),
dependencies,
}; };
} }
@ -1227,7 +1222,7 @@ class Annotation {
return { opList, separateForm: false, separateCanvas: isUsingOwnCanvas }; return { opList, separateForm: false, separateCanvas: isUsingOwnCanvas };
} }
async save(evaluator, task, annotationStorage) { async save(evaluator, task, annotationStorage, changes) {
return null; return null;
} }
@ -1758,14 +1753,13 @@ class MarkupAnnotation extends Annotation {
this._streams.push(this.appearance, appearanceStream); this._streams.push(this.appearance, appearanceStream);
} }
static async createNewAnnotation(xref, annotation, dependencies, params) { static async createNewAnnotation(xref, annotation, changes, params) {
if (!annotation.ref) { if (!annotation.ref) {
annotation.ref = xref.getNewTemporaryRef(); annotation.ref = xref.getNewTemporaryRef();
} }
const annotationRef = annotation.ref; const annotationRef = annotation.ref;
const ap = await this.createNewAppearanceStream(annotation, xref, params); const ap = await this.createNewAppearanceStream(annotation, xref, params);
const buffer = [];
let annotationDict; let annotationDict;
if (ap) { if (ap) {
@ -1773,8 +1767,9 @@ class MarkupAnnotation extends Annotation {
annotationDict = this.createNewDict(annotation, xref, { annotationDict = this.createNewDict(annotation, xref, {
apRef, apRef,
}); });
await writeObject(apRef, ap, buffer, xref); changes.put(apRef, {
dependencies.push({ ref: apRef, data: buffer.join("") }); data: ap,
});
} else { } else {
annotationDict = this.createNewDict(annotation, xref, {}); annotationDict = this.createNewDict(annotation, xref, {});
} }
@ -1782,10 +1777,11 @@ class MarkupAnnotation extends Annotation {
annotationDict.set("StructParent", annotation.parentTreeId); annotationDict.set("StructParent", annotation.parentTreeId);
} }
buffer.length = 0; changes.put(annotationRef, {
await writeObject(annotationRef, annotationDict, buffer, xref); data: annotationDict,
});
return { ref: annotationRef, data: buffer.join("") }; return { ref: annotationRef };
} }
static async createNewPrintAnnotation( static async createNewPrintAnnotation(
@ -2112,7 +2108,7 @@ class WidgetAnnotation extends Annotation {
amendSavedDict(annotationStorage, dict) {} amendSavedDict(annotationStorage, dict) {}
async save(evaluator, task, annotationStorage) { async save(evaluator, task, annotationStorage, changes) {
const storageEntry = annotationStorage?.get(this.data.id); const storageEntry = annotationStorage?.get(this.data.id);
const flags = this._buildFlags(storageEntry?.noView, storageEntry?.noPrint); const flags = this._buildFlags(storageEntry?.noView, storageEntry?.noPrint);
let value = storageEntry?.value, let value = storageEntry?.value,
@ -2123,7 +2119,7 @@ class WidgetAnnotation extends Annotation {
rotation === undefined && rotation === undefined &&
flags === undefined flags === undefined
) { ) {
return null; return;
} }
value ||= this.data.fieldValue; value ||= this.data.fieldValue;
} }
@ -2137,7 +2133,7 @@ class WidgetAnnotation extends Annotation {
isArrayEqual(value, this.data.fieldValue) && isArrayEqual(value, this.data.fieldValue) &&
flags === undefined flags === undefined
) { ) {
return null; return;
} }
if (rotation === undefined) { if (rotation === undefined) {
@ -2154,7 +2150,7 @@ class WidgetAnnotation extends Annotation {
); );
if (appearance === null && flags === undefined) { if (appearance === null && flags === undefined) {
// Appearance didn't change. // Appearance didn't change.
return null; return;
} }
} else { } else {
// No need to create an appearance: the pdf has the flag /NeedAppearances // 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); const originalDict = xref.fetchIfRef(this.ref);
if (!(originalDict instanceof Dict)) { if (!(originalDict instanceof Dict)) {
return null; return;
} }
const dict = new Dict(xref); const dict = new Dict(xref);
@ -2208,12 +2204,11 @@ class WidgetAnnotation extends Annotation {
dict.set("MK", maybeMK); dict.set("MK", maybeMK);
} }
const buffer = []; changes.put(this.ref, {
const changes = [ data: dict,
// data for the original object xfa,
// V field changed + reference for new AP needAppearances,
{ ref: this.ref, data: "", xfa, needAppearances }, });
];
if (appearance !== null) { if (appearance !== null) {
const newRef = xref.getNewTemporaryRef(); const newRef = xref.getNewTemporaryRef();
const AP = new Dict(xref); const AP = new Dict(xref);
@ -2238,26 +2233,14 @@ class WidgetAnnotation extends Annotation {
appearanceDict.set("Matrix", rotationMatrix); appearanceDict.set("Matrix", rotationMatrix);
} }
await writeObject(newRef, appearanceStream, buffer, xref); changes.put(newRef, {
data: appearanceStream,
changes.push( xfa: null,
// data for the new AP needAppearances: false,
{ });
ref: newRef,
data: buffer.join(""),
xfa: null,
needAppearances: false,
}
);
buffer.length = 0;
} }
dict.set("M", `D:${getModificationDate()}`); 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) { 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) { if (this.data.checkBox) {
return this._saveCheckbox(evaluator, task, annotationStorage); this._saveCheckbox(evaluator, task, annotationStorage, changes);
return;
} }
if (this.data.radioButton) { 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) { if (!annotationStorage) {
return null; return;
} }
const storageEntry = annotationStorage.get(this.data.id); const storageEntry = annotationStorage.get(this.data.id);
const flags = this._buildFlags(storageEntry?.noView, storageEntry?.noPrint); const flags = this._buildFlags(storageEntry?.noView, storageEntry?.noPrint);
@ -3102,18 +3083,18 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
if (rotation === undefined && flags === undefined) { if (rotation === undefined && flags === undefined) {
if (value === undefined) { if (value === undefined) {
return null; return;
} }
const defaultValue = this.data.fieldValue === this.data.exportValue; const defaultValue = this.data.fieldValue === this.data.exportValue;
if (defaultValue === value) { if (defaultValue === value) {
return null; return;
} }
} }
let dict = evaluator.xref.fetchIfRef(this.ref); let dict = evaluator.xref.fetchIfRef(this.ref);
if (!(dict instanceof Dict)) { if (!(dict instanceof Dict)) {
return null; return;
} }
dict = dict.clone(); dict = dict.clone();
@ -3142,15 +3123,16 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
dict.set("MK", maybeMK); dict.set("MK", maybeMK);
} }
const buffer = []; changes.put(this.ref, {
await writeObject(this.ref, dict, buffer, evaluator.xref); data: dict,
xfa,
return [{ ref: this.ref, data: buffer.join(""), xfa }]; needAppearances: false,
});
} }
async _saveRadioButton(evaluator, task, annotationStorage) { async _saveRadioButton(evaluator, task, annotationStorage, changes) {
if (!annotationStorage) { if (!annotationStorage) {
return null; return;
} }
const storageEntry = annotationStorage.get(this.data.id); const storageEntry = annotationStorage.get(this.data.id);
const flags = this._buildFlags(storageEntry?.noView, storageEntry?.noPrint); const flags = this._buildFlags(storageEntry?.noView, storageEntry?.noPrint);
@ -3159,18 +3141,18 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
if (rotation === undefined && flags === undefined) { if (rotation === undefined && flags === undefined) {
if (value === undefined) { if (value === undefined) {
return null; return;
} }
const defaultValue = this.data.fieldValue === this.data.buttonValue; const defaultValue = this.data.fieldValue === this.data.buttonValue;
if (defaultValue === value) { if (defaultValue === value) {
return null; return;
} }
} }
let dict = evaluator.xref.fetchIfRef(this.ref); let dict = evaluator.xref.fetchIfRef(this.ref);
if (!(dict instanceof Dict)) { if (!(dict instanceof Dict)) {
return null; return;
} }
dict = dict.clone(); dict = dict.clone();
@ -3188,16 +3170,16 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
}; };
const name = Name.get(value ? this.data.buttonValue : "Off"); const name = Name.get(value ? this.data.buttonValue : "Off");
const buffer = [];
let parentData = null;
if (value) { if (value) {
if (this.parent instanceof Ref) { if (this.parent instanceof Ref) {
const parent = evaluator.xref.fetch(this.parent); const parent = evaluator.xref.fetch(this.parent).clone();
parent.set("V", name); parent.set("V", name);
await writeObject(this.parent, parent, buffer, evaluator.xref); changes.put(this.parent, {
parentData = buffer.join(""); data: parent,
buffer.length = 0; xfa: null,
needAppearances: false,
});
} else if (this.parent instanceof Dict) { } else if (this.parent instanceof Dict) {
this.parent.set("V", name); this.parent.set("V", name);
} }
@ -3219,13 +3201,11 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
dict.set("MK", maybeMK); dict.set("MK", maybeMK);
} }
await writeObject(this.ref, dict, buffer, evaluator.xref); changes.put(this.ref, {
const newRefs = [{ ref: this.ref, data: buffer.join(""), xfa }]; data: dict,
if (parentData) { xfa,
newRefs.push({ ref: this.parent, data: parentData, xfa: null }); needAppearances: false,
} });
return newRefs;
} }
_getDefaultCheckedAppearance(params, type) { _getDefaultCheckedAppearance(params, type) {

View File

@ -70,7 +70,6 @@ import { OperatorList } from "./operator_list.js";
import { PartialEvaluator } from "./evaluator.js"; import { PartialEvaluator } from "./evaluator.js";
import { StreamsSequenceStream } from "./decode_stream.js"; import { StreamsSequenceStream } from "./decode_stream.js";
import { StructTreePage } from "./struct_tree.js"; import { StructTreePage } from "./struct_tree.js";
import { writeObject } from "./writer.js";
import { XFAFactory } from "./xfa/factory.js"; import { XFAFactory } from "./xfa/factory.js";
import { XRef } from "./xref.js"; import { XRef } from "./xref.js";
@ -314,7 +313,7 @@ class Page {
await Promise.all(promises); await Promise.all(promises);
} }
async saveNewAnnotations(handler, task, annotations, imagePromises) { async saveNewAnnotations(handler, task, annotations, imagePromises, changes) {
if (this.xfaFactory) { if (this.xfaFactory) {
throw new Error("XFA: Cannot save new annotations."); throw new Error("XFA: Cannot save new annotations.");
} }
@ -348,7 +347,8 @@ class Page {
partialEvaluator, partialEvaluator,
task, task,
annotations, annotations,
imagePromises imagePromises,
changes
); );
for (const { ref } of newData.annotations) { for (const { ref } of newData.annotations) {
@ -358,27 +358,20 @@ class Page {
} }
} }
const savedDict = pageDict.get("Annots"); const dict = pageDict.clone();
pageDict.set("Annots", annotationsArray); dict.set("Annots", annotationsArray);
const buffer = []; changes.put(this.ref, {
await writeObject(this.ref, pageDict, buffer, this.xref); data: dict,
if (savedDict) { });
pageDict.set("Annots", savedDict);
}
const objects = newData.dependencies;
objects.push(
{ ref: this.ref, data: buffer.join("") },
...newData.annotations
);
for (const deletedRef of deletedAnnotations) { 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({ const partialEvaluator = new PartialEvaluator({
xref: this.xref, xref: this.xref,
handler, handler,
@ -395,11 +388,11 @@ class Page {
// Fetch the page's annotations and save the content // Fetch the page's annotations and save the content
// in case of interactive form fields. // in case of interactive form fields.
return this._parsedAnnotations.then(function (annotations) { return this._parsedAnnotations.then(function (annotations) {
const newRefsPromises = []; const promises = [];
for (const annotation of annotations) { for (const annotation of annotations) {
newRefsPromises.push( promises.push(
annotation annotation
.save(partialEvaluator, task, annotationStorage) .save(partialEvaluator, task, annotationStorage, changes)
.catch(function (reason) { .catch(function (reason) {
warn( warn(
"save - ignoring annotation data during " + "save - ignoring annotation data during " +
@ -410,9 +403,7 @@ class Page {
); );
} }
return Promise.all(newRefsPromises).then(function (newRefs) { return Promise.all(promises);
return newRefs.filter(newRef => !!newRef);
});
}); });
} }

View File

@ -383,6 +383,10 @@ class RefSetCache {
this._map.clear(); this._map.clear();
} }
*values() {
yield* this._map.values();
}
*items() { *items() {
for (const [ref, value] of this._map) { for (const [ref, value] of this._map) {
yield [Ref.fromString(ref), value]; yield [Ref.fromString(ref), value];

View File

@ -17,7 +17,6 @@ import { AnnotationPrefix, stringToPDFString, warn } from "../shared/util.js";
import { Dict, isName, Name, Ref, RefSetCache } from "./primitives.js"; import { Dict, isName, Name, Ref, RefSetCache } from "./primitives.js";
import { lookupNormalRect, stringToAsciiOrUTF16BE } from "./core_utils.js"; import { lookupNormalRect, stringToAsciiOrUTF16BE } from "./core_utils.js";
import { NumberTree } from "./name_number_tree.js"; import { NumberTree } from "./name_number_tree.js";
import { writeObject } from "./writer.js";
const MAX_DEPTH = 40; const MAX_DEPTH = 40;
@ -117,7 +116,7 @@ class StructTreeRoot {
xref, xref,
catalogRef, catalogRef,
pdfManager, pdfManager,
newRefs, changes,
}) { }) {
const root = pdfManager.catalog.cloneDict(); const root = pdfManager.catalog.cloneDict();
const cache = new RefSetCache(); const cache = new RefSetCache();
@ -146,18 +145,17 @@ class StructTreeRoot {
nums, nums,
xref, xref,
pdfManager, pdfManager,
newRefs, changes,
cache, cache,
}); });
structTreeRoot.set("ParentTreeNextKey", nextKey); structTreeRoot.set("ParentTreeNextKey", nextKey);
cache.put(parentTreeRef, parentTree); cache.put(parentTreeRef, parentTree);
const buffer = [];
for (const [ref, obj] of cache.items()) { for (const [ref, obj] of cache.items()) {
buffer.length = 0; changes.put(ref, {
await writeObject(ref, obj, buffer, xref); data: obj,
newRefs.push({ ref, data: buffer.join("") }); });
} }
} }
@ -235,7 +233,7 @@ class StructTreeRoot {
return true; return true;
} }
async updateStructureTree({ newAnnotationsByPage, pdfManager, newRefs }) { async updateStructureTree({ newAnnotationsByPage, pdfManager, changes }) {
const xref = this.dict.xref; const xref = this.dict.xref;
const structTreeRoot = this.dict.clone(); const structTreeRoot = this.dict.clone();
const structTreeRootRef = this.ref; const structTreeRootRef = this.ref;
@ -273,7 +271,7 @@ class StructTreeRoot {
nums, nums,
xref, xref,
pdfManager, pdfManager,
newRefs, changes,
cache, cache,
}); });
@ -288,11 +286,10 @@ class StructTreeRoot {
cache.put(numsRef, nums); cache.put(numsRef, nums);
} }
const buffer = [];
for (const [ref, obj] of cache.items()) { for (const [ref, obj] of cache.items()) {
buffer.length = 0; changes.put(ref, {
await writeObject(ref, obj, buffer, xref); data: obj,
newRefs.push({ ref, data: buffer.join("") }); });
} }
} }
@ -304,13 +301,12 @@ class StructTreeRoot {
nums, nums,
xref, xref,
pdfManager, pdfManager,
newRefs, changes,
cache, cache,
}) { }) {
const objr = Name.get("OBJR"); const objr = Name.get("OBJR");
let nextKey = -1; let nextKey = -1;
let structTreePageObjs; let structTreePageObjs;
const buffer = [];
for (const [pageIndex, elements] of newAnnotationsByPage) { for (const [pageIndex, elements] of newAnnotationsByPage) {
const page = await pdfManager.getPage(pageIndex); const page = await pdfManager.getPage(pageIndex);
@ -350,9 +346,9 @@ class StructTreeRoot {
// We update the existing tag. // We update the existing tag.
const tagDict = xref.fetch(objRef).clone(); const tagDict = xref.fetch(objRef).clone();
StructTreeRoot.#writeProperties(tagDict, accessibilityData); StructTreeRoot.#writeProperties(tagDict, accessibilityData);
buffer.length = 0; changes.put(objRef, {
await writeObject(objRef, tagDict, buffer, xref); data: tagDict,
newRefs.push({ ref: objRef, data: buffer.join("") }); });
continue; continue;
} }
} }

View File

@ -34,7 +34,7 @@ import {
getNewAnnotationsMap, getNewAnnotationsMap,
XRefParseException, XRefParseException,
} from "./core_utils.js"; } 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 { LocalPdfManager, NetworkPdfManager } from "./pdf_manager.js";
import { AnnotationFactory } from "./annotation.js"; import { AnnotationFactory } from "./annotation.js";
import { clearGlobalCaches } from "./cleanup_helper.js"; import { clearGlobalCaches } from "./cleanup_helper.js";
@ -540,6 +540,7 @@ class WorkerMessageHandler {
pdfManager.ensureDoc("linearization"), pdfManager.ensureDoc("linearization"),
pdfManager.ensureCatalog("structTreeRoot"), pdfManager.ensureCatalog("structTreeRoot"),
]; ];
const changes = new RefSetCache();
const promises = []; const promises = [];
const newAnnotationsByPage = !isPureXfa const newAnnotationsByPage = !isPureXfa
@ -590,7 +591,13 @@ class WorkerMessageHandler {
pdfManager.getPage(pageIndex).then(page => { pdfManager.getPage(pageIndex).then(page => {
const task = new WorkerTask(`Save (editor): page ${pageIndex}`); const task = new WorkerTask(`Save (editor): page ${pageIndex}`);
return page return page
.saveNewAnnotations(handler, task, annotations, imagePromises) .saveNewAnnotations(
handler,
task,
annotations,
imagePromises,
changes
)
.finally(function () { .finally(function () {
finishWorkerTask(task); finishWorkerTask(task);
}); });
@ -600,26 +607,24 @@ class WorkerMessageHandler {
if (structTreeRoot === null) { if (structTreeRoot === null) {
// No structTreeRoot exists, so we need to create one. // No structTreeRoot exists, so we need to create one.
promises.push( promises.push(
Promise.all(newAnnotationPromises).then(async newRefs => { Promise.all(newAnnotationPromises).then(async () => {
await StructTreeRoot.createStructureTree({ await StructTreeRoot.createStructureTree({
newAnnotationsByPage, newAnnotationsByPage,
xref, xref,
catalogRef, catalogRef,
pdfManager, pdfManager,
newRefs, changes,
}); });
return newRefs;
}) })
); );
} else if (structTreeRoot) { } else if (structTreeRoot) {
promises.push( promises.push(
Promise.all(newAnnotationPromises).then(async newRefs => { Promise.all(newAnnotationPromises).then(async () => {
await structTreeRoot.updateStructureTree({ await structTreeRoot.updateStructureTree({
newAnnotationsByPage, newAnnotationsByPage,
pdfManager, pdfManager,
newRefs, changes,
}); });
return newRefs;
}) })
); );
} }
@ -633,7 +638,7 @@ class WorkerMessageHandler {
pdfManager.getPage(pageIndex).then(function (page) { pdfManager.getPage(pageIndex).then(function (page) {
const task = new WorkerTask(`Save: page ${pageIndex}`); const task = new WorkerTask(`Save: page ${pageIndex}`);
return page return page
.save(handler, task, annotationStorage) .save(handler, task, annotationStorage, changes)
.finally(function () { .finally(function () {
finishWorkerTask(task); finishWorkerTask(task);
}); });
@ -643,26 +648,21 @@ class WorkerMessageHandler {
} }
const refs = await Promise.all(promises); const refs = await Promise.all(promises);
let newRefs = [];
let xfaData = null; let xfaData = null;
if (isPureXfa) { if (isPureXfa) {
xfaData = refs[0]; xfaData = refs[0];
if (!xfaData) { if (!xfaData) {
return stream.bytes; return stream.bytes;
} }
} else { } else if (changes.size === 0) {
newRefs = refs.flat(2); // No new refs so just return the initial bytes
return stream.bytes;
if (newRefs.length === 0) {
// No new refs so just return the initial bytes
return stream.bytes;
}
} }
const needAppearances = const needAppearances =
acroFormRef && acroFormRef &&
acroForm instanceof Dict && acroForm instanceof Dict &&
newRefs.some(ref => ref.needAppearances); changes.values().some(ref => ref.needAppearances);
const xfa = (acroForm instanceof Dict && acroForm.get("XFA")) || null; const xfa = (acroForm instanceof Dict && acroForm.get("XFA")) || null;
let xfaDatasetsRef = null; let xfaDatasetsRef = null;
@ -712,7 +712,7 @@ class WorkerMessageHandler {
return incrementalUpdate({ return incrementalUpdate({
originalData: stream.bytes, originalData: stream.bytes,
xrefInfo: newXrefInfo, xrefInfo: newXrefInfo,
newRefs, changes,
xref, xref,
hasXfa: !!xfa, hasXfa: !!xfa,
xfaDatasetsRef, xfaDatasetsRef,

View File

@ -23,9 +23,9 @@ import {
parseXFAPath, parseXFAPath,
} from "./core_utils.js"; } from "./core_utils.js";
import { SimpleDOMNode, SimpleXMLParser } from "./xml_parser.js"; import { SimpleDOMNode, SimpleXMLParser } from "./xml_parser.js";
import { Stream, StringStream } from "./stream.js";
import { BaseStream } from "./base_stream.js"; import { BaseStream } from "./base_stream.js";
import { calculateMD5 } from "./crypto.js"; import { calculateMD5 } from "./crypto.js";
import { Stream } from "./stream.js";
async function writeObject(ref, obj, buffer, { encrypt = null }) { async function writeObject(ref, obj, buffer, { encrypt = null }) {
const transform = encrypt?.createCipherTransform(ref.num, ref.gen); const transform = encrypt?.createCipherTransform(ref.num, ref.gen);
@ -192,10 +192,10 @@ function computeMD5(filesize, xrefInfo) {
return bytesToString(calculateMD5(array)); return bytesToString(calculateMD5(array));
} }
function writeXFADataForAcroform(str, newRefs) { function writeXFADataForAcroform(str, changes) {
const xml = new SimpleXMLParser({ hasAttributes: true }).parseFromString(str); const xml = new SimpleXMLParser({ hasAttributes: true }).parseFromString(str);
for (const { xfa } of newRefs) { for (const { xfa } of changes) {
if (!xfa) { if (!xfa) {
continue; continue;
} }
@ -230,7 +230,7 @@ async function updateAcroform({
hasXfaDatasetsEntry, hasXfaDatasetsEntry,
xfaDatasetsRef, xfaDatasetsRef,
needAppearances, needAppearances,
newRefs, changes,
}) { }) {
if (hasXfa && !hasXfaDatasetsEntry && !xfaDatasetsRef) { if (hasXfa && !hasXfaDatasetsEntry && !xfaDatasetsRef) {
warn("XFA - Cannot save it"); warn("XFA - Cannot save it");
@ -257,33 +257,23 @@ async function updateAcroform({
dict.set("NeedAppearances", true); dict.set("NeedAppearances", true);
} }
const buffer = []; changes.put(acroFormRef, {
await writeObject(acroFormRef, dict, buffer, xref); data: dict,
});
newRefs.push({ ref: acroFormRef, data: buffer.join("") });
} }
function updateXFA({ xfaData, xfaDatasetsRef, newRefs, xref }) { function updateXFA({ xfaData, xfaDatasetsRef, changes, xref }) {
if (xfaData === null) { if (xfaData === null) {
const datasets = xref.fetchIfRef(xfaDatasetsRef); 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; changes.put(xfaDatasetsRef, {
if (encrypt) { data: xfaDataStream,
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 });
} }
async function getXRefTable(xrefInfo, baseOffset, newRefs, newXref, buffer) { 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); const newXref = new Dict(null);
newXref.set("Prev", xrefInfo.startXRef); newXref.set("Prev", xrefInfo.startXRef);
const refForXrefTable = xrefInfo.newRef; const refForXrefTable = xrefInfo.newRef;
if (useXrefStream) { if (useXrefStream) {
newRefs.push({ ref: refForXrefTable, data: "" }); changes.put(refForXrefTable, { data: "" });
newXref.set("Size", refForXrefTable.num + 1); newXref.set("Size", refForXrefTable.num + 1);
newXref.set("Type", Name.get("XRef")); newXref.set("Type", Name.get("XRef"));
} else { } else {
@ -406,10 +396,24 @@ function getTrailerDict(xrefInfo, newRefs, useXrefStream) {
return newXref; 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({ async function incrementalUpdate({
originalData, originalData,
xrefInfo, xrefInfo,
newRefs, changes,
xref = null, xref = null,
hasXfa = false, hasXfa = false,
xfaDatasetsRef = null, xfaDatasetsRef = null,
@ -428,19 +432,21 @@ async function incrementalUpdate({
hasXfaDatasetsEntry, hasXfaDatasetsEntry,
xfaDatasetsRef, xfaDatasetsRef,
needAppearances, needAppearances,
newRefs, changes,
}); });
if (hasXfa) { if (hasXfa) {
updateXFA({ updateXFA({
xfaData, xfaData,
xfaDatasetsRef, xfaDatasetsRef,
newRefs, changes,
xref, xref,
}); });
} }
const newXref = getTrailerDict(xrefInfo, changes, useXrefStream);
const buffer = []; const buffer = [];
const newRefs = await writeChanges(changes, xref, buffer);
let baseOffset = originalData.length; let baseOffset = originalData.length;
const lastByte = originalData.at(-1); const lastByte = originalData.at(-1);
if (lastByte !== /* \n */ 0x0a && lastByte !== /* \r */ 0x0d) { if (lastByte !== /* \n */ 0x0a && lastByte !== /* \r */ 0x0d) {
@ -449,10 +455,6 @@ async function incrementalUpdate({
baseOffset += 1; 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) { for (const { data } of newRefs) {
if (data !== null) { if (data !== null) {
buffer.push(data); buffer.push(data);
@ -482,4 +484,4 @@ async function incrementalUpdate({
return array; return array;
} }
export { incrementalUpdate, writeDict, writeObject }; export { incrementalUpdate, writeChanges, writeDict, writeObject };

View File

@ -47,6 +47,7 @@ import { FlateStream } from "../../src/core/flate_stream.js";
import { PartialEvaluator } from "../../src/core/evaluator.js"; import { PartialEvaluator } from "../../src/core/evaluator.js";
import { StringStream } from "../../src/core/stream.js"; import { StringStream } from "../../src/core/stream.js";
import { WorkerTask } from "../../src/core/worker.js"; import { WorkerTask } from "../../src/core/worker.js";
import { writeChanges } from "../../src/core/writer.js";
describe("annotation", function () { describe("annotation", function () {
class PDFManagerMock { class PDFManagerMock {
@ -2120,14 +2121,12 @@ describe("annotation", function () {
); );
const annotationStorage = new Map(); const annotationStorage = new Map();
annotationStorage.set(annotation.data.id, { value: "hello world" }); annotationStorage.set(annotation.data.id, { value: "hello world" });
const changes = new RefSetCache();
const data = await annotation.save( await annotation.save(partialEvaluator, task, annotationStorage, changes);
partialEvaluator, const data = await writeChanges(changes, xref);
task,
annotationStorage
);
expect(data.length).toEqual(2); expect(data.length).toEqual(2);
const [oldData, newData] = data; const [newData, oldData] = data;
expect(oldData.ref).toEqual(Ref.get(123, 0)); expect(oldData.ref).toEqual(Ref.get(123, 0));
expect(newData.ref).toEqual(Ref.get(2, 0)); expect(newData.ref).toEqual(Ref.get(2, 0));
@ -2166,14 +2165,12 @@ describe("annotation", function () {
value: "hello world", value: "hello world",
rotation: 90, rotation: 90,
}); });
const changes = new RefSetCache();
const data = await annotation.save( await annotation.save(partialEvaluator, task, annotationStorage, changes);
partialEvaluator, const data = await writeChanges(changes, xref);
task,
annotationStorage
);
expect(data.length).toEqual(2); expect(data.length).toEqual(2);
const [oldData, newData] = data; const [newData, oldData] = data;
expect(oldData.ref).toEqual(Ref.get(123, 0)); expect(oldData.ref).toEqual(Ref.get(123, 0));
expect(newData.ref).toEqual(Ref.get(2, 0)); expect(newData.ref).toEqual(Ref.get(2, 0));
@ -2210,14 +2207,12 @@ describe("annotation", function () {
const annotationStorage = new Map(); const annotationStorage = new Map();
const value = "a".repeat(256); const value = "a".repeat(256);
annotationStorage.set(annotation.data.id, { value }); annotationStorage.set(annotation.data.id, { value });
const changes = new RefSetCache();
const data = await annotation.save( await annotation.save(partialEvaluator, task, annotationStorage, changes);
partialEvaluator, const data = await writeChanges(changes, xref);
task,
annotationStorage
);
expect(data.length).toEqual(2); expect(data.length).toEqual(2);
const [oldData, newData] = data; const [newData, oldData] = data;
expect(oldData.ref).toEqual(Ref.get(123, 0)); expect(oldData.ref).toEqual(Ref.get(123, 0));
expect(newData.ref).toEqual(Ref.get(2, 0)); expect(newData.ref).toEqual(Ref.get(2, 0));
@ -2356,16 +2351,14 @@ describe("annotation", function () {
annotationStorage.set(annotation.data.id, { annotationStorage.set(annotation.data.id, {
value: "こんにちは世界の", value: "こんにちは世界の",
}); });
const changes = new RefSetCache();
const data = await annotation.save( await annotation.save(partialEvaluator, task, annotationStorage, changes);
partialEvaluator, const data = await writeChanges(changes, xref);
task,
annotationStorage
);
const utf16String = const utf16String =
"\x30\x53\x30\x93\x30\x6b\x30\x61\x30\x6f\x4e\x16\x75\x4c\x30\x6e"; "\x30\x53\x30\x93\x30\x6b\x30\x61\x30\x6f\x4e\x16\x75\x4c\x30\x6e";
expect(data.length).toEqual(2); expect(data.length).toEqual(2);
const [oldData, newData] = data; const [newData, oldData] = data;
expect(oldData.ref).toEqual(Ref.get(123, 0)); expect(oldData.ref).toEqual(Ref.get(123, 0));
expect(newData.ref).toEqual(Ref.get(2, 0)); expect(newData.ref).toEqual(Ref.get(2, 0));
@ -2771,12 +2764,10 @@ describe("annotation", function () {
); );
const annotationStorage = new Map(); const annotationStorage = new Map();
annotationStorage.set(annotation.data.id, { value: true }); annotationStorage.set(annotation.data.id, { value: true });
const changes = new RefSetCache();
const [oldData] = await annotation.save( await annotation.save(partialEvaluator, task, annotationStorage, changes);
partialEvaluator, const [oldData] = await writeChanges(changes, xref);
task,
annotationStorage
);
oldData.data = oldData.data.replace(/\(D:\d+\)/, "(date)"); oldData.data = oldData.data.replace(/\(D:\d+\)/, "(date)");
expect(oldData.ref).toEqual(Ref.get(123, 0)); expect(oldData.ref).toEqual(Ref.get(123, 0));
expect(oldData.data).toEqual( expect(oldData.data).toEqual(
@ -2788,12 +2779,9 @@ describe("annotation", function () {
annotationStorage.set(annotation.data.id, { value: false }); annotationStorage.set(annotation.data.id, { value: false });
const data = await annotation.save( changes.clear();
partialEvaluator, await annotation.save(partialEvaluator, task, annotationStorage, changes);
task, expect(changes.size).toEqual(0);
annotationStorage
);
expect(data).toEqual(null);
}); });
it("should save rotated checkboxes", async function () { it("should save rotated checkboxes", async function () {
@ -2822,12 +2810,10 @@ describe("annotation", function () {
); );
const annotationStorage = new Map(); const annotationStorage = new Map();
annotationStorage.set(annotation.data.id, { value: true, rotation: 180 }); annotationStorage.set(annotation.data.id, { value: true, rotation: 180 });
const changes = new RefSetCache();
const [oldData] = await annotation.save( await annotation.save(partialEvaluator, task, annotationStorage, changes);
partialEvaluator, const [oldData] = await writeChanges(changes, xref);
task,
annotationStorage
);
oldData.data = oldData.data.replace(/\(D:\d+\)/, "(date)"); oldData.data = oldData.data.replace(/\(D:\d+\)/, "(date)");
expect(oldData.ref).toEqual(Ref.get(123, 0)); expect(oldData.ref).toEqual(Ref.get(123, 0));
expect(oldData.data).toEqual( expect(oldData.data).toEqual(
@ -2839,12 +2825,9 @@ describe("annotation", function () {
annotationStorage.set(annotation.data.id, { value: false }); annotationStorage.set(annotation.data.id, { value: false });
const data = await annotation.save( changes.clear();
partialEvaluator, await annotation.save(partialEvaluator, task, annotationStorage);
task, expect(changes.size).toEqual(0);
annotationStorage
);
expect(data).toEqual(null);
}); });
it("should handle radio buttons with a field value", async function () { it("should handle radio buttons with a field value", async function () {
@ -3117,12 +3100,10 @@ describe("annotation", function () {
); );
const annotationStorage = new Map(); const annotationStorage = new Map();
annotationStorage.set(annotation.data.id, { value: true }); annotationStorage.set(annotation.data.id, { value: true });
const changes = new RefSetCache();
let data = await annotation.save( await annotation.save(partialEvaluator, task, annotationStorage, changes);
partialEvaluator, const data = await writeChanges(changes, xref);
task,
annotationStorage
);
expect(data.length).toEqual(2); expect(data.length).toEqual(2);
const [radioData, parentData] = data; const [radioData, parentData] = data;
radioData.data = radioData.data.replace(/\(D:\d+\)/, "(date)"); radioData.data = radioData.data.replace(/\(D:\d+\)/, "(date)");
@ -3140,8 +3121,9 @@ describe("annotation", function () {
annotationStorage.set(annotation.data.id, { value: false }); annotationStorage.set(annotation.data.id, { value: false });
data = await annotation.save(partialEvaluator, task, annotationStorage); changes.clear();
expect(data).toEqual(null); await annotation.save(partialEvaluator, task, annotationStorage, changes);
expect(changes.size).toEqual(0);
}); });
it("should save radio buttons without a field value", async function () { it("should save radio buttons without a field value", async function () {
@ -3180,12 +3162,10 @@ describe("annotation", function () {
); );
const annotationStorage = new Map(); const annotationStorage = new Map();
annotationStorage.set(annotation.data.id, { value: true }); annotationStorage.set(annotation.data.id, { value: true });
const changes = new RefSetCache();
const data = await annotation.save( await annotation.save(partialEvaluator, task, annotationStorage, changes);
partialEvaluator, const data = await writeChanges(changes, xref);
task,
annotationStorage
);
expect(data.length).toEqual(2); expect(data.length).toEqual(2);
const [radioData, parentData] = data; const [radioData, parentData] = data;
radioData.data = radioData.data.replace(/\(D:\d+\)/, "(date)"); radioData.data = radioData.data.replace(/\(D:\d+\)/, "(date)");
@ -3216,13 +3196,10 @@ describe("annotation", function () {
idFactoryMock idFactoryMock
); );
const annotationStorage = new Map(); const annotationStorage = new Map();
const changes = new RefSetCache();
const data = await annotation.save( await annotation.save(partialEvaluator, task, annotationStorage, changes);
partialEvaluator, expect(changes.size).toEqual(0);
task,
annotationStorage
);
expect(data).toEqual(null);
}); });
it("should handle push buttons", async function () { it("should handle push buttons", async function () {
@ -3734,14 +3711,12 @@ describe("annotation", function () {
); );
const annotationStorage = new Map(); const annotationStorage = new Map();
annotationStorage.set(annotation.data.id, { value: "C", rotation: 270 }); annotationStorage.set(annotation.data.id, { value: "C", rotation: 270 });
const changes = new RefSetCache();
const data = await annotation.save( await annotation.save(partialEvaluator, task, annotationStorage, changes);
partialEvaluator, const data = await writeChanges(changes, xref);
task,
annotationStorage
);
expect(data.length).toEqual(2); expect(data.length).toEqual(2);
const [oldData, newData] = data; const [newData, oldData] = data;
expect(oldData.ref).toEqual(Ref.get(123, 0)); expect(oldData.ref).toEqual(Ref.get(123, 0));
expect(newData.ref).toEqual(Ref.get(2, 0)); expect(newData.ref).toEqual(Ref.get(2, 0));
@ -3795,14 +3770,12 @@ describe("annotation", function () {
); );
const annotationStorage = new Map(); const annotationStorage = new Map();
annotationStorage.set(annotation.data.id, { value: "C" }); annotationStorage.set(annotation.data.id, { value: "C" });
const changes = new RefSetCache();
const data = await annotation.save( await annotation.save(partialEvaluator, task, annotationStorage, changes);
partialEvaluator, const data = await writeChanges(changes, xref);
task,
annotationStorage
);
expect(data.length).toEqual(2); expect(data.length).toEqual(2);
const [oldData, newData] = data; const [newData, oldData] = data;
expect(oldData.ref).toEqual(Ref.get(123, 0)); expect(oldData.ref).toEqual(Ref.get(123, 0));
expect(newData.ref).toEqual(Ref.get(2, 0)); expect(newData.ref).toEqual(Ref.get(2, 0));
@ -3860,15 +3833,12 @@ describe("annotation", function () {
); );
const annotationStorage = new Map(); const annotationStorage = new Map();
annotationStorage.set(annotation.data.id, { value: ["B", "C"] }); annotationStorage.set(annotation.data.id, { value: ["B", "C"] });
const changes = new RefSetCache();
const data = await annotation.save( await annotation.save(partialEvaluator, task, annotationStorage, changes);
partialEvaluator, const data = await writeChanges(changes, xref);
task,
annotationStorage
);
expect(data.length).toEqual(2); expect(data.length).toEqual(2);
const [oldData, newData] = data; const [newData, oldData] = data;
expect(oldData.ref).toEqual(Ref.get(123, 0)); expect(oldData.ref).toEqual(Ref.get(123, 0));
expect(newData.ref).toEqual(Ref.get(2, 0)); expect(newData.ref).toEqual(Ref.get(2, 0));
@ -4154,9 +4124,10 @@ describe("annotation", function () {
describe("FreeTextAnnotation", () => { describe("FreeTextAnnotation", () => {
it("should create a new FreeText annotation", async () => { 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 task = new WorkerTask("test FreeText creation");
const data = await AnnotationFactory.saveNewAnnotations( const changes = new RefSetCache();
await AnnotationFactory.saveNewAnnotations(
partialEvaluator, partialEvaluator,
task, task,
[ [
@ -4168,10 +4139,13 @@ describe("annotation", function () {
color: [0, 0, 0], color: [0, 0, 0],
value: "Hello PDF.js World!", 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( expect(base).toEqual(
"2 0 obj\n" + "2 0 obj\n" +
"<< /Type /Annot /Subtype /FreeText /CreationDate (date) " + "<< /Type /Annot /Subtype /FreeText /CreationDate (date) " +
@ -4180,7 +4154,7 @@ describe("annotation", function () {
"endobj\n" "endobj\n"
); );
const font = data.dependencies[0].data; const font = data[0].data;
expect(font).toEqual( expect(font).toEqual(
"1 0 obj\n" + "1 0 obj\n" +
"<< /BaseFont /Helvetica /Type /Font /Subtype /Type1 /Encoding " + "<< /BaseFont /Helvetica /Type /Font /Subtype /Type1 /Encoding " +
@ -4188,7 +4162,7 @@ describe("annotation", function () {
"endobj\n" "endobj\n"
); );
const appearance = data.dependencies[1].data; const appearance = data[2].data;
expect(appearance).toEqual( expect(appearance).toEqual(
"3 0 obj\n" + "3 0 obj\n" +
"<< /FormType 1 /Subtype /Form /Type /XObject /BBox [12 34 56 78] " + "<< /FormType 1 /Subtype /Form /Type /XObject /BBox [12 34 56 78] " +
@ -4265,12 +4239,13 @@ describe("annotation", function () {
freeTextDict.set("Foo", Name.get("Bar")); freeTextDict.set("Foo", Name.get("Bar"));
const freeTextRef = Ref.get(143, 0); const freeTextRef = Ref.get(143, 0);
partialEvaluator.xref = new XRefMock([ const xref = (partialEvaluator.xref = new XRefMock([
{ ref: freeTextRef, data: freeTextDict }, { ref: freeTextRef, data: freeTextDict },
]); ]));
const changes = new RefSetCache();
const task = new WorkerTask("test FreeText update"); const task = new WorkerTask("test FreeText update");
const data = await AnnotationFactory.saveNewAnnotations( await AnnotationFactory.saveNewAnnotations(
partialEvaluator, partialEvaluator,
task, task,
[ [
@ -4285,10 +4260,13 @@ describe("annotation", function () {
ref: freeTextRef, ref: freeTextRef,
oldAnnotation: freeTextDict, 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( expect(base).toEqual(
"143 0 obj\n" + "143 0 obj\n" +
"<< /Type /Annot /Subtype /FreeText /CreationDate (date) /Foo /Bar /M (date) " + "<< /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 () { 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 task = new WorkerTask("test Ink creation");
const data = await AnnotationFactory.saveNewAnnotations( await AnnotationFactory.saveNewAnnotations(
partialEvaluator, partialEvaluator,
task, 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( expect(base).toEqual(
"1 0 obj\n" + "1 0 obj\n" +
"<< /Type /Annot /Subtype /Ink /CreationDate (date) /Rect [12 34 56 78] " + "<< /Type /Annot /Subtype /Ink /CreationDate (date) /Rect [12 34 56 78] " +
@ -4420,7 +4402,7 @@ describe("annotation", function () {
"endobj\n" "endobj\n"
); );
const appearance = data.dependencies[0].data; const appearance = data[1].data;
expect(appearance).toEqual( expect(appearance).toEqual(
"2 0 obj\n" + "2 0 obj\n" +
"<< /FormType 1 /Subtype /Form /Type /XObject /BBox [12 34 56 78] /Length 129>> stream\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 () { 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 task = new WorkerTask("test Ink creation");
const data = await AnnotationFactory.saveNewAnnotations( await AnnotationFactory.saveNewAnnotations(
partialEvaluator, partialEvaluator,
task, 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( expect(base).toEqual(
"1 0 obj\n" + "1 0 obj\n" +
"<< /Type /Annot /Subtype /Ink /CreationDate (date) /Rect [12 34 56 78] " + "<< /Type /Annot /Subtype /Ink /CreationDate (date) /Rect [12 34 56 78] " +
@ -4481,7 +4467,7 @@ describe("annotation", function () {
"endobj\n" "endobj\n"
); );
const appearance = data.dependencies[0].data; const appearance = data[1].data;
expect(appearance).toEqual( expect(appearance).toEqual(
"2 0 obj\n" + "2 0 obj\n" +
"<< /FormType 1 /Subtype /Form /Type /XObject /BBox [12 34 56 78] /Length 136 /Resources " + "<< /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 () { 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 task = new WorkerTask("test Highlight creation");
const data = await AnnotationFactory.saveNewAnnotations( await AnnotationFactory.saveNewAnnotations(
partialEvaluator, partialEvaluator,
task, task,
[ [
@ -4645,10 +4632,13 @@ describe("annotation", function () {
[12, 13, 14, 15], [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( expect(base).toEqual(
"1 0 obj\n" + "1 0 obj\n" +
"<< /Type /Annot /Subtype /Highlight /CreationDate (date) /Rect [12 34 56 78] " + "<< /Type /Annot /Subtype /Highlight /CreationDate (date) /Rect [12 34 56 78] " +
@ -4657,7 +4647,7 @@ describe("annotation", function () {
"endobj\n" "endobj\n"
); );
const appearance = data.dependencies[0].data; const appearance = data[1].data;
expect(appearance).toEqual( expect(appearance).toEqual(
"2 0 obj\n" + "2 0 obj\n" +
"<< /FormType 1 /Subtype /Form /Type /XObject /BBox [12 34 56 78] " + "<< /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 () { 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 task = new WorkerTask("test free Highlight creation");
const data = await AnnotationFactory.saveNewAnnotations( await AnnotationFactory.saveNewAnnotations(
partialEvaluator, partialEvaluator,
task, task,
[ [
@ -4749,10 +4740,13 @@ describe("annotation", function () {
points: [Float32Array.from([16, 17, 18, 19])], 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( expect(base).toEqual(
"1 0 obj\n" + "1 0 obj\n" +
"<< /Type /Annot /Subtype /Ink /CreationDate (date) /Rect [12 34 56 78] " + "<< /Type /Annot /Subtype /Ink /CreationDate (date) /Rect [12 34 56 78] " +
@ -4761,7 +4755,7 @@ describe("annotation", function () {
"endobj\n" "endobj\n"
); );
const appearance = data.dependencies[0].data; const appearance = data[1].data;
expect(appearance).toEqual( expect(appearance).toEqual(
"2 0 obj\n" + "2 0 obj\n" +
"<< /FormType 1 /Subtype /Form /Type /XObject /BBox [12 34 56 78] " + "<< /FormType 1 /Subtype /Form /Type /XObject /BBox [12 34 56 78] " +

View File

@ -13,7 +13,7 @@
* limitations under the License. * 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 { incrementalUpdate, writeDict } from "../../src/core/writer.js";
import { bytesToString } from "../../src/shared/util.js"; import { bytesToString } from "../../src/shared/util.js";
import { StringStream } from "../../src/core/stream.js"; import { StringStream } from "../../src/core/stream.js";
@ -22,10 +22,9 @@ describe("Writer", function () {
describe("Incremental update", function () { describe("Incremental update", function () {
it("should update a file with new objects", async function () { it("should update a file with new objects", async function () {
const originalData = new Uint8Array(); const originalData = new Uint8Array();
const newRefs = [ const changes = new RefSetCache();
{ ref: Ref.get(123, 0x2d), data: "abc\n" }, changes.put(Ref.get(123, 0x2d), { data: "abc\n" });
{ ref: Ref.get(456, 0x4e), data: "defg\n" }, changes.put(Ref.get(456, 0x4e), { data: "defg\n" });
];
const xrefInfo = { const xrefInfo = {
newRef: Ref.get(789, 0), newRef: Ref.get(789, 0),
startXRef: 314, startXRef: 314,
@ -40,7 +39,8 @@ describe("Writer", function () {
let data = await incrementalUpdate({ let data = await incrementalUpdate({
originalData, originalData,
xrefInfo, xrefInfo,
newRefs, changes,
xref: {},
useXrefStream: true, useXrefStream: true,
}); });
data = bytesToString(data); data = bytesToString(data);
@ -65,7 +65,8 @@ describe("Writer", function () {
data = await incrementalUpdate({ data = await incrementalUpdate({
originalData, originalData,
xrefInfo, xrefInfo,
newRefs, changes,
xref: {},
useXrefStream: false, useXrefStream: false,
}); });
data = bytesToString(data); data = bytesToString(data);
@ -91,7 +92,8 @@ describe("Writer", function () {
it("should update a file, missing the /ID-entry, with new objects", async function () { it("should update a file, missing the /ID-entry, with new objects", async function () {
const originalData = new Uint8Array(); 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 = { const xrefInfo = {
newRef: Ref.get(789, 0), newRef: Ref.get(789, 0),
startXRef: 314, startXRef: 314,
@ -106,7 +108,8 @@ describe("Writer", function () {
let data = await incrementalUpdate({ let data = await incrementalUpdate({
originalData, originalData,
xrefInfo, xrefInfo,
newRefs, changes,
xref: {},
useXrefStream: true, useXrefStream: true,
}); });
data = bytesToString(data); data = bytesToString(data);
@ -185,7 +188,7 @@ describe("Writer", function () {
describe("XFA", function () { describe("XFA", function () {
it("should update AcroForm when no datasets in XFA array", async function () { it("should update AcroForm when no datasets in XFA array", async function () {
const originalData = new Uint8Array(); const originalData = new Uint8Array();
const newRefs = []; const changes = new RefSetCache();
const acroForm = new Dict(null); const acroForm = new Dict(null);
acroForm.set("XFA", [ acroForm.set("XFA", [
@ -212,7 +215,7 @@ describe("Writer", function () {
let data = await incrementalUpdate({ let data = await incrementalUpdate({
originalData, originalData,
xrefInfo, xrefInfo,
newRefs, changes,
hasXfa: true, hasXfa: true,
xfaDatasetsRef, xfaDatasetsRef,
hasXfaDatasetsEntry: false, hasXfaDatasetsEntry: false,
@ -230,8 +233,7 @@ describe("Writer", function () {
"<< /XFA [(preamble) 123 0 R (datasets) 101112 0 R (postamble) 456 0 R]>>\n" + "<< /XFA [(preamble) 123 0 R (datasets) 101112 0 R (postamble) 456 0 R]>>\n" +
"endobj\n" + "endobj\n" +
"101112 0 obj\n" + "101112 0 obj\n" +
"<< /Type /EmbeddedFile /Length 20>>\n" + "<< /Type /EmbeddedFile /Length 20>> stream\n" +
"stream\n" +
"<hello>world</hello>\n" + "<hello>world</hello>\n" +
"endstream\n" + "endstream\n" +
"endobj\n" + "endobj\n" +
@ -250,10 +252,9 @@ describe("Writer", function () {
it("should update a file with a deleted object", async function () { it("should update a file with a deleted object", async function () {
const originalData = new Uint8Array(); const originalData = new Uint8Array();
const newRefs = [ const changes = new RefSetCache();
{ ref: Ref.get(123, 0x2d), data: null }, changes.put(Ref.get(123, 0x2d), { data: null });
{ ref: Ref.get(456, 0x4e), data: "abc\n" }, changes.put(Ref.get(456, 0x4e), { data: "abc\n" });
];
const xrefInfo = { const xrefInfo = {
newRef: Ref.get(789, 0), newRef: Ref.get(789, 0),
startXRef: 314, startXRef: 314,
@ -268,7 +269,8 @@ describe("Writer", function () {
let data = await incrementalUpdate({ let data = await incrementalUpdate({
originalData, originalData,
xrefInfo, xrefInfo,
newRefs, changes,
xref: {},
useXrefStream: true, useXrefStream: true,
}); });
data = bytesToString(data); data = bytesToString(data);
@ -292,7 +294,8 @@ describe("Writer", function () {
data = await incrementalUpdate({ data = await incrementalUpdate({
originalData, originalData,
xrefInfo, xrefInfo,
newRefs, changes,
xref: {},
useXrefStream: false, useXrefStream: false,
}); });
data = bytesToString(data); data = bytesToString(data);