[edition] Add support for saving a newly added FreeText
This commit is contained in:
parent
1816b5e926
commit
7773b3f5be
@ -16,6 +16,7 @@
|
|||||||
import {
|
import {
|
||||||
AnnotationActionEventType,
|
AnnotationActionEventType,
|
||||||
AnnotationBorderStyleType,
|
AnnotationBorderStyleType,
|
||||||
|
AnnotationEditorType,
|
||||||
AnnotationFieldFlag,
|
AnnotationFieldFlag,
|
||||||
AnnotationFlag,
|
AnnotationFlag,
|
||||||
AnnotationReplyType,
|
AnnotationReplyType,
|
||||||
@ -24,12 +25,14 @@ import {
|
|||||||
escapeString,
|
escapeString,
|
||||||
getModificationDate,
|
getModificationDate,
|
||||||
isAscii,
|
isAscii,
|
||||||
|
LINE_DESCENT_FACTOR,
|
||||||
LINE_FACTOR,
|
LINE_FACTOR,
|
||||||
OPS,
|
OPS,
|
||||||
RenderingIntentFlag,
|
RenderingIntentFlag,
|
||||||
shadow,
|
shadow,
|
||||||
stringToPDFString,
|
stringToPDFString,
|
||||||
stringToUTF16BEString,
|
stringToUTF16BEString,
|
||||||
|
stringToUTF8String,
|
||||||
unreachable,
|
unreachable,
|
||||||
Util,
|
Util,
|
||||||
warn,
|
warn,
|
||||||
@ -45,6 +48,7 @@ import {
|
|||||||
parseDefaultAppearance,
|
parseDefaultAppearance,
|
||||||
} from "./default_appearance.js";
|
} from "./default_appearance.js";
|
||||||
import { Dict, isName, Name, Ref, RefSet } from "./primitives.js";
|
import { Dict, isName, Name, Ref, RefSet } from "./primitives.js";
|
||||||
|
import { writeDict, writeObject } from "./writer.js";
|
||||||
import { BaseStream } from "./base_stream.js";
|
import { BaseStream } from "./base_stream.js";
|
||||||
import { bidi } from "./bidi.js";
|
import { bidi } from "./bidi.js";
|
||||||
import { Catalog } from "./catalog.js";
|
import { Catalog } from "./catalog.js";
|
||||||
@ -53,7 +57,6 @@ import { FileSpec } from "./file_spec.js";
|
|||||||
import { ObjectLoader } from "./object_loader.js";
|
import { ObjectLoader } from "./object_loader.js";
|
||||||
import { OperatorList } from "./operator_list.js";
|
import { OperatorList } from "./operator_list.js";
|
||||||
import { StringStream } from "./stream.js";
|
import { StringStream } from "./stream.js";
|
||||||
import { writeDict } from "./writer.js";
|
|
||||||
import { XFAFactory } from "./xfa/factory.js";
|
import { XFAFactory } from "./xfa/factory.js";
|
||||||
|
|
||||||
class AnnotationFactory {
|
class AnnotationFactory {
|
||||||
@ -237,6 +240,49 @@ class AnnotationFactory {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async saveNewAnnotations(evaluator, task, annotations) {
|
||||||
|
const xref = evaluator.xref;
|
||||||
|
let baseFontRef;
|
||||||
|
const results = [];
|
||||||
|
const dependencies = [];
|
||||||
|
const promises = [];
|
||||||
|
for (const annotation of annotations) {
|
||||||
|
switch (annotation.annotationType) {
|
||||||
|
case AnnotationEditorType.FREETEXT:
|
||||||
|
if (!baseFontRef) {
|
||||||
|
const baseFont = new Dict(xref);
|
||||||
|
baseFont.set("BaseFont", Name.get("Helvetica"));
|
||||||
|
baseFont.set("Type", Name.get("Font"));
|
||||||
|
baseFont.set("Subtype", Name.get("Type1"));
|
||||||
|
baseFont.set("Encoding", Name.get("WinAnsiEncoding"));
|
||||||
|
const buffer = [];
|
||||||
|
baseFontRef = xref.getNewRef();
|
||||||
|
writeObject(baseFontRef, baseFont, buffer, null);
|
||||||
|
dependencies.push({ ref: baseFontRef, data: buffer.join("") });
|
||||||
|
}
|
||||||
|
promises.push(
|
||||||
|
FreeTextAnnotation.createNewAnnotation(
|
||||||
|
xref,
|
||||||
|
evaluator,
|
||||||
|
task,
|
||||||
|
annotation,
|
||||||
|
baseFontRef,
|
||||||
|
results,
|
||||||
|
dependencies
|
||||||
|
)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
return {
|
||||||
|
annotations: results,
|
||||||
|
dependencies,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRgbColor(color, defaultColor = new Uint8ClampedArray(3)) {
|
function getRgbColor(color, defaultColor = new Uint8ClampedArray(3)) {
|
||||||
@ -1617,7 +1663,12 @@ class WidgetAnnotation extends Annotation {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const font = await this._getFontData(evaluator, task);
|
const font = await WidgetAnnotation._getFontData(
|
||||||
|
evaluator,
|
||||||
|
task,
|
||||||
|
this.data.defaultAppearanceData,
|
||||||
|
this._fieldResources.mergedResources
|
||||||
|
);
|
||||||
const [defaultAppearance, fontSize] = this._computeFontSize(
|
const [defaultAppearance, fontSize] = this._computeFontSize(
|
||||||
totalHeight - defaultPadding,
|
totalHeight - defaultPadding,
|
||||||
totalWidth - 2 * hPadding,
|
totalWidth - 2 * hPadding,
|
||||||
@ -1700,7 +1751,7 @@ class WidgetAnnotation extends Annotation {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _getFontData(evaluator, task) {
|
static async _getFontData(evaluator, task, appearanceData, resources) {
|
||||||
const operatorList = new OperatorList();
|
const operatorList = new OperatorList();
|
||||||
const initialState = {
|
const initialState = {
|
||||||
font: null,
|
font: null,
|
||||||
@ -1709,9 +1760,9 @@ class WidgetAnnotation extends Annotation {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const { fontName, fontSize } = this.data.defaultAppearanceData;
|
const { fontName, fontSize } = appearanceData;
|
||||||
await evaluator.handleSetFont(
|
await evaluator.handleSetFont(
|
||||||
this._fieldResources.mergedResources,
|
resources,
|
||||||
[fontName && Name.get(fontName), fontSize],
|
[fontName && Name.get(fontName), fontSize],
|
||||||
/* fontRef = */ null,
|
/* fontRef = */ null,
|
||||||
operatorList,
|
operatorList,
|
||||||
@ -2640,7 +2691,12 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const font = await this._getFontData(evaluator, task);
|
const font = await WidgetAnnotation._getFontData(
|
||||||
|
evaluator,
|
||||||
|
task,
|
||||||
|
this.data.defaultAppearanceData,
|
||||||
|
this._fieldResources.mergedResources
|
||||||
|
);
|
||||||
|
|
||||||
let defaultAppearance;
|
let defaultAppearance;
|
||||||
let { fontSize } = this.data.defaultAppearanceData;
|
let { fontSize } = this.data.defaultAppearanceData;
|
||||||
@ -2871,6 +2927,129 @@ class FreeTextAnnotation extends MarkupAnnotation {
|
|||||||
|
|
||||||
this.data.annotationType = AnnotationType.FREETEXT;
|
this.data.annotationType = AnnotationType.FREETEXT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async createNewAnnotation(
|
||||||
|
xref,
|
||||||
|
evaluator,
|
||||||
|
task,
|
||||||
|
annotation,
|
||||||
|
baseFontRef,
|
||||||
|
results,
|
||||||
|
dependencies
|
||||||
|
) {
|
||||||
|
const { color, fontSize, rect, user, value } = annotation;
|
||||||
|
const freetextRef = xref.getNewRef();
|
||||||
|
const freetext = new Dict(xref);
|
||||||
|
freetext.set("Type", Name.get("Annot"));
|
||||||
|
freetext.set("Subtype", Name.get("FreeText"));
|
||||||
|
freetext.set("CreationDate", `D:${getModificationDate()}`);
|
||||||
|
freetext.set("Rect", rect);
|
||||||
|
const da = `/Helv ${fontSize} Tf ${getPdfColor(color)}`;
|
||||||
|
freetext.set("DA", da);
|
||||||
|
freetext.set("Contents", value);
|
||||||
|
freetext.set("F", 4);
|
||||||
|
freetext.set("Border", [0, 0, 0]);
|
||||||
|
freetext.set("Rotate", 0);
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
freetext.set("T", stringToUTF8String(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
const resources = new Dict(xref);
|
||||||
|
const font = new Dict(xref);
|
||||||
|
font.set("Helv", baseFontRef);
|
||||||
|
resources.set("Font", font);
|
||||||
|
|
||||||
|
const helv = await WidgetAnnotation._getFontData(
|
||||||
|
evaluator,
|
||||||
|
task,
|
||||||
|
{
|
||||||
|
fontName: "Helvetica",
|
||||||
|
fontSize,
|
||||||
|
},
|
||||||
|
resources
|
||||||
|
);
|
||||||
|
|
||||||
|
const [x1, y1, x2, y2] = rect;
|
||||||
|
const w = x2 - x1;
|
||||||
|
const h = y2 - y1;
|
||||||
|
|
||||||
|
const lines = value.split("\n");
|
||||||
|
const scale = fontSize / 1000;
|
||||||
|
let totalWidth = -Infinity;
|
||||||
|
const encodedLines = [];
|
||||||
|
for (let line of lines) {
|
||||||
|
line = helv.encodeString(line).join("");
|
||||||
|
encodedLines.push(line);
|
||||||
|
let lineWidth = 0;
|
||||||
|
const glyphs = helv.charsToGlyphs(line);
|
||||||
|
for (const glyph of glyphs) {
|
||||||
|
lineWidth += glyph.width * scale;
|
||||||
|
}
|
||||||
|
totalWidth = Math.max(totalWidth, lineWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
let hscale = 1;
|
||||||
|
if (totalWidth > w) {
|
||||||
|
hscale = w / totalWidth;
|
||||||
|
}
|
||||||
|
let vscale = 1;
|
||||||
|
const lineHeight = LINE_FACTOR * fontSize;
|
||||||
|
const lineDescent = LINE_DESCENT_FACTOR * fontSize;
|
||||||
|
const totalHeight = lineHeight * lines.length;
|
||||||
|
if (totalHeight > h) {
|
||||||
|
vscale = h / totalHeight;
|
||||||
|
}
|
||||||
|
const fscale = Math.min(hscale, vscale);
|
||||||
|
const newFontSize = fontSize * fscale;
|
||||||
|
const buffer = [
|
||||||
|
"q",
|
||||||
|
`0 0 ${numberToString(w)} ${numberToString(h)} re W n`,
|
||||||
|
`BT`,
|
||||||
|
`1 0 0 1 0 ${numberToString(h + lineDescent)} Tm 0 Tc ${getPdfColor(
|
||||||
|
color
|
||||||
|
)}`,
|
||||||
|
`/Helv ${numberToString(newFontSize)} Tf`,
|
||||||
|
];
|
||||||
|
|
||||||
|
const vShift = numberToString(lineHeight);
|
||||||
|
for (const line of encodedLines) {
|
||||||
|
buffer.push(`0 -${vShift} Td (${escapeString(line)}) Tj`);
|
||||||
|
}
|
||||||
|
buffer.push("ET", "Q");
|
||||||
|
const appearance = buffer.join("\n");
|
||||||
|
|
||||||
|
const appearanceStreamDict = new Dict(xref);
|
||||||
|
appearanceStreamDict.set("FormType", 1);
|
||||||
|
appearanceStreamDict.set("Subtype", Name.get("Form"));
|
||||||
|
appearanceStreamDict.set("Type", Name.get("XObject"));
|
||||||
|
appearanceStreamDict.set("BBox", [0, 0, w, h]);
|
||||||
|
appearanceStreamDict.set("Length", appearance.length);
|
||||||
|
appearanceStreamDict.set("Resources", resources);
|
||||||
|
|
||||||
|
const ap = new StringStream(appearance);
|
||||||
|
ap.dict = appearanceStreamDict;
|
||||||
|
|
||||||
|
buffer.length = 0;
|
||||||
|
const apRef = xref.getNewRef();
|
||||||
|
let transform = xref.encrypt
|
||||||
|
? xref.encrypt.createCipherTransform(apRef.num, apRef.gen)
|
||||||
|
: null;
|
||||||
|
writeObject(apRef, ap, buffer, transform);
|
||||||
|
dependencies.push({ ref: apRef, data: buffer.join("") });
|
||||||
|
|
||||||
|
const n = new Dict(xref);
|
||||||
|
n.set("N", apRef);
|
||||||
|
freetext.set("AP", n);
|
||||||
|
|
||||||
|
buffer.length = 0;
|
||||||
|
transform = xref.encrypt
|
||||||
|
? xref.encrypt.createCipherTransform(freetextRef.num, freetextRef.gen)
|
||||||
|
: null;
|
||||||
|
writeObject(freetextRef, freetext, buffer, transform);
|
||||||
|
|
||||||
|
results.push({ ref: freetextRef, data: buffer.join("") });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LineAnnotation extends MarkupAnnotation {
|
class LineAnnotation extends MarkupAnnotation {
|
||||||
|
@ -55,6 +55,7 @@ import { OperatorList } from "./operator_list.js";
|
|||||||
import { PartialEvaluator } from "./evaluator.js";
|
import { PartialEvaluator } from "./evaluator.js";
|
||||||
import { StreamsSequenceStream } from "./decode_stream.js";
|
import { StreamsSequenceStream } from "./decode_stream.js";
|
||||||
import { StructTreePage } from "./struct_tree.js";
|
import { StructTreePage } from "./struct_tree.js";
|
||||||
|
import { writeObject } from "./writer.js";
|
||||||
import { XFAFactory } from "./xfa/factory.js";
|
import { XFAFactory } from "./xfa/factory.js";
|
||||||
import { XRef } from "./xref.js";
|
import { XRef } from "./xref.js";
|
||||||
|
|
||||||
@ -261,6 +262,60 @@ class Page {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async saveNewAnnotations(handler, task, annotations) {
|
||||||
|
if (this.xfaFactory) {
|
||||||
|
throw new Error("XFA: Cannot save new annotations.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const partialEvaluator = new PartialEvaluator({
|
||||||
|
xref: this.xref,
|
||||||
|
handler,
|
||||||
|
pageIndex: this.pageIndex,
|
||||||
|
idFactory: this._localIdFactory,
|
||||||
|
fontCache: this.fontCache,
|
||||||
|
builtInCMapCache: this.builtInCMapCache,
|
||||||
|
standardFontDataCache: this.standardFontDataCache,
|
||||||
|
globalImageCache: this.globalImageCache,
|
||||||
|
options: this.evaluatorOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageDict = this.pageDict;
|
||||||
|
const annotationsArray = this.annotations.slice();
|
||||||
|
const newData = await AnnotationFactory.saveNewAnnotations(
|
||||||
|
partialEvaluator,
|
||||||
|
task,
|
||||||
|
annotations
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const { ref } of newData.annotations) {
|
||||||
|
annotationsArray.push(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedDict = pageDict.get("Annots");
|
||||||
|
pageDict.set("Annots", annotationsArray);
|
||||||
|
const buffer = [];
|
||||||
|
|
||||||
|
let transform = null;
|
||||||
|
if (this.xref.encrypt) {
|
||||||
|
transform = this.xref.encrypt.createCipherTransform(
|
||||||
|
this.ref.num,
|
||||||
|
this.ref.gen
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeObject(this.ref, pageDict, buffer, transform);
|
||||||
|
if (savedDict) {
|
||||||
|
pageDict.set("Annots", savedDict);
|
||||||
|
}
|
||||||
|
|
||||||
|
const objects = newData.dependencies;
|
||||||
|
objects.push(
|
||||||
|
{ ref: this.ref, data: buffer.join("") },
|
||||||
|
...newData.annotations
|
||||||
|
);
|
||||||
|
return objects;
|
||||||
|
}
|
||||||
|
|
||||||
save(handler, task, annotationStorage) {
|
save(handler, task, annotationStorage) {
|
||||||
const partialEvaluator = new PartialEvaluator({
|
const partialEvaluator = new PartialEvaluator({
|
||||||
xref: this.xref,
|
xref: this.xref,
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AbortException,
|
AbortException,
|
||||||
|
AnnotationEditorPrefix,
|
||||||
arrayByteLength,
|
arrayByteLength,
|
||||||
arraysToBytes,
|
arraysToBytes,
|
||||||
createPromiseCapability,
|
createPromiseCapability,
|
||||||
@ -557,6 +558,23 @@ class WorkerMessageHandler {
|
|||||||
function ({ isPureXfa, numPages, annotationStorage, filename }) {
|
function ({ isPureXfa, numPages, annotationStorage, filename }) {
|
||||||
pdfManager.requestLoadedStream();
|
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 promises = [
|
const promises = [
|
||||||
pdfManager.onLoadedStream(),
|
pdfManager.onLoadedStream(),
|
||||||
pdfManager.ensureCatalog("acroForm"),
|
pdfManager.ensureCatalog("acroForm"),
|
||||||
@ -565,6 +583,19 @@ class WorkerMessageHandler {
|
|||||||
pdfManager.ensureDoc("startXRef"),
|
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 (isPureXfa) {
|
if (isPureXfa) {
|
||||||
promises.push(pdfManager.serializeXfaData(annotationStorage));
|
promises.push(pdfManager.serializeXfaData(annotationStorage));
|
||||||
} else {
|
} else {
|
||||||
|
@ -20,6 +20,16 @@ import { SimpleDOMNode, SimpleXMLParser } from "./xml_parser.js";
|
|||||||
import { BaseStream } from "./base_stream.js";
|
import { BaseStream } from "./base_stream.js";
|
||||||
import { calculateMD5 } from "./crypto.js";
|
import { calculateMD5 } from "./crypto.js";
|
||||||
|
|
||||||
|
function writeObject(ref, obj, buffer, transform) {
|
||||||
|
buffer.push(`${ref.num} ${ref.gen} obj\n`);
|
||||||
|
if (obj instanceof Dict) {
|
||||||
|
writeDict(obj, buffer, transform);
|
||||||
|
} else if (obj instanceof BaseStream) {
|
||||||
|
writeStream(obj, buffer, transform);
|
||||||
|
}
|
||||||
|
buffer.push("\nendobj\n");
|
||||||
|
}
|
||||||
|
|
||||||
function writeDict(dict, buffer, transform) {
|
function writeDict(dict, buffer, transform) {
|
||||||
buffer.push("<<");
|
buffer.push("<<");
|
||||||
for (const key of dict.getKeys()) {
|
for (const key of dict.getKeys()) {
|
||||||
@ -328,4 +338,4 @@ function incrementalUpdate({
|
|||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { incrementalUpdate, writeDict };
|
export { incrementalUpdate, writeDict, writeObject };
|
||||||
|
@ -47,7 +47,7 @@ class XRef {
|
|||||||
|
|
||||||
getNewRef() {
|
getNewRef() {
|
||||||
if (this._newRefNum === null) {
|
if (this._newRefNum === null) {
|
||||||
this._newRefNum = this.entries.length;
|
this._newRefNum = this.entries.length || 1;
|
||||||
}
|
}
|
||||||
return Ref.get(this._newRefNum++, 0);
|
return Ref.get(this._newRefNum++, 0);
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ class AnnotationEditorLayer {
|
|||||||
|
|
||||||
#uiManager;
|
#uiManager;
|
||||||
|
|
||||||
static _l10nInitialized = false;
|
static _initialized = false;
|
||||||
|
|
||||||
static _keyboardManager = new KeyboardManager([
|
static _keyboardManager = new KeyboardManager([
|
||||||
[["ctrl+a", "mac+meta+a"], AnnotationEditorLayer.prototype.selectAll],
|
[["ctrl+a", "mac+meta+a"], AnnotationEditorLayer.prototype.selectAll],
|
||||||
@ -73,9 +73,9 @@ class AnnotationEditorLayer {
|
|||||||
* @param {AnnotationEditorLayerOptions} options
|
* @param {AnnotationEditorLayerOptions} options
|
||||||
*/
|
*/
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
if (!AnnotationEditorLayer._l10nInitialized) {
|
if (!AnnotationEditorLayer._initialized) {
|
||||||
AnnotationEditorLayer._l10nInitialized = true;
|
AnnotationEditorLayer._initialized = true;
|
||||||
FreeTextEditor.setL10n(options.l10n);
|
FreeTextEditor.initialize(options.l10n);
|
||||||
}
|
}
|
||||||
this.#uiManager = options.uiManager;
|
this.#uiManager = options.uiManager;
|
||||||
this.annotationStorage = options.annotationStorage;
|
this.annotationStorage = options.annotationStorage;
|
||||||
|
@ -140,6 +140,14 @@ class AnnotationEditor {
|
|||||||
this.div.style.height = `${height}px`;
|
this.div.style.height = `${height}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the translation used to position this editor when it's created.
|
||||||
|
* @returns {Array<number>}
|
||||||
|
*/
|
||||||
|
getInitialTranslation() {
|
||||||
|
return [0, 0];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render this editor in a div.
|
* Render this editor in a div.
|
||||||
* @returns {HTMLDivElement}
|
* @returns {HTMLDivElement}
|
||||||
@ -150,6 +158,11 @@ class AnnotationEditor {
|
|||||||
this.div.setAttribute("id", this.id);
|
this.div.setAttribute("id", this.id);
|
||||||
this.div.draggable = true;
|
this.div.draggable = true;
|
||||||
this.div.tabIndex = 100;
|
this.div.tabIndex = 100;
|
||||||
|
|
||||||
|
const [tx, ty] = this.getInitialTranslation();
|
||||||
|
this.x = Math.round(this.x + tx);
|
||||||
|
this.y = Math.round(this.y + ty);
|
||||||
|
|
||||||
this.div.style.left = `${this.x}px`;
|
this.div.style.left = `${this.x}px`;
|
||||||
this.div.style.top = `${this.y}px`;
|
this.div.style.top = `${this.y}px`;
|
||||||
|
|
||||||
|
@ -13,9 +13,15 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AnnotationEditorType, Util } from "../../shared/util.js";
|
import {
|
||||||
|
AnnotationEditorType,
|
||||||
|
assert,
|
||||||
|
LINE_FACTOR,
|
||||||
|
Util,
|
||||||
|
} from "../../shared/util.js";
|
||||||
import { AnnotationEditor } from "./editor.js";
|
import { AnnotationEditor } from "./editor.js";
|
||||||
import { bindEvents } from "./tools.js";
|
import { bindEvents } from "./tools.js";
|
||||||
|
import { PixelsPerInch } from "../display_utils.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic text editor in order to create a FreeTex annotation.
|
* Basic text editor in order to create a FreeTex annotation.
|
||||||
@ -33,14 +39,36 @@ class FreeTextEditor extends AnnotationEditor {
|
|||||||
|
|
||||||
static _l10nPromise;
|
static _l10nPromise;
|
||||||
|
|
||||||
|
static _internalPadding = 0;
|
||||||
|
|
||||||
constructor(params) {
|
constructor(params) {
|
||||||
super({ ...params, name: "freeTextEditor" });
|
super({ ...params, name: "freeTextEditor" });
|
||||||
this.#color = params.color || "CanvasText";
|
this.#color = params.color || "CanvasText";
|
||||||
this.#fontSize = params.fontSize || 10;
|
this.#fontSize = params.fontSize || 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
static setL10n(l10n) {
|
static initialize(l10n) {
|
||||||
this._l10nPromise = l10n.get("freetext_default_content");
|
this._l10nPromise = l10n.get("freetext_default_content");
|
||||||
|
const style = getComputedStyle(document.documentElement);
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof PDFJSDev === "undefined" ||
|
||||||
|
PDFJSDev.test("!PRODUCTION || TESTING")
|
||||||
|
) {
|
||||||
|
const lineHeight = parseFloat(
|
||||||
|
style.getPropertyValue("--freetext-line-height"),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
lineHeight === LINE_FACTOR,
|
||||||
|
"Update the CSS variable to agree with the constant."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._internalPadding = parseFloat(
|
||||||
|
style.getPropertyValue("--freetext-padding"),
|
||||||
|
10
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
@ -62,6 +90,16 @@ class FreeTextEditor extends AnnotationEditor {
|
|||||||
return editor;
|
return editor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
getInitialTranslation() {
|
||||||
|
// The start of the base line is where the user clicked.
|
||||||
|
return [
|
||||||
|
-FreeTextEditor._internalPadding * this.parent.zoomFactor,
|
||||||
|
-(FreeTextEditor._internalPadding + this.#fontSize) *
|
||||||
|
this.parent.zoomFactor,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
rebuild() {
|
rebuild() {
|
||||||
if (this.div === null) {
|
if (this.div === null) {
|
||||||
@ -174,7 +212,6 @@ class FreeTextEditor extends AnnotationEditor {
|
|||||||
|
|
||||||
const { style } = this.editorDiv;
|
const { style } = this.editorDiv;
|
||||||
style.fontSize = `calc(${this.#fontSize}px * var(--zoom-factor))`;
|
style.fontSize = `calc(${this.#fontSize}px * var(--zoom-factor))`;
|
||||||
style.minHeight = `calc(${1.5 * this.#fontSize}px * var(--zoom-factor))`;
|
|
||||||
style.color = this.#color;
|
style.color = this.#color;
|
||||||
|
|
||||||
this.div.appendChild(this.editorDiv);
|
this.div.appendChild(this.editorDiv);
|
||||||
@ -200,21 +237,21 @@ class FreeTextEditor extends AnnotationEditor {
|
|||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
serialize() {
|
serialize() {
|
||||||
const rect = this.div.getBoundingClientRect();
|
const rect = this.editorDiv.getBoundingClientRect();
|
||||||
|
const padding = FreeTextEditor._internalPadding * this.parent.zoomFactor;
|
||||||
const [x1, y1] = Util.applyTransform(
|
const [x1, y1] = Util.applyTransform(
|
||||||
[this.x, this.y + rect.height],
|
[this.x + padding, this.y + padding + rect.height],
|
||||||
this.parent.inverseViewportTransform
|
this.parent.inverseViewportTransform
|
||||||
);
|
);
|
||||||
|
|
||||||
const [x2, y2] = Util.applyTransform(
|
const [x2, y2] = Util.applyTransform(
|
||||||
[this.x + rect.width, this.y],
|
[this.x + padding + rect.width, this.y + padding],
|
||||||
this.parent.inverseViewportTransform
|
this.parent.inverseViewportTransform
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
annotationType: AnnotationEditorType.FREETEXT,
|
annotationType: AnnotationEditorType.FREETEXT,
|
||||||
color: [0, 0, 0],
|
color: [0, 0, 0],
|
||||||
fontSize: this.#fontSize,
|
fontSize: this.#fontSize / PixelsPerInch.PDF_TO_CSS_UNITS,
|
||||||
value: this.#content,
|
value: this.#content,
|
||||||
pageIndex: this.parent.pageIndex,
|
pageIndex: this.parent.pageIndex,
|
||||||
rect: [x1, y1, x2, y2],
|
rect: [x1, y1, x2, y2],
|
||||||
|
@ -21,6 +21,7 @@ const FONT_IDENTITY_MATRIX = [0.001, 0, 0, 0.001, 0, 0];
|
|||||||
// Represent the percentage of the height of a single-line field over
|
// Represent the percentage of the height of a single-line field over
|
||||||
// the font size. Acrobat seems to use this value.
|
// the font size. Acrobat seems to use this value.
|
||||||
const LINE_FACTOR = 1.35;
|
const LINE_FACTOR = 1.35;
|
||||||
|
const LINE_DESCENT_FACTOR = 0.35;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refer to the `WorkerTransport.getRenderingIntent`-method in the API, to see
|
* Refer to the `WorkerTransport.getRenderingIntent`-method in the API, to see
|
||||||
@ -1175,6 +1176,7 @@ export {
|
|||||||
isArrayBuffer,
|
isArrayBuffer,
|
||||||
isArrayEqual,
|
isArrayEqual,
|
||||||
isAscii,
|
isAscii,
|
||||||
|
LINE_DESCENT_FACTOR,
|
||||||
LINE_FACTOR,
|
LINE_FACTOR,
|
||||||
MissingPDFException,
|
MissingPDFException,
|
||||||
objectFromMap,
|
objectFromMap,
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
} from "../../src/core/annotation.js";
|
} from "../../src/core/annotation.js";
|
||||||
import {
|
import {
|
||||||
AnnotationBorderStyleType,
|
AnnotationBorderStyleType,
|
||||||
|
AnnotationEditorType,
|
||||||
AnnotationFieldFlag,
|
AnnotationFieldFlag,
|
||||||
AnnotationFlag,
|
AnnotationFlag,
|
||||||
AnnotationType,
|
AnnotationType,
|
||||||
@ -3857,6 +3858,61 @@ describe("annotation", function () {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("FreeTextAnnotation", () => {
|
||||||
|
it("should create an new FreeText annotation", async () => {
|
||||||
|
partialEvaluator.xref = new XRefMock();
|
||||||
|
const task = new WorkerTask("test FreeText creation");
|
||||||
|
const data = await AnnotationFactory.saveNewAnnotations(
|
||||||
|
partialEvaluator,
|
||||||
|
task,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
annotationType: AnnotationEditorType.FREETEXT,
|
||||||
|
rect: [12, 34, 56, 78],
|
||||||
|
fontSize: 10,
|
||||||
|
color: [0, 0, 0],
|
||||||
|
value: "Hello PDF.js World!",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const base = data.annotations[0].data.replace(/\(D:\d+\)/, "(date)");
|
||||||
|
expect(base).toEqual(
|
||||||
|
"2 0 obj\n" +
|
||||||
|
"<< /Type /Annot /Subtype /FreeText /CreationDate (date) " +
|
||||||
|
"/Rect [12 34 56 78] /DA (/Helv 10 Tf 0 g) /Contents (Hello PDF.js World!) " +
|
||||||
|
"/F 4 /Border [0 0 0] /Rotate 0 /AP << /N 3 0 R>>>>\n" +
|
||||||
|
"endobj\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
const font = data.dependencies[0].data;
|
||||||
|
expect(font).toEqual(
|
||||||
|
"1 0 obj\n" +
|
||||||
|
"<< /BaseFont /Helvetica /Type /Font /Subtype /Type1 /Encoding " +
|
||||||
|
"/WinAnsiEncoding>>\n" +
|
||||||
|
"endobj\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
const appearance = data.dependencies[1].data;
|
||||||
|
expect(appearance).toEqual(
|
||||||
|
"3 0 obj\n" +
|
||||||
|
"<< /FormType 1 /Subtype /Form /Type /XObject /BBox [0 0 44 44] " +
|
||||||
|
"/Length 101 /Resources << /Font << /Helv 1 0 R>>>>>> stream\n" +
|
||||||
|
"q\n" +
|
||||||
|
"0 0 44 44 re W n\n" +
|
||||||
|
"BT\n" +
|
||||||
|
"1 0 0 1 0 47.5 Tm 0 Tc 0 g\n" +
|
||||||
|
"/Helv 10 Tf\n" +
|
||||||
|
"0 -13.5 Td (Hello PDF.js World!) Tj\n" +
|
||||||
|
"ET\n" +
|
||||||
|
"Q\n" +
|
||||||
|
"endstream\n" +
|
||||||
|
"\n" +
|
||||||
|
"endobj\n"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("InkAnnotation", function () {
|
describe("InkAnnotation", function () {
|
||||||
it("should handle a single ink list", async function () {
|
it("should handle a single ink list", async function () {
|
||||||
const inkDict = new Dict();
|
const inkDict = new Dict();
|
||||||
|
@ -88,7 +88,7 @@ class XRefMock {
|
|||||||
|
|
||||||
getNewRef() {
|
getNewRef() {
|
||||||
if (this._newRefNum === null) {
|
if (this._newRefNum === null) {
|
||||||
this._newRefNum = Object.keys(this._map).length;
|
this._newRefNum = Object.keys(this._map).length || 1;
|
||||||
}
|
}
|
||||||
return Ref.get(this._newRefNum++, 0);
|
return Ref.get(this._newRefNum++, 0);
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
:root {
|
:root {
|
||||||
--focus-outline: solid 2px red;
|
--focus-outline: solid 2px red;
|
||||||
--hover-outline: dashed 2px blue;
|
--hover-outline: dashed 2px blue;
|
||||||
|
--freetext-line-height: 1.35;
|
||||||
|
--freetext-padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.annotationEditorLayer {
|
.annotationEditorLayer {
|
||||||
@ -31,7 +33,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 5px;
|
padding: calc(var(--freetext-padding) * var(--zoom-factor));
|
||||||
resize: none;
|
resize: none;
|
||||||
width: auto;
|
width: auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
@ -42,10 +44,11 @@
|
|||||||
border: none;
|
border: none;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
min-height: 15px;
|
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
resize: none;
|
resize: none;
|
||||||
|
font: 10px sans-serif;
|
||||||
|
line-height: var(--freetext-line-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
.annotationEditorLayer .freeTextEditor .overlay {
|
.annotationEditorLayer .freeTextEditor .overlay {
|
||||||
|
Loading…
Reference in New Issue
Block a user