1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-19 22:58:07 +02:00

[Editor] Add the possibility to save an updated stamp annotation (bug 1921291)

This commit is contained in:
Calixte Denizet 2024-09-26 18:52:53 +02:00
parent 0308b8075f
commit c9050be863
9 changed files with 338 additions and 83 deletions

View file

@ -381,11 +381,10 @@ class AnnotationFactory {
);
break;
case AnnotationEditorType.STAMP:
if (!isOffscreenCanvasSupported) {
break;
}
const image = await imagePromises.get(annotation.bitmapId);
if (image.imageStream) {
const image = isOffscreenCanvasSupported
? await imagePromises?.get(annotation.bitmapId)
: null;
if (image?.imageStream) {
const { imageStream, smaskStream } = image;
const buffer = [];
if (smaskStream) {
@ -488,11 +487,10 @@ class AnnotationFactory {
);
break;
case AnnotationEditorType.STAMP:
if (!options.isOffscreenCanvasSupported) {
break;
}
const image = await imagePromises.get(annotation.bitmapId);
if (image.imageStream) {
const image = options.isOffscreenCanvasSupported
? await imagePromises?.get(annotation.bitmapId)
: null;
if (image?.imageStream) {
const { imageStream, smaskStream } = image;
if (smaskStream) {
imageStream.dict.set("SMask", smaskStream);
@ -653,17 +651,6 @@ class Annotation {
const isLocked = !!(this.flags & AnnotationFlag.LOCKED);
const isContentLocked = !!(this.flags & AnnotationFlag.LOCKEDCONTENTS);
if (annotationGlobals.structTreeRoot) {
let structParent = dict.get("StructParent");
structParent =
Number.isInteger(structParent) && structParent >= 0 ? structParent : -1;
annotationGlobals.structTreeRoot.addAnnotationIdToPage(
params.pageRef,
structParent
);
}
// Expose public properties using a data object.
this.data = {
annotationFlags: this.flags,
@ -682,8 +669,20 @@ class Annotation {
noRotate: !!(this.flags & AnnotationFlag.NOROTATE),
noHTML: isLocked && isContentLocked,
isEditable: false,
structParent: -1,
};
if (annotationGlobals.structTreeRoot) {
let structParent = dict.get("StructParent");
this.data.structParent = structParent =
Number.isInteger(structParent) && structParent >= 0 ? structParent : -1;
annotationGlobals.structTreeRoot.addAnnotationIdToPage(
params.pageRef,
structParent
);
}
if (params.collectFields) {
// Fields can act as container for other fields and have
// some actions even if no Annotation inherit from them.
@ -1751,10 +1750,7 @@ class MarkupAnnotation extends Annotation {
}
static async createNewAnnotation(xref, annotation, dependencies, params) {
let oldAnnotation;
if (annotation.ref) {
oldAnnotation = (await xref.fetchIfRefAsync(annotation.ref)).clone();
} else {
if (!annotation.ref) {
annotation.ref = xref.getNewTemporaryRef();
}
@ -1767,12 +1763,11 @@ class MarkupAnnotation extends Annotation {
const apRef = xref.getNewTemporaryRef();
annotationDict = this.createNewDict(annotation, xref, {
apRef,
oldAnnotation,
});
await writeObject(apRef, ap, buffer, xref);
dependencies.push({ ref: apRef, data: buffer.join("") });
} else {
annotationDict = this.createNewDict(annotation, xref, { oldAnnotation });
annotationDict = this.createNewDict(annotation, xref, {});
}
if (Number.isInteger(annotation.parentTreeId)) {
annotationDict.set("StructParent", annotation.parentTreeId);
@ -1791,7 +1786,11 @@ class MarkupAnnotation extends Annotation {
params
) {
const ap = await this.createNewAppearanceStream(annotation, xref, params);
const annotationDict = this.createNewDict(annotation, xref, { ap });
const annotationDict = this.createNewDict(
annotation,
xref,
ap ? { ap } : {}
);
const newAnnotation = new this.prototype.constructor({
dict: annotationDict,
@ -3904,8 +3903,9 @@ class FreeTextAnnotation extends MarkupAnnotation {
return this._hasAppearance;
}
static createNewDict(annotation, xref, { apRef, ap, oldAnnotation }) {
const { color, fontSize, rect, rotation, user, value } = annotation;
static createNewDict(annotation, xref, { apRef, ap }) {
const { color, fontSize, oldAnnotation, rect, rotation, user, value } =
annotation;
const freetext = oldAnnotation || new Dict(xref);
freetext.set("Type", Name.get("Annot"));
freetext.set("Subtype", Name.get("FreeText"));
@ -4646,8 +4646,9 @@ class HighlightAnnotation extends MarkupAnnotation {
}
}
static createNewDict(annotation, xref, { apRef, ap, oldAnnotation }) {
const { color, opacity, rect, rotation, user, quadPoints } = annotation;
static createNewDict(annotation, xref, { apRef, ap }) {
const { color, oldAnnotation, opacity, rect, rotation, user, quadPoints } =
annotation;
const highlight = oldAnnotation || new Dict(xref);
highlight.set("Type", Name.get("Annot"));
highlight.set("Subtype", Name.get("Highlight"));
@ -4943,10 +4944,14 @@ class StampAnnotation extends MarkupAnnotation {
}
static createNewDict(annotation, xref, { apRef, ap }) {
const { rect, rotation, user } = annotation;
const stamp = new Dict(xref);
const { oldAnnotation, rect, rotation, user } = annotation;
const stamp = oldAnnotation || new Dict(xref);
stamp.set("Type", Name.get("Annot"));
stamp.set("Subtype", Name.get("Stamp"));
stamp.set(
oldAnnotation ? "M" : "CreationDate",
`D:${getModificationDate()}`
);
stamp.set("CreationDate", `D:${getModificationDate()}`);
stamp.set("Rect", rect);
stamp.set("F", 4);
@ -4972,6 +4977,11 @@ class StampAnnotation extends MarkupAnnotation {
}
static async createNewAppearanceStream(annotation, xref, params) {
if (annotation.oldAnnotation) {
// We'll use the AP we already have.
return null;
}
const { rotation } = annotation;
const { imageRef, width, height } = params.image;
const resources = new Dict(xref);

View file

@ -274,7 +274,8 @@ class Page {
);
}
#replaceIdByRef(annotations, deletedAnnotations, existingAnnotations) {
async #replaceIdByRef(annotations, deletedAnnotations, existingAnnotations) {
const promises = [];
for (const annotation of annotations) {
if (annotation.id) {
const ref = Ref.fromString(annotation.id);
@ -294,9 +295,22 @@ class Page {
}
existingAnnotations?.put(ref);
annotation.ref = ref;
promises.push(
this.xref.fetchAsync(ref).then(
obj => {
if (obj instanceof Dict) {
annotation.oldAnnotation = obj.clone();
}
},
() => {
warn(`Cannot fetch \`oldAnnotation\` for: ${ref}.`);
}
)
);
delete annotation.id;
}
}
await Promise.all(promises);
}
async saveNewAnnotations(handler, task, annotations, imagePromises) {
@ -319,7 +333,11 @@ class Page {
const deletedAnnotations = new RefSetCache();
const existingAnnotations = new RefSet();
this.#replaceIdByRef(annotations, deletedAnnotations, existingAnnotations);
await this.#replaceIdByRef(
annotations,
deletedAnnotations,
existingAnnotations
);
const pageDict = this.pageDict;
const annotationsArray = this.annotations.filter(
@ -489,23 +507,23 @@ class Page {
}
deletedAnnotations = new RefSet();
this.#replaceIdByRef(newAnnots, deletedAnnotations, null);
newAnnotationsPromise = annotationGlobalsPromise.then(
annotationGlobals => {
if (!annotationGlobals) {
return null;
}
return AnnotationFactory.printNewAnnotations(
annotationGlobals,
partialEvaluator,
task,
newAnnots,
imagePromises
);
newAnnotationsPromise = Promise.all([
annotationGlobalsPromise,
this.#replaceIdByRef(newAnnots, deletedAnnotations, null),
]).then(([annotationGlobals]) => {
if (!annotationGlobals) {
return null;
}
);
return AnnotationFactory.printNewAnnotations(
annotationGlobals,
partialEvaluator,
task,
newAnnots,
imagePromises
);
});
}
const pageListPromise = Promise.all([

View file

@ -74,7 +74,7 @@ class NameOrNumberTree {
return map;
}
get(key) {
getRaw(key) {
if (!this.root) {
return null;
}
@ -135,12 +135,16 @@ class NameOrNumberTree {
} else if (key > currentKey) {
l = m + 2;
} else {
return xref.fetchIfRef(entries[m + 1]);
return entries[m + 1];
}
}
}
return null;
}
get(key) {
return this.xref.fetchIfRef(this.getRaw(key));
}
}
class NameTree extends NameOrNumberTree {

View file

@ -141,10 +141,12 @@ class StructTreeRoot {
const nextKey = await this.#writeKids({
newAnnotationsByPage,
structTreeRootRef,
structTreeRoot: null,
kids,
nums,
xref,
pdfManager,
newRefs,
cache,
});
structTreeRoot.set("ParentTreeNextKey", nextKey);
@ -209,8 +211,12 @@ class StructTreeRoot {
for (const element of elements) {
if (element.accessibilityData?.type) {
// Each tag must have a structure type.
element.parentTreeId = nextKey++;
// structParent can be undefined and in this case the positivity check
// will fail (it's why the expression isn't equivalent to a `.<.`).
if (!(element.accessibilityData.structParent >= 0)) {
// Each tag must have a structure type.
element.parentTreeId = nextKey++;
}
hasNothingToUpdate = false;
}
}
@ -259,16 +265,24 @@ class StructTreeRoot {
parentTree.set("Nums", nums);
}
const newNextkey = await StructTreeRoot.#writeKids({
const newNextKey = await StructTreeRoot.#writeKids({
newAnnotationsByPage,
structTreeRootRef,
structTreeRoot: this,
kids: null,
nums,
xref,
pdfManager,
newRefs,
cache,
});
structTreeRoot.set("ParentTreeNextKey", newNextkey);
if (newNextKey === -1) {
// No new tags were added.
return;
}
structTreeRoot.set("ParentTreeNextKey", newNextKey);
if (numsRef) {
cache.put(numsRef, nums);
@ -285,17 +299,22 @@ class StructTreeRoot {
static async #writeKids({
newAnnotationsByPage,
structTreeRootRef,
structTreeRoot,
kids,
nums,
xref,
pdfManager,
newRefs,
cache,
}) {
const objr = Name.get("OBJR");
let nextKey = -Infinity;
let nextKey = -1;
let structTreePageObjs;
const buffer = [];
for (const [pageIndex, elements] of newAnnotationsByPage) {
const { ref: pageRef } = await pdfManager.getPage(pageIndex);
const page = await pdfManager.getPage(pageIndex);
const { ref: pageRef } = page;
const isPageRef = pageRef instanceof Ref;
for (const {
accessibilityData,
@ -306,31 +325,43 @@ class StructTreeRoot {
if (!accessibilityData?.type) {
continue;
}
const { type, title, lang, alt, expanded, actualText } =
accessibilityData;
// We've some accessibility data, so we need to create a new tag or
// update an existing one.
const { structParent } = accessibilityData;
if (
structTreeRoot &&
Number.isInteger(structParent) &&
structParent >= 0
) {
let objs = (structTreePageObjs ||= new Map()).get(pageIndex);
if (objs === undefined) {
// We need to collect the objects for the page.
const structTreePage = new StructTreePage(
structTreeRoot,
page.pageDict
);
objs = structTreePage.collectObjects(pageRef);
structTreePageObjs.set(pageIndex, objs);
}
const objRef = objs?.get(structParent);
if (objRef) {
// 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("") });
continue;
}
}
nextKey = Math.max(nextKey, parentTreeId);
const tagRef = xref.getNewTemporaryRef();
const tagDict = new Dict(xref);
// The structure type is required.
tagDict.set("S", Name.get(type));
if (title) {
tagDict.set("T", stringToAsciiOrUTF16BE(title));
}
if (lang) {
tagDict.set("Lang", lang);
}
if (alt) {
tagDict.set("Alt", stringToAsciiOrUTF16BE(alt));
}
if (expanded) {
tagDict.set("E", stringToAsciiOrUTF16BE(expanded));
}
if (actualText) {
tagDict.set("ActualText", stringToAsciiOrUTF16BE(actualText));
}
StructTreeRoot.#writeProperties(tagDict, accessibilityData);
await this.#updateParentTag({
structTreeParent,
@ -358,6 +389,30 @@ class StructTreeRoot {
return nextKey + 1;
}
static #writeProperties(
tagDict,
{ type, title, lang, alt, expanded, actualText }
) {
// The structure type is required.
tagDict.set("S", Name.get(type));
if (title) {
tagDict.set("T", stringToAsciiOrUTF16BE(title));
}
if (lang) {
tagDict.set("Lang", stringToAsciiOrUTF16BE(lang));
}
if (alt) {
tagDict.set("Alt", stringToAsciiOrUTF16BE(alt));
}
if (expanded) {
tagDict.set("E", stringToAsciiOrUTF16BE(expanded));
}
if (actualText) {
tagDict.set("ActualText", stringToAsciiOrUTF16BE(actualText));
}
}
static #collectParents({ elements, xref, pageDict, numberTree }) {
const idToElements = new Map();
for (const element of elements) {
@ -616,8 +671,40 @@ class StructTreePage {
this.nodes = [];
}
/**
* Collect all the objects (i.e. tag) that are part of the page and return a
* map of the structure element id to the object reference.
* @param {Ref} pageRef
* @returns {Map<number, Ref>}
*/
collectObjects(pageRef) {
if (!this.root || !this.rootDict || !(pageRef instanceof Ref)) {
return null;
}
const parentTree = this.rootDict.get("ParentTree");
if (!parentTree) {
return null;
}
const ids = this.root.structParentIds?.get(pageRef);
if (!ids) {
return null;
}
const map = new Map();
const numberTree = new NumberTree(parentTree, this.rootDict.xref);
for (const [elemId] of ids) {
const obj = numberTree.getRaw(elemId);
if (obj instanceof Ref) {
map.set(elemId, obj);
}
}
return map;
}
parse(pageRef) {
if (!this.root || !this.rootDict) {
if (!this.root || !this.rootDict || !(pageRef instanceof Ref)) {
return;
}
@ -626,8 +713,7 @@ class StructTreePage {
return;
}
const id = this.pageDict.get("StructParents");
const ids =
pageRef instanceof Ref && this.root.structParentIds?.get(pageRef);
const ids = this.root.structParentIds?.get(pageRef);
if (!Number.isInteger(id) && !ids) {
return;
}