Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 121 additions & 38 deletions src/core/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -2390,26 +2390,13 @@ class WidgetAnnotation extends Annotation {
dict.set("AP", AP);
AP.set("N", newRef);

const resources = this._getSaveFieldResources(xref),
appearanceDict = new Dict(xref);
appearanceDict.setIfName("Subtype", "Form");
appearanceDict.set("Resources", resources);
const bbox =
rotation % 180 === 0
? [0, 0, this.width, this.height]
: [0, 0, this.height, this.width];
appearanceDict.set("BBox", bbox);

const appearanceStream = new StringStream(appearance, appearanceDict);

const rotationMatrix = this.getRotationMatrix(annotationStorage);
if (rotationMatrix !== IDENTITY_MATRIX) {
// The matrix isn't the identity one.
appearanceDict.set("Matrix", rotationMatrix);
}

changes.put(newRef, {
data: appearanceStream,
data: this.#createAppearanceStream(
appearance,
xref,
annotationStorage,
rotation
),
xfa: null,
needAppearances: false,
});
Expand All @@ -2418,6 +2405,57 @@ class WidgetAnnotation extends Annotation {
dict.set("M", `D:${getModificationDate()}`);
}

#createAppearanceStream(appearance, xref, annotationStorage, rotation) {
const resources = this._getSaveFieldResources(xref),
appearanceDict = new Dict(xref);
appearanceDict.setIfName("Subtype", "Form");
appearanceDict.set("Resources", resources);
const bbox =
rotation % 180 === 0
? [0, 0, this.width, this.height]
: [0, 0, this.height, this.width];
appearanceDict.set("BBox", bbox);

const appearanceStream = new StringStream(appearance, appearanceDict);

const rotationMatrix = this.getRotationMatrix(annotationStorage);
if (rotationMatrix !== IDENTITY_MATRIX) {
appearanceDict.set("Matrix", rotationMatrix);
}
return appearanceStream;
}

/**
* Generate a field appearance for flattening.
* @returns {Promise<BaseStream|null|false>} The generated appearance, `null`
* to use the AP dictionary, or `false` if the font can't encode the value.
*/
async getFlattenedAppearance(evaluator, task, annotationStorage, intent) {
if (!this._hasText) {
return null;
}
const appearance = await this._getAppearance(
evaluator,
task,
intent | RenderingIntentFlag.SAVE,
annotationStorage
);
if (appearance === null) {
return null;
}
if (typeof appearance !== "string") {
return false;
}
const rotation =
annotationStorage?.get(this.data.id)?.rotation ?? this.rotation;
return this.#createAppearanceStream(
appearance,
evaluator.xref,
annotationStorage,
rotation
);
}

async _getAppearance(evaluator, task, intent, annotationStorage) {
if (this.data.password) {
return null;
Expand Down Expand Up @@ -3274,6 +3312,9 @@ class TextWidgetAnnotation extends WidgetAnnotation {
}

class ButtonWidgetAnnotation extends WidgetAnnotation {
// Appearances using the in-memory fallback font.
#generatedAppearances = new Set();

constructor(params) {
super(params);

Expand Down Expand Up @@ -3305,6 +3346,21 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
return this.data.checkBox || this.data.radioButton;
}

// Return null to use this.appearance, otherwise the checked state.
#getAppearanceValue(annotationStorage) {
const storageEntry = annotationStorage?.get(this.data.id);
const value = storageEntry ? storageEntry.value : null;
if (value === null && this.appearance) {
return null;
}
return (
value ??
(this.data.checkBox
? this.data.fieldValue === this.data.exportValue
: this.data.fieldValue === this.data.buttonValue)
);
}

#getOperatorListForAppearance(
evaluator,
task,
Expand Down Expand Up @@ -3389,36 +3445,60 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
return checked;
}

let value = null;
let rotation = null;
if (annotationStorage) {
const storageEntry = annotationStorage.get(this.data.id);
value = storageEntry ? storageEntry.value : null;
rotation = storageEntry ? storageEntry.rotation : null;
}

if (value === null && this.appearance) {
// Nothing in the annotationStorage.
// But we've a default appearance so use it.
const value = this.#getAppearanceValue(annotationStorage);
if (value === null) {
return super.getOperatorList(evaluator, task, intent, annotationStorage);
}

// There is no default appearance, `value === null || value === undefined`,
// so use the one derived from the field value.
value ??= this.data.checkBox
? this.data.fieldValue === this.data.exportValue
: this.data.fieldValue === this.data.buttonValue;

return this.#getOperatorListForAppearance(
evaluator,
task,
intent,
annotationStorage,
rotation,
annotationStorage?.get(this.data.id)?.rotation,
value ? this.checkedAppearance : this.uncheckedAppearance
);
}

async getFlattenedAppearance(evaluator, task, annotationStorage) {
if (this.data.pushButton) {
return null;
}
const value = this.#getAppearanceValue(annotationStorage);
if (value === null) {
return null;
}
const appearance = value
? this.checkedAppearance
: this.uncheckedAppearance;
const { xref } = evaluator;
if (!appearance) {
const dict = new Dict(xref);
dict.setIfName("Subtype", "Form");
dict.set("BBox", [0, 0, this.width, this.height]);
return new StringStream("", dict);
}
if (!this.#generatedAppearances.has(appearance)) {
return appearance;
}
// Serialize ZapfDingbats in place of the in-memory fallback font.
const dict = appearance.dict.clone();
const font = new Dict(xref);
font.setIfName("Type", "Font");
font.setIfName("Subtype", "Type1");
font.setIfName("BaseFont", "ZapfDingbats");
const fonts = new Dict(xref);
fonts.set("PdfJsZaDb", font);
const resources = new Dict(xref);
resources.set("Font", fonts);
dict.set("Resources", resources);

appearance.reset();
const content = appearance.getString();
appearance.reset();
return new StringStream(content, dict);
}

