diff --git a/src/core/annotation.js b/src/core/annotation.js index 2665ac66afe20..232961e1d1921 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -295,6 +295,27 @@ class AnnotationFactory { dependencies, }; } + + static async printNewAnnotations(evaluator, task, annotations) { + if (!annotations) { + return null; + } + + const xref = evaluator.xref; + const promises = []; + for (const annotation of annotations) { + switch (annotation.annotationType) { + case AnnotationEditorType.FREETEXT: + break; + case AnnotationEditorType.INK: + promises.push( + InkAnnotation.createNewPrintAnnotation(annotation, xref) + ); + } + } + + return Promise.all(promises); + } } function getRgbColor(color, defaultColor = new Uint8ClampedArray(3)) { @@ -3621,15 +3642,7 @@ class InkAnnotation extends MarkupAnnotation { } } - static async createNewAnnotation( - xref, - evaluator, - task, - annotation, - results, - others - ) { - const inkRef = xref.getNewRef(); + static createInkDict(annotation, xref, { apRef, ap }) { const ink = new Dict(xref); ink.set("Type", Name.get("Annot")); ink.set("Subtype", Name.get("Ink")); @@ -3643,6 +3656,19 @@ class InkAnnotation extends MarkupAnnotation { ink.set("Border", [0, 0, 0]); ink.set("Rotate", 0); + const n = new Dict(xref); + ink.set("AP", n); + + if (apRef) { + n.set("N", apRef); + } else { + n.set("N", ap); + } + + return ink; + } + + static createNewAppearanceStream(annotation, xref) { const [x1, y1, x2, y2] = annotation.rect; const w = x2 - x1; const h = y2 - y1; @@ -3679,18 +3705,29 @@ class InkAnnotation extends MarkupAnnotation { const ap = new StringStream(appearance); ap.dict = appearanceStreamDict; - buffer.length = 0; + return ap; + } + + static async createNewAnnotation( + xref, + evaluator, + task, + annotation, + results, + others + ) { + const inkRef = xref.getNewRef(); const apRef = xref.getNewRef(); + const ink = this.createInkDict(annotation, xref, { apRef }); + const ap = this.createNewAppearanceStream(annotation, xref); + + const buffer = []; let transform = xref.encrypt ? xref.encrypt.createCipherTransform(apRef.num, apRef.gen) : null; writeObject(apRef, ap, buffer, transform); others.push({ ref: apRef, data: buffer.join("") }); - const n = new Dict(xref); - n.set("N", apRef); - ink.set("AP", n); - buffer.length = 0; transform = xref.encrypt ? xref.encrypt.createCipherTransform(inkRef.num, inkRef.gen) @@ -3699,6 +3736,16 @@ class InkAnnotation extends MarkupAnnotation { results.push({ ref: inkRef, data: buffer.join("") }); } + + static async createNewPrintAnnotation(annotation, xref) { + const ap = this.createNewAppearanceStream(annotation, xref); + const ink = this.createInkDict(annotation, xref, { ap }); + + return new InkAnnotation({ + dict: ink, + xref, + }); + } } class HighlightAnnotation extends MarkupAnnotation { diff --git a/src/core/core_utils.js b/src/core/core_utils.js index 3e258c96427d0..83dc5f11c6691 100644 --- a/src/core/core_utils.js +++ b/src/core/core_utils.js @@ -14,6 +14,7 @@ */ import { + AnnotationEditorPrefix, assert, BaseException, FontType, @@ -548,6 +549,27 @@ function numberToString(value) { return value.toFixed(2); } +function getNewAnnotationsMap(annotationStorage) { + if (!annotationStorage) { + return null; + } + const newAnnotationsByPage = new Map(); + // The concept of page in a XFA is very different, so + // editing is just not implemented. + for (const [key, value] of annotationStorage) { + if (!key.startsWith(AnnotationEditorPrefix)) { + continue; + } + let annotations = newAnnotationsByPage.get(value.pageIndex); + if (!annotations) { + annotations = []; + newAnnotationsByPage.set(value.pageIndex, annotations); + } + annotations.push(value); + } + return newAnnotationsByPage.size > 0 ? newAnnotationsByPage : null; +} + export { collectActions, DocStats, @@ -556,6 +578,7 @@ export { getArrayLookupTableFactory, getInheritableProperty, getLookupTableFactory, + getNewAnnotationsMap, isWhiteSpace, log2, MissingDataException, diff --git a/src/core/document.js b/src/core/document.js index a943e5dddb63a..069d1f3611ff4 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -33,6 +33,7 @@ import { import { collectActions, getInheritableProperty, + getNewAnnotationsMap, isWhiteSpace, MissingDataException, validateCSSFont, @@ -312,6 +313,8 @@ class Page { { ref: this.ref, data: buffer.join("") }, ...newData.annotations ); + + this.xref.resetNewRef(); return objects; } @@ -397,6 +400,21 @@ class Page { options: this.evaluatorOptions, }); + const newAnnotationsByPage = !this.xfaFactory + ? getNewAnnotationsMap(annotationStorage) + : null; + + let newAnnotationsPromise = Promise.resolve(null); + if (newAnnotationsByPage) { + const newAnnotations = newAnnotationsByPage.get(this.pageIndex); + if (newAnnotations) { + newAnnotationsPromise = AnnotationFactory.printNewAnnotations( + partialEvaluator, + task, + newAnnotations + ); + } + } const dataPromises = Promise.all([contentStreamPromise, resourcesPromise]); const pageListPromise = dataPromises.then(([contentStream]) => { const opList = new OperatorList(intent, sink); @@ -424,58 +442,63 @@ class Page { // Fetch the page's annotations and add their operator lists to the // page's operator list to render them. - return Promise.all([pageListPromise, this._parsedAnnotations]).then( - function ([pageOpList, annotations]) { + return Promise.all([ + pageListPromise, + this._parsedAnnotations, + newAnnotationsPromise, + ]).then(function ([pageOpList, annotations, newAnnotations]) { + if (newAnnotations) { + annotations = annotations.concat(newAnnotations); + } + if ( + annotations.length === 0 || + intent & RenderingIntentFlag.ANNOTATIONS_DISABLE + ) { + pageOpList.flush(true); + return { length: pageOpList.totalLength }; + } + const renderForms = !!(intent & RenderingIntentFlag.ANNOTATIONS_FORMS), + intentAny = !!(intent & RenderingIntentFlag.ANY), + intentDisplay = !!(intent & RenderingIntentFlag.DISPLAY), + intentPrint = !!(intent & RenderingIntentFlag.PRINT); + + // Collect the operator list promises for the annotations. Each promise + // is resolved with the complete operator list for a single annotation. + const opListPromises = []; + for (const annotation of annotations) { if ( - annotations.length === 0 || - intent & RenderingIntentFlag.ANNOTATIONS_DISABLE + intentAny || + (intentDisplay && annotation.mustBeViewed(annotationStorage)) || + (intentPrint && annotation.mustBePrinted(annotationStorage)) ) { - pageOpList.flush(true); - return { length: pageOpList.totalLength }; - } - const renderForms = !!(intent & RenderingIntentFlag.ANNOTATIONS_FORMS), - intentAny = !!(intent & RenderingIntentFlag.ANY), - intentDisplay = !!(intent & RenderingIntentFlag.DISPLAY), - intentPrint = !!(intent & RenderingIntentFlag.PRINT); - - // Collect the operator list promises for the annotations. Each promise - // is resolved with the complete operator list for a single annotation. - const opListPromises = []; - for (const annotation of annotations) { - if ( - intentAny || - (intentDisplay && annotation.mustBeViewed(annotationStorage)) || - (intentPrint && annotation.mustBePrinted(annotationStorage)) - ) { - opListPromises.push( - annotation - .getOperatorList( - partialEvaluator, - task, - intent, - renderForms, - annotationStorage - ) - .catch(function (reason) { - warn( - "getOperatorList - ignoring annotation data during " + - `"${task.name}" task: "${reason}".` - ); - return null; - }) - ); - } + opListPromises.push( + annotation + .getOperatorList( + partialEvaluator, + task, + intent, + renderForms, + annotationStorage + ) + .catch(function (reason) { + warn( + "getOperatorList - ignoring annotation data during " + + `"${task.name}" task: "${reason}".` + ); + return null; + }) + ); } - - return Promise.all(opListPromises).then(function (opLists) { - for (const opList of opLists) { - pageOpList.addOpList(opList); - } - pageOpList.flush(true); - return { length: pageOpList.totalLength }; - }); } - ); + + return Promise.all(opListPromises).then(function (opLists) { + for (const opList of opLists) { + pageOpList.addOpList(opList); + } + pageOpList.flush(true); + return { length: pageOpList.totalLength }; + }); + }); } extractTextContent({ diff --git a/src/core/worker.js b/src/core/worker.js index 8384c3a702e4b..0d80cbbb86c0f 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -15,7 +15,6 @@ import { AbortException, - AnnotationEditorPrefix, arrayByteLength, arraysToBytes, createPromiseCapability, @@ -33,13 +32,13 @@ import { warn, } from "../shared/util.js"; import { Dict, Ref } from "./primitives.js"; +import { getNewAnnotationsMap, XRefParseException } from "./core_utils.js"; import { LocalPdfManager, NetworkPdfManager } from "./pdf_manager.js"; import { clearGlobalCaches } from "./cleanup_helper.js"; import { incrementalUpdate } from "./writer.js"; import { isNodeJS } from "../shared/is_node.js"; import { MessageHandler } from "../shared/message_handler.js"; import { PDFWorkerStream } from "./worker_stream.js"; -import { XRefParseException } from "./core_utils.js"; class WorkerTask { constructor(name) { @@ -558,22 +557,9 @@ class WorkerMessageHandler { function ({ isPureXfa, numPages, annotationStorage, filename }) { pdfManager.requestLoadedStream(); - const newAnnotationsByPage = new Map(); - if (!isPureXfa) { - // The concept of page in a XFA is very different, so - // editing is just not implemented. - for (const [key, value] of annotationStorage) { - if (!key.startsWith(AnnotationEditorPrefix)) { - continue; - } - let annotations = newAnnotationsByPage.get(value.pageIndex); - if (!annotations) { - annotations = []; - newAnnotationsByPage.set(value.pageIndex, annotations); - } - annotations.push(value); - } - } + const newAnnotationsByPage = !isPureXfa + ? getNewAnnotationsMap(annotationStorage) + : null; const promises = [ pdfManager.onLoadedStream(), @@ -583,17 +569,19 @@ class WorkerMessageHandler { pdfManager.ensureDoc("startXRef"), ]; - for (const [pageIndex, annotations] of newAnnotationsByPage) { - promises.push( - pdfManager.getPage(pageIndex).then(page => { - const task = new WorkerTask(`Save (editor): page ${pageIndex}`); - return page - .saveNewAnnotations(handler, task, annotations) - .finally(function () { - finishWorkerTask(task); - }); - }) - ); + if (newAnnotationsByPage) { + for (const [pageIndex, annotations] of newAnnotationsByPage) { + promises.push( + pdfManager.getPage(pageIndex).then(page => { + const task = new WorkerTask(`Save (editor): page ${pageIndex}`); + return page + .saveNewAnnotations(handler, task, annotations) + .finally(function () { + finishWorkerTask(task); + }); + }) + ); + } } if (isPureXfa) { diff --git a/test/test_manifest.json b/test/test_manifest.json index e5c1208133581..5cb901d6bd4da 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -6622,5 +6622,49 @@ "rotation": 270 } } + }, + { + "id": "tracemonkey-editor", + "file": "pdfs/tracemonkey.pdf", + "md5": "9a192d8b1a7dc652a19835f6f08098bd", + "rounds": 1, + "lastPage": 1, + "type": "eq", + "print": true, + "annotationStorage": { + "pdfjs_internal_editor_0": { + "annotationType": 3, + "color": [0, 0, 0], + "fontSize": 10, + "value": "Hello World", + "pageIndex": 0, + "rect": [67.5, 543, 119, 556.5], + "orderIndex": 0 + }, + "pdfjs_internal_editor_1": { + "annotationType": 15, + "color": [255, 0, 0], + "thickness": 3, + "paths": [{ + "bezier": [ + 1.5, 25.727771084724367, 2.8040804485100495, 27.031851533234402, + 5.396811581133676, 23.25556095123241, 6, 22.727771084724367, + 10.45407020558315, 18.830459654839103, 15.981183968598401, + 16.364531104350363, 21, 13.227771084724367, 25.88795894206055, + 10.172796745936523, 37.988543516372076, 5.739227568352277, 42, + 1.7277710847243668 + ], + "points": [ + 1.5, 25.727771084724367, 5.225791198862495, 23.602568747729173, + 4.012834511116397, 24.914722452856147, 6, 22.727771084724367, 21, + 13.227771084724367, 37.71378602219673, 4.78737352236285, + 31.828688421912233, 7.836451889039392, 42, 1.7277710847243668 + ] + }], + "pageIndex": 0, + "rect": [71.5, 534.5, 115, 562], + "orderIndex": 1 + } + } } ] diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 50f2c2eaa29d4..7d6c8641adb54 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -4186,6 +4186,58 @@ describe("annotation", function () { "endobj\n" ); }); + + it("should render an added Ink annotation for printing", async function () { + partialEvaluator.xref = new XRefMock(); + const task = new WorkerTask("test Ink printing"); + const inkAnnotation = ( + await AnnotationFactory.printNewAnnotations(partialEvaluator, task, [ + { + annotationType: AnnotationEditorType.INK, + rect: [12, 34, 56, 78], + thickness: 1, + color: [0, 0, 0], + paths: [ + { + bezier: [1, 2, 3, 4, 5, 6, 7, 8], + // Useless in the printing case. + points: [1, 2, 3, 4, 5, 6, 7, 8], + }, + ], + }, + ]) + )[0]; + + const operatorList = await inkAnnotation.getOperatorList( + partialEvaluator, + task, + RenderingIntentFlag.PRINT, + false, + null + ); + + expect(operatorList.argsArray.length).toEqual(6); + expect(operatorList.fnArray).toEqual([ + OPS.beginAnnotation, + OPS.setLineWidth, + OPS.setStrokeRGBColor, + OPS.constructPath, + OPS.stroke, + OPS.endAnnotation, + ]); + + // Linewidth. + expect(operatorList.argsArray[1]).toEqual([1]); + // Color. + expect(operatorList.argsArray[2]).toEqual( + new Uint8ClampedArray([0, 0, 0]) + ); + // Path. + expect(operatorList.argsArray[3][0]).toEqual([OPS.moveTo, OPS.curveTo]); + expect(operatorList.argsArray[3][1]).toEqual([1, 2, 3, 4, 5, 6, 7, 8]); + // Min-max. + expect(operatorList.argsArray[3][2]).toEqual([1, 1, 2, 2]); + }); }); describe("HightlightAnnotation", function () {