[Editor] Add support for printing newly added Ink annotations

This commit is contained in:
Calixte Denizet 2022-06-15 16:57:33 +02:00
parent 8d466f5dac
commit f27c8c4471
6 changed files with 271 additions and 94 deletions

View File

@ -295,6 +295,27 @@ class AnnotationFactory {
dependencies, 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)) { function getRgbColor(color, defaultColor = new Uint8ClampedArray(3)) {
@ -3621,15 +3642,7 @@ class InkAnnotation extends MarkupAnnotation {
} }
} }
static async createNewAnnotation( static createInkDict(annotation, xref, { apRef, ap }) {
xref,
evaluator,
task,
annotation,
results,
others
) {
const inkRef = xref.getNewRef();
const ink = new Dict(xref); const ink = new Dict(xref);
ink.set("Type", Name.get("Annot")); ink.set("Type", Name.get("Annot"));
ink.set("Subtype", Name.get("Ink")); ink.set("Subtype", Name.get("Ink"));
@ -3643,6 +3656,19 @@ class InkAnnotation extends MarkupAnnotation {
ink.set("Border", [0, 0, 0]); ink.set("Border", [0, 0, 0]);
ink.set("Rotate", 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 [x1, y1, x2, y2] = annotation.rect;
const w = x2 - x1; const w = x2 - x1;
const h = y2 - y1; const h = y2 - y1;
@ -3679,18 +3705,29 @@ class InkAnnotation extends MarkupAnnotation {
const ap = new StringStream(appearance); const ap = new StringStream(appearance);
ap.dict = appearanceStreamDict; 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 apRef = xref.getNewRef();
const ink = this.createInkDict(annotation, xref, { apRef });
const ap = this.createNewAppearanceStream(annotation, xref);
const buffer = [];
let transform = xref.encrypt let transform = xref.encrypt
? xref.encrypt.createCipherTransform(apRef.num, apRef.gen) ? xref.encrypt.createCipherTransform(apRef.num, apRef.gen)
: null; : null;
writeObject(apRef, ap, buffer, transform); writeObject(apRef, ap, buffer, transform);
others.push({ ref: apRef, data: buffer.join("") }); others.push({ ref: apRef, data: buffer.join("") });
const n = new Dict(xref);
n.set("N", apRef);
ink.set("AP", n);
buffer.length = 0; buffer.length = 0;
transform = xref.encrypt transform = xref.encrypt
? xref.encrypt.createCipherTransform(inkRef.num, inkRef.gen) ? xref.encrypt.createCipherTransform(inkRef.num, inkRef.gen)
@ -3699,6 +3736,16 @@ class InkAnnotation extends MarkupAnnotation {
results.push({ ref: inkRef, data: buffer.join("") }); 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 { class HighlightAnnotation extends MarkupAnnotation {

View File

@ -14,6 +14,7 @@
*/ */
import { import {
AnnotationEditorPrefix,
assert, assert,
BaseException, BaseException,
FontType, FontType,
@ -548,6 +549,27 @@ function numberToString(value) {
return value.toFixed(2); 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 { export {
collectActions, collectActions,
DocStats, DocStats,
@ -556,6 +578,7 @@ export {
getArrayLookupTableFactory, getArrayLookupTableFactory,
getInheritableProperty, getInheritableProperty,
getLookupTableFactory, getLookupTableFactory,
getNewAnnotationsMap,
isWhiteSpace, isWhiteSpace,
log2, log2,
MissingDataException, MissingDataException,

View File

@ -33,6 +33,7 @@ import {
import { import {
collectActions, collectActions,
getInheritableProperty, getInheritableProperty,
getNewAnnotationsMap,
isWhiteSpace, isWhiteSpace,
MissingDataException, MissingDataException,
validateCSSFont, validateCSSFont,
@ -312,6 +313,8 @@ class Page {
{ ref: this.ref, data: buffer.join("") }, { ref: this.ref, data: buffer.join("") },
...newData.annotations ...newData.annotations
); );
this.xref.resetNewRef();
return objects; return objects;
} }
@ -397,6 +400,21 @@ class Page {
options: this.evaluatorOptions, 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 dataPromises = Promise.all([contentStreamPromise, resourcesPromise]);
const pageListPromise = dataPromises.then(([contentStream]) => { const pageListPromise = dataPromises.then(([contentStream]) => {
const opList = new OperatorList(intent, sink); const opList = new OperatorList(intent, sink);
@ -424,58 +442,63 @@ class Page {
// Fetch the page's annotations and add their operator lists to the // Fetch the page's annotations and add their operator lists to the
// page's operator list to render them. // page's operator list to render them.
return Promise.all([pageListPromise, this._parsedAnnotations]).then( return Promise.all([
function ([pageOpList, annotations]) { pageListPromise,
if ( this._parsedAnnotations,
annotations.length === 0 || newAnnotationsPromise,
intent & RenderingIntentFlag.ANNOTATIONS_DISABLE ]).then(function ([pageOpList, annotations, newAnnotations]) {
) { if (newAnnotations) {
pageOpList.flush(true); annotations = annotations.concat(newAnnotations);
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;
})
);
}
}
return Promise.all(opListPromises).then(function (opLists) {
for (const opList of opLists) {
pageOpList.addOpList(opList);
}
pageOpList.flush(true);
return { length: pageOpList.totalLength };
});
} }
); 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 (
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;
})
);
}
}
return Promise.all(opListPromises).then(function (opLists) {
for (const opList of opLists) {
pageOpList.addOpList(opList);
}
pageOpList.flush(true);
return { length: pageOpList.totalLength };
});
});
} }
extractTextContent({ extractTextContent({

View File

@ -15,7 +15,6 @@
import { import {
AbortException, AbortException,
AnnotationEditorPrefix,
arrayByteLength, arrayByteLength,
arraysToBytes, arraysToBytes,
createPromiseCapability, createPromiseCapability,
@ -33,13 +32,13 @@ import {
warn, warn,
} from "../shared/util.js"; } from "../shared/util.js";
import { Dict, Ref } from "./primitives.js"; import { Dict, Ref } from "./primitives.js";
import { getNewAnnotationsMap, XRefParseException } from "./core_utils.js";
import { LocalPdfManager, NetworkPdfManager } from "./pdf_manager.js"; import { LocalPdfManager, NetworkPdfManager } from "./pdf_manager.js";
import { clearGlobalCaches } from "./cleanup_helper.js"; import { clearGlobalCaches } from "./cleanup_helper.js";
import { incrementalUpdate } from "./writer.js"; import { incrementalUpdate } from "./writer.js";
import { isNodeJS } from "../shared/is_node.js"; import { isNodeJS } from "../shared/is_node.js";
import { MessageHandler } from "../shared/message_handler.js"; import { MessageHandler } from "../shared/message_handler.js";
import { PDFWorkerStream } from "./worker_stream.js"; import { PDFWorkerStream } from "./worker_stream.js";
import { XRefParseException } from "./core_utils.js";
class WorkerTask { class WorkerTask {
constructor(name) { constructor(name) {
@ -558,22 +557,9 @@ class WorkerMessageHandler {
function ({ isPureXfa, numPages, annotationStorage, filename }) { function ({ isPureXfa, numPages, annotationStorage, filename }) {
pdfManager.requestLoadedStream(); pdfManager.requestLoadedStream();
const newAnnotationsByPage = new Map(); const newAnnotationsByPage = !isPureXfa
if (!isPureXfa) { ? getNewAnnotationsMap(annotationStorage)
// The concept of page in a XFA is very different, so : null;
// 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 promises = [ const promises = [
pdfManager.onLoadedStream(), pdfManager.onLoadedStream(),
@ -583,17 +569,19 @@ class WorkerMessageHandler {
pdfManager.ensureDoc("startXRef"), pdfManager.ensureDoc("startXRef"),
]; ];
for (const [pageIndex, annotations] of newAnnotationsByPage) { if (newAnnotationsByPage) {
promises.push( for (const [pageIndex, annotations] of newAnnotationsByPage) {
pdfManager.getPage(pageIndex).then(page => { promises.push(
const task = new WorkerTask(`Save (editor): page ${pageIndex}`); pdfManager.getPage(pageIndex).then(page => {
return page const task = new WorkerTask(`Save (editor): page ${pageIndex}`);
.saveNewAnnotations(handler, task, annotations) return page
.finally(function () { .saveNewAnnotations(handler, task, annotations)
finishWorkerTask(task); .finally(function () {
}); finishWorkerTask(task);
}) });
); })
);
}
} }
if (isPureXfa) { if (isPureXfa) {

View File

@ -6622,5 +6622,49 @@
"rotation": 270 "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
}
}
} }
] ]

View File

@ -4186,6 +4186,58 @@ describe("annotation", function () {
"endobj\n" "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 () { describe("HightlightAnnotation", function () {