async save(evaluator, task, annotationStorage, changes) {
if (this.data.checkBox) {
this._saveCheckbox(evaluator, task, annotationStorage, changes);
Expand Down Expand Up @@ -3634,7 +3714,9 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {

appearanceStreamDict.set("Resources", resources);

return new StringStream(appearance, appearanceStreamDict);
const stream = new StringStream(appearance, appearanceStreamDict);
this.#generatedAppearances.add(stream);
return stream;
}

_getOnStateName(dict) {
Expand Down Expand Up @@ -5978,6 +6060,7 @@ export {
AnnotationBorderStyle,
AnnotationFactory,
getQuadPoints,
getTransformMatrix,
MarkupAnnotation,
PopupAnnotation,
WidgetAnnotation,
Expand Down
37 changes: 37 additions & 0 deletions src/core/core_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -739,8 +739,45 @@ function getSizeInBytes(x) {
return Math.ceil(Math.ceil(Math.log2(1 + x)) / 8);
}

/** Resolve references from `changes` before falling back to the XRef. */
class ChangesXRefWrapper {
#changes;

#xref;

constructor(changes, xref) {
this.#changes = changes;
this.#xref = xref;
}

getNewTemporaryRef() {
return this.#xref.getNewTemporaryRef();
}

fetch(ref) {
return this.#changes.has(ref)
? this.#changes.get(ref).data
: this.#xref.fetch(ref);
}

fetchIfRef(obj) {
return obj instanceof Ref ? this.fetch(obj) : obj;
}

async fetchAsync(ref) {
return this.#changes.has(ref)
? this.#changes.get(ref).data
: this.#xref.fetchAsync(ref);
}

async fetchIfRefAsync(obj) {
return obj instanceof Ref ? this.fetchAsync(obj) : obj;
}
}

export {
arrayBuffersToBytes,
ChangesXRefWrapper,
codePointIter,
collectActions,
deepCompare,
Expand Down
12 changes: 11 additions & 1 deletion src/core/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import { Linearization } from "./parser.js";
import { LocalColorSpaceCache } from "./image_utils.js";
import { ObjectLoader } from "./object_loader.js";
import { OperatorList } from "./operator_list.js";
import { PageAnnotationFlattener } from "./flattener.js";
import { PartialEvaluator } from "./evaluator.js";
import { PDFImage } from "./image.js";
import { StreamsSequenceStream } from "./decode_stream.js";
Expand Down Expand Up @@ -445,6 +446,15 @@ class Page {
return Promise.all(promises);
}

async flattenAnnotations(handler, task, options) {
return new PageAnnotationFlattener({
...options,
page: this,
getResources: streamDict =>
this.#getMergedResources(streamDict, RESOURCES_KEYS_OPERATOR_LIST),
}).flatten(handler, task);
}

async loadResources(keys) {
// TODO: add async `#getInheritableProperty` and remove this.
await (this.#resourcesPromise ??= this.pdfManager.ensure(
Expand Down Expand Up @@ -1510,7 +1520,7 @@ class PDFDocument {
}

serializeXfaData(annotationStorage) {
return this.xfaFactory
return this.xfaFactory && annotationStorage
? this.xfaFactory.serializeData(annotationStorage)
: null;
}
Expand Down
45 changes: 42 additions & 3 deletions src/core/editor/pdf_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ class PDFEditor {

#primaryDocument = null;

// Reference mappings for each copy of the primary document.
#primaryRefMappings = [];

// Deduplicates resource streams (fonts/images) shared across the merged
// documents. Maps a cheap content key to a bucket of { ref, dictStr, stream }
// candidates; the key only groups possible matches, an exact byte comparison
Expand Down Expand Up @@ -1066,6 +1069,9 @@ class PDFEditor {
}
const documentData = new DocumentData(document);
allDocumentData.push(documentData);
if (document === primaryDocument) {
this.#primaryRefMappings.push(documentData.oldRefMapping);
}
promises.push(this.#collectDocumentData(documentData));
let pageIndex = 0;
for (const i of filteredPageIndices) {
Expand Down Expand Up @@ -3063,16 +3069,49 @@ class PDFEditor {
return infoMap;
}

/** Return the sole source of retained PDF pages, or null. */
#getSingleDocumentData() {
return this.isSingleFile ? this.oldPages.find(p => !!p).documentData : null;
}

/**
* @type {string|null} Password of the sole retained PDF source.
*/
get password() {
return this.#getSingleDocumentData()?.document.pdfManager.password ?? null;
}

/**
* Remap the primary document's annotation storage to copied annotation ids.
* @param {Map<string, object> | null} annotationStorage
* @returns {Map<string, object> | null}
*/
getCopiedAnnotationStorage(annotationStorage) {
const storage = new Map();
for (const [key, value] of annotationStorage ?? []) {
const ref = Ref.fromString(key);
if (!ref) {
continue;
}
for (const oldRefMapping of this.#primaryRefMappings) {
const newRef = oldRefMapping.get(ref);
if (newRef) {
storage.set(newRef.toString(), value);
}
}
}
return storage.size > 0 ? storage : null;
}

/**
* Create the encryption dictionary if required.
* @returns {Promise<[Dict|null, CipherTransformFactory|null, Array|null]>}
*/
async #makeEncrypt() {
if (!this.isSingleFile) {
const documentData = this.#getSingleDocumentData();
if (!documentData) {
return [null, null, null];
}
const firstRealPage = this.oldPages.find(p => !!p);
const { documentData } = firstRealPage;
const {
document: {
xref: { trailer, encrypt },
Expand Down
Loading
Loading