Merge pull request #19976 from calixteman/write_popup

[Editor] Add the possibility to add a popup to an annotation when saving
This commit is contained in:
Tim van der Meij 2025-07-12 15:28:56 +02:00 committed by GitHub
commit b7a0f01f40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 181 additions and 15 deletions

View File

@ -428,7 +428,7 @@ class AnnotationFactory {
}
return {
annotations: await Promise.all(promises),
annotations: (await Promise.all(promises)).flat(),
};
}
@ -1798,7 +1798,29 @@ class MarkupAnnotation extends Annotation {
data: annotationDict,
});
return { ref: annotationRef };
const retRef = { ref: annotationRef };
if (annotation.popup) {
const popup = annotation.popup;
if (popup.deleted) {
annotationDict.delete("Popup");
annotationDict.delete("Contents");
annotationDict.delete("RC");
return retRef;
}
const popupRef = (popup.ref ||= xref.getNewTemporaryRef());
popup.parent = annotationRef;
const popupDict = PopupAnnotation.createNewDict(popup, xref);
changes.put(popupRef, { data: popupDict });
annotationDict.setIfDefined(
"Contents",
stringToAsciiOrUTF16BE(popup.contents)
);
annotationDict.set("Popup", popupRef);
return [retRef, { ref: popupRef }];
}
return retRef;
}
static async createNewPrintAnnotation(
@ -3880,6 +3902,22 @@ class PopupAnnotation extends Annotation {
this.data.open = !!dict.get("Open");
}
static createNewDict(annotation, xref, _params) {
const { oldAnnotation, rect, parent } = annotation;
const popup = oldAnnotation || new Dict(xref);
popup.setIfNotExists("Type", Name.get("Annot"));
popup.setIfNotExists("Subtype", Name.get("Popup"));
popup.setIfNotExists("Open", false);
popup.setIfArray("Rect", rect);
popup.set("Parent", parent);
return popup;
}
static async createNewAppearanceStream(annotation, xref, params) {
return null;
}
}
class FreeTextAnnotation extends MarkupAnnotation {
@ -3947,18 +3985,27 @@ class FreeTextAnnotation extends MarkupAnnotation {
}
static createNewDict(annotation, xref, { apRef, ap }) {
const { color, fontSize, oldAnnotation, rect, rotation, user, value } =
annotation;
const {
color,
date,
fontSize,
oldAnnotation,
rect,
rotation,
user,
value,
} = annotation;
const freetext = oldAnnotation || new Dict(xref);
freetext.setIfNotExists("Type", Name.get("Annot"));
freetext.setIfNotExists("Subtype", Name.get("FreeText"));
freetext.set(
oldAnnotation ? "M" : "CreationDate",
`D:${getModificationDate(date)}`
);
if (oldAnnotation) {
freetext.set("M", `D:${getModificationDate()}`);
// TODO: We should try to generate a new RC from the content we've.
// For now we can just remove it to avoid any issues.
freetext.delete("RC");
} else {
freetext.set("CreationDate", `D:${getModificationDate()}`);
}
freetext.setIfArray("Rect", rect);
const da = `/Helv ${fontSize} Tf ${getPdfColor(color, /* isFill */ true)}`;
@ -4492,6 +4539,7 @@ class InkAnnotation extends MarkupAnnotation {
const {
oldAnnotation,
color,
date,
opacity,
paths,
outlines,
@ -4503,7 +4551,10 @@ class InkAnnotation extends MarkupAnnotation {
const ink = oldAnnotation || new Dict(xref);
ink.setIfNotExists("Type", Name.get("Annot"));
ink.setIfNotExists("Subtype", Name.get("Ink"));
ink.set(oldAnnotation ? "M" : "CreationDate", `D:${getModificationDate()}`);
ink.set(
oldAnnotation ? "M" : "CreationDate",
`D:${getModificationDate(date)}`
);
ink.setIfArray("Rect", rect);
ink.setIfArray("InkList", outlines?.points || paths?.points);
ink.setIfNotExists("F", 4);
@ -4730,13 +4781,23 @@ class HighlightAnnotation extends MarkupAnnotation {
}
static createNewDict(annotation, xref, { apRef, ap }) {
const { color, oldAnnotation, opacity, rect, rotation, user, quadPoints } =
annotation;
const date = `D:${getModificationDate()}`;
const {
color,
date,
oldAnnotation,
opacity,
rect,
rotation,
user,
quadPoints,
} = annotation;
const highlight = oldAnnotation || new Dict(xref);
highlight.setIfNotExists("Type", Name.get("Annot"));
highlight.setIfNotExists("Subtype", Name.get("Highlight"));
highlight.set(oldAnnotation ? "M" : "CreationDate", date);
highlight.set(
oldAnnotation ? "M" : "CreationDate",
`D:${getModificationDate(date)}`
);
highlight.setIfArray("Rect", rect);
highlight.setIfNotExists("F", 4);
highlight.setIfNotExists("Border", [0, 0, 0]);
@ -5046,12 +5107,14 @@ class StampAnnotation extends MarkupAnnotation {
}
static createNewDict(annotation, xref, { apRef, ap }) {
const { oldAnnotation, rect, rotation, user } = annotation;
const date = `D:${getModificationDate(annotation.date)}`;
const { date, oldAnnotation, rect, rotation, user } = annotation;
const stamp = oldAnnotation || new Dict(xref);
stamp.setIfNotExists("Type", Name.get("Annot"));
stamp.setIfNotExists("Subtype", Name.get("Stamp"));
stamp.set(oldAnnotation ? "M" : "CreationDate", date);
stamp.set(
oldAnnotation ? "M" : "CreationDate",
`D:${getModificationDate(date)}`
);
stamp.setIfArray("Rect", rect);
stamp.setIfNotExists("F", 4);
stamp.setIfNotExists("Border", [0, 0, 0]);

View File

@ -310,6 +310,12 @@ class Page {
}
continue;
}
if (annotation.popup?.deleted) {
const popupRef = Ref.fromString(annotation.popupRef);
if (popupRef) {
deletedAnnotations.put(popupRef, popupRef);
}
}
existingAnnotations?.put(ref);
annotation.ref = ref;
promises.push(

View File

@ -1092,6 +1092,9 @@ function isArrayEqual(arr1, arr2) {
}
function getModificationDate(date = new Date()) {
if (!(date instanceof Date)) {
date = new Date(date);
}
const buffer = [
date.getUTCFullYear().toString(),
(date.getUTCMonth() + 1).toString().padStart(2, "0"),

View File

@ -12171,5 +12171,47 @@
"md5": "9fa985242476c642464d94893528e40f",
"rounds": 1,
"type": "eq"
},
{
"id": "highlights-popup",
"file": "pdfs/highlights.pdf",
"md5": "55c12c918f3e2253b39b42075cb38205",
"rounds": 1,
"type": "eq",
"lastPage": 1,
"save": true,
"annotations": true,
"annotationStorage": {
"pdfjs_internal_editor_0": {
"annotationType": 9,
"popup": {
"contents": "Hello PDF.js World"
},
"pageIndex": 0,
"date": "2013-11-12T14:15:16Z",
"id": "612R"
}
}
},
{
"id": "annotation-caret-ink-popup-deleted",
"file": "pdfs/annotation-caret-ink.pdf",
"md5": "6218ca235580d1975474c979e0128c2d",
"rounds": 1,
"type": "eq",
"lastPage": 1,
"save": true,
"annotations": true,
"annotationStorage": {
"pdfjs_internal_editor_0": {
"annotationType": 15,
"popup": {
"deleted": true
},
"pageIndex": 0,
"id": "25R",
"popupRef": "27R"
}
}
}
]

View File

@ -4971,6 +4971,57 @@ describe("annotation", function () {
OPS.endAnnotation,
]);
});
it("should update an existing Highlight annotation", async function () {
const highlightDict = new Dict();
highlightDict.set("Type", Name.get("Annot"));
highlightDict.set("Subtype", Name.get("Highlight"));
highlightDict.set("Rotate", 0);
highlightDict.set("CreationDate", "D:20190423");
const highlightRef = Ref.get(143, 0);
const xref = (partialEvaluator.xref = new XRefMock([
{ ref: highlightRef, data: highlightDict },
]));
const changes = new RefSetCache();
const task = new WorkerTask("test Highlight update");
await AnnotationFactory.saveNewAnnotations(
partialEvaluator,
task,
[
{
annotationType: AnnotationEditorType.HIGHLIGHT,
rotation: 90,
popup: {
contents: "Hello PDF.js World !",
},
id: "143R",
ref: highlightRef,
oldAnnotation: highlightDict,
},
],
null,
changes
);
const data = await writeChanges(changes, xref);
const popup = data[0];
expect(popup.data).toEqual(
"1 0 obj\n" +
"<< /Type /Annot /Subtype /Popup /Open false /Parent 143 0 R>>\n" +
"endobj\n"
);
const base = data[1].data.replaceAll(/\(D:\d+\)/g, "(date)");
expect(base).toEqual(
"143 0 obj\n" +
"<< /Type /Annot /Subtype /Highlight /Rotate 90 /CreationDate (date) /M (date) " +
"/F 4 /Contents (Hello PDF.js World !) /Popup 1 0 R>>\n" +
"endobj\n"
);
});
});
describe("UnderlineAnnotation", function () {

View File

@ -247,6 +247,7 @@ describe("util", function () {
it("should get a correctly formatted date", function () {
const date = new Date(Date.UTC(3141, 5, 9, 2, 6, 53));
expect(getModificationDate(date)).toEqual("31410609020653");
expect(getModificationDate(date.toString())).toEqual("31410609020653");
});
});