Skip to content

Commit 2fbf14a

Browse files
authored
Merge pull request #14978 from calixteman/editor2
[editor] Add support for saving a newly added FreeText
2 parents c8b8db6 + 7773b3f commit 2fbf14a

12 files changed

+409
-23
lines changed

src/core/annotation.js

+185-6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import {
1717
AnnotationActionEventType,
1818
AnnotationBorderStyleType,
19+
AnnotationEditorType,
1920
AnnotationFieldFlag,
2021
AnnotationFlag,
2122
AnnotationReplyType,
@@ -24,12 +25,14 @@ import {
2425
escapeString,
2526
getModificationDate,
2627
isAscii,
28+
LINE_DESCENT_FACTOR,
2729
LINE_FACTOR,
2830
OPS,
2931
RenderingIntentFlag,
3032
shadow,
3133
stringToPDFString,
3234
stringToUTF16BEString,
35+
stringToUTF8String,
3336
unreachable,
3437
Util,
3538
warn,
@@ -45,6 +48,7 @@ import {
4548
parseDefaultAppearance,
4649
} from "./default_appearance.js";
4750
import { Dict, isName, Name, Ref, RefSet } from "./primitives.js";
51+
import { writeDict, writeObject } from "./writer.js";
4852
import { BaseStream } from "./base_stream.js";
4953
import { bidi } from "./bidi.js";
5054
import { Catalog } from "./catalog.js";
@@ -53,7 +57,6 @@ import { FileSpec } from "./file_spec.js";
5357
import { ObjectLoader } from "./object_loader.js";
5458
import { OperatorList } from "./operator_list.js";
5559
import { StringStream } from "./stream.js";
56-
import { writeDict } from "./writer.js";
5760
import { XFAFactory } from "./xfa/factory.js";
5861

5962
class AnnotationFactory {
@@ -237,6 +240,49 @@ class AnnotationFactory {
237240
return -1;
238241
}
239242
}
243+
244+
static async saveNewAnnotations(evaluator, task, annotations) {
245+
const xref = evaluator.xref;
246+
let baseFontRef;
247+
const results = [];
248+
const dependencies = [];
249+
const promises = [];
250+
for (const annotation of annotations) {
251+
switch (annotation.annotationType) {
252+
case AnnotationEditorType.FREETEXT:
253+
if (!baseFontRef) {
254+
const baseFont = new Dict(xref);
255+
baseFont.set("BaseFont", Name.get("Helvetica"));
256+
baseFont.set("Type", Name.get("Font"));
257+
baseFont.set("Subtype", Name.get("Type1"));
258+
baseFont.set("Encoding", Name.get("WinAnsiEncoding"));
259+
const buffer = [];
260+
baseFontRef = xref.getNewRef();
261+
writeObject(baseFontRef, baseFont, buffer, null);
262+
dependencies.push({ ref: baseFontRef, data: buffer.join("") });
263+
}
264+
promises.push(
265+
FreeTextAnnotation.createNewAnnotation(
266+
xref,
267+
evaluator,
268+
task,
269+
annotation,
270+
baseFontRef,
271+
results,
272+
dependencies
273+
)
274+
);
275+
break;
276+
}
277+
}
278+
279+
await Promise.all(promises);
280+
281+
return {
282+
annotations: results,
283+
dependencies,
284+
};
285+
}
240286
}
241287

242288
function getRgbColor(color, defaultColor = new Uint8ClampedArray(3)) {
@@ -1618,7 +1664,12 @@ class WidgetAnnotation extends Annotation {
16181664
);
16191665
}
16201666

1621-
const font = await this._getFontData(evaluator, task);
1667+
const font = await WidgetAnnotation._getFontData(
1668+
evaluator,
1669+
task,
1670+
this.data.defaultAppearanceData,
1671+
this._fieldResources.mergedResources
1672+
);
16221673
const [defaultAppearance, fontSize] = this._computeFontSize(
16231674
totalHeight - defaultPadding,
16241675
totalWidth - 2 * hPadding,
@@ -1701,7 +1752,7 @@ class WidgetAnnotation extends Annotation {
17011752
);
17021753
}
17031754

1704-
async _getFontData(evaluator, task) {
1755+
static async _getFontData(evaluator, task, appearanceData, resources) {
17051756
const operatorList = new OperatorList();
17061757
const initialState = {
17071758
font: null,
@@ -1710,9 +1761,9 @@ class WidgetAnnotation extends Annotation {
17101761
},
17111762
};
17121763

1713-
const { fontName, fontSize } = this.data.defaultAppearanceData;
1764+
const { fontName, fontSize } = appearanceData;
17141765
await evaluator.handleSetFont(
1715-
this._fieldResources.mergedResources,
1766+
resources,
17161767
[fontName && Name.get(fontName), fontSize],
17171768
/* fontRef = */ null,
17181769
operatorList,
@@ -2641,7 +2692,12 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation {
26412692
);
26422693
}
26432694

2644-
const font = await this._getFontData(evaluator, task);
2695+
const font = await WidgetAnnotation._getFontData(
2696+
evaluator,
2697+
task,
2698+
this.data.defaultAppearanceData,
2699+
this._fieldResources.mergedResources
2700+
);
26452701

26462702
let defaultAppearance;
26472703
let { fontSize } = this.data.defaultAppearanceData;
@@ -2872,6 +2928,129 @@ class FreeTextAnnotation extends MarkupAnnotation {
28722928

28732929
this.data.annotationType = AnnotationType.FREETEXT;
28742930
}
2931+
2932+
static async createNewAnnotation(
2933+
xref,
2934+
evaluator,
2935+
task,
2936+
annotation,
2937+
baseFontRef,
2938+
results,
2939+
dependencies
2940+
) {
2941+
const { color, fontSize, rect, user, value } = annotation;
2942+
const freetextRef = xref.getNewRef();
2943+
const freetext = new Dict(xref);
2944+
freetext.set("Type", Name.get("Annot"));
2945+
freetext.set("Subtype", Name.get("FreeText"));
2946+
freetext.set("CreationDate", `D:${getModificationDate()}`);
2947+
freetext.set("Rect", rect);
2948+
const da = `/Helv ${fontSize} Tf ${getPdfColor(color)}`;
2949+
freetext.set("DA", da);
2950+
freetext.set("Contents", value);
2951+
freetext.set("F", 4);
2952+
freetext.set("Border", [0, 0, 0]);
2953+
freetext.set("Rotate", 0);
2954+
2955+
if (user) {
2956+
freetext.set("T", stringToUTF8String(user));
2957+
}
2958+
2959+
const resources = new Dict(xref);
2960+
const font = new Dict(xref);
2961+
font.set("Helv", baseFontRef);
2962+
resources.set("Font", font);
2963+
2964+
const helv = await WidgetAnnotation._getFontData(
2965+
evaluator,
2966+
task,
2967+
{
2968+
fontName: "Helvetica",
2969+
fontSize,
2970+
},
2971+
resources
2972+
);
2973+
2974+
const [x1, y1, x2, y2] = rect;
2975+
const w = x2 - x1;
2976+
const h = y2 - y1;
2977+
2978+
const lines = value.split("\n");
2979+
const scale = fontSize / 1000;
2980+
let totalWidth = -Infinity;
2981+
const encodedLines = [];
2982+
for (let line of lines) {
2983+
line = helv.encodeString(line).join("");
2984+
encodedLines.push(line);
2985+
let lineWidth = 0;
2986+
const glyphs = helv.charsToGlyphs(line);
2987+
for (const glyph of glyphs) {
2988+
lineWidth += glyph.width * scale;
2989+
}
2990+
totalWidth = Math.max(totalWidth, lineWidth);
2991+
}
2992+
2993+
let hscale = 1;
2994+
if (totalWidth > w) {
2995+
hscale = w / totalWidth;
2996+
}
2997+
let vscale = 1;
2998+
const lineHeight = LINE_FACTOR * fontSize;
2999+
const lineDescent = LINE_DESCENT_FACTOR * fontSize;
3000+
const totalHeight = lineHeight * lines.length;
3001+
if (totalHeight > h) {
3002+
vscale = h / totalHeight;
3003+
}
3004+
const fscale = Math.min(hscale, vscale);
3005+
const newFontSize = fontSize * fscale;
3006+
const buffer = [
3007+
"q",
3008+
`0 0 ${numberToString(w)} ${numberToString(h)} re W n`,
3009+
`BT`,
3010+
`1 0 0 1 0 ${numberToString(h + lineDescent)} Tm 0 Tc ${getPdfColor(
3011+
color
3012+
)}`,
3013+
`/Helv ${numberToString(newFontSize)} Tf`,
3014+
];
3015+
3016+
const vShift = numberToString(lineHeight);
3017+
for (const line of encodedLines) {
3018+
buffer.push(`0 -${vShift} Td (${escapeString(line)}) Tj`);
3019+
}
3020+
buffer.push("ET", "Q");
3021+
const appearance = buffer.join("\n");
3022+
3023+
const appearanceStreamDict = new Dict(xref);
3024+
appearanceStreamDict.set("FormType", 1);
3025+
appearanceStreamDict.set("Subtype", Name.get("Form"));
3026+
appearanceStreamDict.set("Type", Name.get("XObject"));
3027+
appearanceStreamDict.set("BBox", [0, 0, w, h]);
3028+
appearanceStreamDict.set("Length", appearance.length);
3029+
appearanceStreamDict.set("Resources", resources);
3030+
3031+
const ap = new StringStream(appearance);
3032+
ap.dict = appearanceStreamDict;
3033+
3034+
buffer.length = 0;
3035+
const apRef = xref.getNewRef();
3036+
let transform = xref.encrypt
3037+
? xref.encrypt.createCipherTransform(apRef.num, apRef.gen)
3038+
: null;
3039+
writeObject(apRef, ap, buffer, transform);
3040+
dependencies.push({ ref: apRef, data: buffer.join("") });
3041+
3042+
const n = new Dict(xref);
3043+
n.set("N", apRef);
3044+
freetext.set("AP", n);
3045+
3046+
buffer.length = 0;
3047+
transform = xref.encrypt
3048+
? xref.encrypt.createCipherTransform(freetextRef.num, freetextRef.gen)
3049+
: null;
3050+
writeObject(freetextRef, freetext, buffer, transform);
3051+
3052+
results.push({ ref: freetextRef, data: buffer.join("") });
3053+
}
28753054
}
28763055

28773056
class LineAnnotation extends MarkupAnnotation {

src/core/document.js

+55
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import { OperatorList } from "./operator_list.js";
5555
import { PartialEvaluator } from "./evaluator.js";
5656
import { StreamsSequenceStream } from "./decode_stream.js";
5757
import { StructTreePage } from "./struct_tree.js";
58+
import { writeObject } from "./writer.js";
5859
import { XFAFactory } from "./xfa/factory.js";
5960
import { XRef } from "./xref.js";
6061

@@ -261,6 +262,60 @@ class Page {
261262
);
262263
}
263264

265+
async saveNewAnnotations(handler, task, annotations) {
266+
if (this.xfaFactory) {
267+
throw new Error("XFA: Cannot save new annotations.");
268+
}
269+
270+
const partialEvaluator = new PartialEvaluator({
271+
xref: this.xref,
272+
handler,
273+
pageIndex: this.pageIndex,
274+
idFactory: this._localIdFactory,
275+
fontCache: this.fontCache,
276+
builtInCMapCache: this.builtInCMapCache,
277+
standardFontDataCache: this.standardFontDataCache,
278+
globalImageCache: this.globalImageCache,
279+
options: this.evaluatorOptions,
280+
});
281+
282+
const pageDict = this.pageDict;
283+
const annotationsArray = this.annotations.slice();
284+
const newData = await AnnotationFactory.saveNewAnnotations(
285+
partialEvaluator,
286+
task,
287+
annotations
288+
);
289+
290+
for (const { ref } of newData.annotations) {
291+
annotationsArray.push(ref);
292+
}
293+
294+
const savedDict = pageDict.get("Annots");
295+
pageDict.set("Annots", annotationsArray);
296+
const buffer = [];
297+
298+
let transform = null;
299+
if (this.xref.encrypt) {
300+
transform = this.xref.encrypt.createCipherTransform(
301+
this.ref.num,
302+
this.ref.gen
303+
);
304+
}
305+
306+
writeObject(this.ref, pageDict, buffer, transform);
307+
if (savedDict) {
308+
pageDict.set("Annots", savedDict);
309+
}
310+
311+
const objects = newData.dependencies;
312+
objects.push(
313+
{ ref: this.ref, data: buffer.join("") },
314+
...newData.annotations
315+
);
316+
return objects;
317+
}
318+
264319
save(handler, task, annotationStorage) {
265320
const partialEvaluator = new PartialEvaluator({
266321
xref: this.xref,

src/core/worker.js

+31
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import {
1717
AbortException,
18+
AnnotationEditorPrefix,
1819
arrayByteLength,
1920
arraysToBytes,
2021
createPromiseCapability,
@@ -557,6 +558,23 @@ class WorkerMessageHandler {
557558
function ({ isPureXfa, numPages, annotationStorage, filename }) {
558559
pdfManager.requestLoadedStream();
559560

561+
const newAnnotationsByPage = new Map();
562+
if (!isPureXfa) {
563+
// The concept of page in a XFA is very different, so
564+
// editing is just not implemented.
565+
for (const [key, value] of annotationStorage) {
566+
if (!key.startsWith(AnnotationEditorPrefix)) {
567+
continue;
568+
}
569+
let annotations = newAnnotationsByPage.get(value.pageIndex);
570+
if (!annotations) {
571+
annotations = [];
572+
newAnnotationsByPage.set(value.pageIndex, annotations);
573+
}
574+
annotations.push(value);
575+
}
576+
}
577+
560578
const promises = [
561579
pdfManager.onLoadedStream(),
562580
pdfManager.ensureCatalog("acroForm"),
@@ -565,6 +583,19 @@ class WorkerMessageHandler {
565583
pdfManager.ensureDoc("startXRef"),
566584
];
567585

586+
for (const [pageIndex, annotations] of newAnnotationsByPage) {
587+
promises.push(
588+
pdfManager.getPage(pageIndex).then(page => {
589+
const task = new WorkerTask(`Save (editor): page ${pageIndex}`);
590+
return page
591+
.saveNewAnnotations(handler, task, annotations)
592+
.finally(function () {
593+
finishWorkerTask(task);
594+
});
595+
})
596+
);
597+
}
598+
568599
if (isPureXfa) {
569600
promises.push(pdfManager.serializeXfaData(annotationStorage));
570601
} else {

0 commit comments

Comments
 (0)