[Annotation] Fix printing/saving for annotations containing some non-ascii chars and with no fonts to handle them (bug 1666824)
- For text fields * when printing, we generate a fake font which contains some widths computed thanks to an OffscreenCanvas and its method measureText. In order to avoid to have to layout the glyphs ourselves, we just render all of them in one call in the showText method in using the system sans-serif/monospace fonts. * when saving, we continue to create the appearance streams if the fonts contain the char but when a char is missing, we just set, in the AcroForm dict, the flag /NeedAppearances to true and remove the appearance stream. This way, we let the different readers handle the rendering of the strings. - For FreeText annotations * when printing, we use the same trick as for text fields. * there is no need to save an appearance since Acrobat is able to infer one from the Content entry.
This commit is contained in:
parent
f7449563ef
commit
3ca03603c2
@ -22,7 +22,9 @@ import {
|
||||
AnnotationReplyType,
|
||||
AnnotationType,
|
||||
assert,
|
||||
BASELINE_FACTOR,
|
||||
escapeString,
|
||||
FeatureTest,
|
||||
getModificationDate,
|
||||
IDENTITY_MATRIX,
|
||||
isAscii,
|
||||
@ -33,7 +35,6 @@ import {
|
||||
shadow,
|
||||
stringToPDFString,
|
||||
stringToUTF16BEString,
|
||||
stringToUTF8String,
|
||||
unreachable,
|
||||
Util,
|
||||
warn,
|
||||
@ -41,10 +42,13 @@ import {
|
||||
import {
|
||||
collectActions,
|
||||
getInheritableProperty,
|
||||
getRotationMatrix,
|
||||
numberToString,
|
||||
stringToUTF16String,
|
||||
} from "./core_utils.js";
|
||||
import {
|
||||
createDefaultAppearance,
|
||||
FakeUnicodeFont,
|
||||
getPdfColor,
|
||||
parseDefaultAppearance,
|
||||
} from "./default_appearance.js";
|
||||
@ -143,6 +147,9 @@ class AnnotationFactory {
|
||||
needAppearances:
|
||||
!collectFields && acroFormDict.get("NeedAppearances") === true,
|
||||
pageIndex,
|
||||
isOffscreenCanvasSupported:
|
||||
FeatureTest.isOffscreenCanvasSupported &&
|
||||
pdfManager.evaluatorOptions.isOffscreenCanvasSupported,
|
||||
};
|
||||
|
||||
switch (subtype) {
|
||||
@ -268,7 +275,7 @@ class AnnotationFactory {
|
||||
baseFont.set("Subtype", Name.get("Type1"));
|
||||
baseFont.set("Encoding", Name.get("WinAnsiEncoding"));
|
||||
const buffer = [];
|
||||
baseFontRef = xref.getNewRef();
|
||||
baseFontRef = xref.getNewTemporaryRef();
|
||||
writeObject(baseFontRef, baseFont, buffer, null);
|
||||
dependencies.push({ ref: baseFontRef, data: buffer.join("") });
|
||||
}
|
||||
@ -301,6 +308,9 @@ class AnnotationFactory {
|
||||
|
||||
const xref = evaluator.xref;
|
||||
const promises = [];
|
||||
const isOffscreenCanvasSupported =
|
||||
FeatureTest.isOffscreenCanvasSupported &&
|
||||
evaluator.options.isOffscreenCanvasSupported;
|
||||
for (const annotation of annotations) {
|
||||
switch (annotation.annotationType) {
|
||||
case AnnotationEditorType.FREETEXT:
|
||||
@ -308,12 +318,15 @@ class AnnotationFactory {
|
||||
FreeTextAnnotation.createNewPrintAnnotation(xref, annotation, {
|
||||
evaluator,
|
||||
task,
|
||||
isOffscreenCanvasSupported,
|
||||
})
|
||||
);
|
||||
break;
|
||||
case AnnotationEditorType.INK:
|
||||
promises.push(
|
||||
InkAnnotation.createNewPrintAnnotation(xref, annotation)
|
||||
InkAnnotation.createNewPrintAnnotation(xref, annotation, {
|
||||
isOffscreenCanvasSupported,
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
@ -614,6 +627,17 @@ class Annotation {
|
||||
return { str, dir };
|
||||
}
|
||||
|
||||
setDefaultAppearance(params) {
|
||||
const defaultAppearance =
|
||||
getInheritableProperty({ dict: params.dict, key: "DA" }) ||
|
||||
params.acroForm.get("DA");
|
||||
this._defaultAppearance =
|
||||
typeof defaultAppearance === "string" ? defaultAppearance : "";
|
||||
this.data.defaultAppearanceData = parseDefaultAppearance(
|
||||
this._defaultAppearance
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the title.
|
||||
*
|
||||
@ -1449,20 +1473,25 @@ class MarkupAnnotation extends Annotation {
|
||||
}
|
||||
|
||||
static async createNewAnnotation(xref, annotation, dependencies, params) {
|
||||
const annotationRef = xref.getNewRef();
|
||||
const apRef = xref.getNewRef();
|
||||
const annotationDict = this.createNewDict(annotation, xref, { apRef });
|
||||
const annotationRef = xref.getNewTemporaryRef();
|
||||
const ap = await this.createNewAppearanceStream(annotation, xref, params);
|
||||
|
||||
const buffer = [];
|
||||
let transform = xref.encrypt
|
||||
? xref.encrypt.createCipherTransform(apRef.num, apRef.gen)
|
||||
: null;
|
||||
writeObject(apRef, ap, buffer, transform);
|
||||
dependencies.push({ ref: apRef, data: buffer.join("") });
|
||||
let annotationDict;
|
||||
|
||||
if (ap) {
|
||||
const apRef = xref.getNewTemporaryRef();
|
||||
annotationDict = this.createNewDict(annotation, xref, { apRef });
|
||||
const transform = xref.encrypt
|
||||
? xref.encrypt.createCipherTransform(apRef.num, apRef.gen)
|
||||
: null;
|
||||
writeObject(apRef, ap, buffer, transform);
|
||||
dependencies.push({ ref: apRef, data: buffer.join("") });
|
||||
} else {
|
||||
annotationDict = this.createNewDict(annotation, xref, {});
|
||||
}
|
||||
|
||||
buffer.length = 0;
|
||||
transform = xref.encrypt
|
||||
const transform = xref.encrypt
|
||||
? xref.encrypt.createCipherTransform(annotationRef.num, annotationRef.gen)
|
||||
: null;
|
||||
writeObject(annotationRef, annotationDict, buffer, transform);
|
||||
@ -1477,6 +1506,7 @@ class MarkupAnnotation extends Annotation {
|
||||
return new this.prototype.constructor({
|
||||
dict: annotationDict,
|
||||
xref,
|
||||
isOffscreenCanvasSupported: params.isOffscreenCanvasSupported,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1489,6 +1519,7 @@ class WidgetAnnotation extends Annotation {
|
||||
const data = this.data;
|
||||
this.ref = params.ref;
|
||||
this._needAppearances = params.needAppearances;
|
||||
this._isOffscreenCanvasSupported = params.isOffscreenCanvasSupported;
|
||||
|
||||
data.annotationType = AnnotationType.WIDGET;
|
||||
if (data.fieldName === undefined) {
|
||||
@ -1533,13 +1564,7 @@ class WidgetAnnotation extends Annotation {
|
||||
|
||||
data.alternativeText = stringToPDFString(dict.get("TU") || "");
|
||||
|
||||
const defaultAppearance =
|
||||
getInheritableProperty({ dict, key: "DA" }) || params.acroForm.get("DA");
|
||||
this._defaultAppearance =
|
||||
typeof defaultAppearance === "string" ? defaultAppearance : "";
|
||||
data.defaultAppearanceData = parseDefaultAppearance(
|
||||
this._defaultAppearance
|
||||
);
|
||||
this.setDefaultAppearance(params);
|
||||
|
||||
data.hasAppearance =
|
||||
(this._needAppearances &&
|
||||
@ -1612,19 +1637,6 @@ class WidgetAnnotation extends Annotation {
|
||||
return !!(this.data.fieldFlags & flag);
|
||||
}
|
||||
|
||||
static _getRotationMatrix(rotation, width, height) {
|
||||
switch (rotation) {
|
||||
case 90:
|
||||
return [0, 1, -1, 0, width, 0];
|
||||
case 180:
|
||||
return [-1, 0, 0, -1, width, height];
|
||||
case 270:
|
||||
return [0, -1, 1, 0, 0, height];
|
||||
default:
|
||||
throw new Error("Invalid rotation");
|
||||
}
|
||||
}
|
||||
|
||||
getRotationMatrix(annotationStorage) {
|
||||
const storageEntry = annotationStorage
|
||||
? annotationStorage.get(this.data.id)
|
||||
@ -1641,7 +1653,7 @@ class WidgetAnnotation extends Annotation {
|
||||
const width = this.data.rect[2] - this.data.rect[0];
|
||||
const height = this.data.rect[3] - this.data.rect[1];
|
||||
|
||||
return WidgetAnnotation._getRotationMatrix(rotation, width, height);
|
||||
return getRotationMatrix(rotation, width, height);
|
||||
}
|
||||
|
||||
getBorderAndBackgroundAppearances(annotationStorage) {
|
||||
@ -1712,6 +1724,7 @@ class WidgetAnnotation extends Annotation {
|
||||
const content = await this._getAppearance(
|
||||
evaluator,
|
||||
task,
|
||||
intent,
|
||||
annotationStorage
|
||||
);
|
||||
if (this.appearance && content === null) {
|
||||
@ -1824,89 +1837,121 @@ class WidgetAnnotation extends Annotation {
|
||||
rotation = this.rotation;
|
||||
}
|
||||
|
||||
let appearance = await this._getAppearance(
|
||||
evaluator,
|
||||
task,
|
||||
annotationStorage
|
||||
);
|
||||
if (appearance === null) {
|
||||
return null;
|
||||
let appearance = null;
|
||||
if (!this._needAppearances) {
|
||||
appearance = await this._getAppearance(
|
||||
evaluator,
|
||||
task,
|
||||
RenderingIntentFlag.SAVE,
|
||||
annotationStorage
|
||||
);
|
||||
if (appearance === null) {
|
||||
// Appearance didn't change.
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
// No need to create an appearance: the pdf has the flag /NeedAppearances
|
||||
// which means that it's up to the reader to produce an appearance.
|
||||
}
|
||||
|
||||
let needAppearances = false;
|
||||
if (appearance && appearance.needAppearances) {
|
||||
needAppearances = true;
|
||||
appearance = null;
|
||||
}
|
||||
|
||||
const { xref } = evaluator;
|
||||
|
||||
const dict = xref.fetchIfRef(this.ref);
|
||||
if (!(dict instanceof Dict)) {
|
||||
const originalDict = xref.fetchIfRef(this.ref);
|
||||
if (!(originalDict instanceof Dict)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bbox = [
|
||||
0,
|
||||
0,
|
||||
this.data.rect[2] - this.data.rect[0],
|
||||
this.data.rect[3] - this.data.rect[1],
|
||||
];
|
||||
const dict = new Dict(xref);
|
||||
for (const key of originalDict.getKeys()) {
|
||||
if (key !== "AP") {
|
||||
dict.set(key, originalDict.getRaw(key));
|
||||
}
|
||||
}
|
||||
|
||||
const xfa = {
|
||||
path: stringToPDFString(dict.get("T") || ""),
|
||||
value,
|
||||
};
|
||||
|
||||
const newRef = xref.getNewRef();
|
||||
const AP = new Dict(xref);
|
||||
AP.set("N", newRef);
|
||||
|
||||
const encrypt = xref.encrypt;
|
||||
let originalTransform = null;
|
||||
let newTransform = null;
|
||||
if (encrypt) {
|
||||
originalTransform = encrypt.createCipherTransform(
|
||||
this.ref.num,
|
||||
this.ref.gen
|
||||
);
|
||||
newTransform = encrypt.createCipherTransform(newRef.num, newRef.gen);
|
||||
appearance = newTransform.encryptString(appearance);
|
||||
}
|
||||
|
||||
const encoder = val => (isAscii(val) ? val : stringToUTF16BEString(val));
|
||||
dict.set("V", Array.isArray(value) ? value.map(encoder) : encoder(value));
|
||||
dict.set("AP", AP);
|
||||
dict.set("M", `D:${getModificationDate()}`);
|
||||
|
||||
const maybeMK = this._getMKDict(rotation);
|
||||
if (maybeMK) {
|
||||
dict.set("MK", maybeMK);
|
||||
}
|
||||
|
||||
const appearanceDict = new Dict(xref);
|
||||
appearanceDict.set("Length", appearance.length);
|
||||
appearanceDict.set("Subtype", Name.get("Form"));
|
||||
appearanceDict.set("Resources", this._getSaveFieldResources(xref));
|
||||
appearanceDict.set("BBox", bbox);
|
||||
const encrypt = xref.encrypt;
|
||||
const originalTransform = encrypt
|
||||
? encrypt.createCipherTransform(this.ref.num, this.ref.gen)
|
||||
: null;
|
||||
|
||||
const rotationMatrix = this.getRotationMatrix(annotationStorage);
|
||||
if (rotationMatrix !== IDENTITY_MATRIX) {
|
||||
// The matrix isn't the identity one.
|
||||
appearanceDict.set("Matrix", rotationMatrix);
|
||||
}
|
||||
|
||||
const bufferOriginal = [`${this.ref.num} ${this.ref.gen} obj\n`];
|
||||
writeDict(dict, bufferOriginal, originalTransform);
|
||||
bufferOriginal.push("\nendobj\n");
|
||||
|
||||
const bufferNew = [`${newRef.num} ${newRef.gen} obj\n`];
|
||||
writeDict(appearanceDict, bufferNew, newTransform);
|
||||
bufferNew.push(" stream\n", appearance, "\nendstream\nendobj\n");
|
||||
|
||||
return [
|
||||
const buffer = [];
|
||||
const changes = [
|
||||
// data for the original object
|
||||
// V field changed + reference for new AP
|
||||
{ ref: this.ref, data: bufferOriginal.join(""), xfa },
|
||||
// data for the new AP
|
||||
{ ref: newRef, data: bufferNew.join(""), xfa: null },
|
||||
{ ref: this.ref, data: "", xfa, needAppearances },
|
||||
];
|
||||
if (appearance !== null) {
|
||||
const newRef = xref.getNewTemporaryRef();
|
||||
const AP = new Dict(xref);
|
||||
dict.set("AP", AP);
|
||||
AP.set("N", newRef);
|
||||
|
||||
let newTransform = null;
|
||||
if (encrypt) {
|
||||
newTransform = encrypt.createCipherTransform(newRef.num, newRef.gen);
|
||||
appearance = newTransform.encryptString(appearance);
|
||||
}
|
||||
|
||||
const resources = this._getSaveFieldResources(xref);
|
||||
const appearanceStream = new StringStream(appearance);
|
||||
const appearanceDict = (appearanceStream.dict = new Dict(xref));
|
||||
appearanceDict.set("Length", appearance.length);
|
||||
appearanceDict.set("Subtype", Name.get("Form"));
|
||||
appearanceDict.set("Resources", resources);
|
||||
appearanceDict.set("BBox", [
|
||||
0,
|
||||
0,
|
||||
this.data.rect[2] - this.data.rect[0],
|
||||
this.data.rect[3] - this.data.rect[1],
|
||||
]);
|
||||
|
||||
const rotationMatrix = this.getRotationMatrix(annotationStorage);
|
||||
if (rotationMatrix !== IDENTITY_MATRIX) {
|
||||
// The matrix isn't the identity one.
|
||||
appearanceDict.set("Matrix", rotationMatrix);
|
||||
}
|
||||
|
||||
writeObject(newRef, appearanceStream, buffer, newTransform);
|
||||
|
||||
changes.push(
|
||||
// data for the new AP
|
||||
{
|
||||
ref: newRef,
|
||||
data: buffer.join(""),
|
||||
xfa: null,
|
||||
needAppearances: false,
|
||||
}
|
||||
);
|
||||
buffer.length = 0;
|
||||
}
|
||||
|
||||
dict.set("M", `D:${getModificationDate()}`);
|
||||
writeObject(this.ref, dict, buffer, originalTransform);
|
||||
|
||||
changes[0].data = buffer.join("");
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
async _getAppearance(evaluator, task, annotationStorage) {
|
||||
async _getAppearance(evaluator, task, intent, annotationStorage) {
|
||||
const isPassword = this.hasFieldFlag(AnnotationFieldFlag.PASSWORD);
|
||||
if (isPassword) {
|
||||
return null;
|
||||
@ -1961,12 +2006,30 @@ class WidgetAnnotation extends Annotation {
|
||||
}
|
||||
|
||||
let lineCount = -1;
|
||||
let lines;
|
||||
|
||||
// We could have a text containing for example some sequences of chars and
|
||||
// their diacritics (e.g. "é".normalize("NFKD") shows 1 char when it's 2).
|
||||
// Positioning diacritics is really something we don't want to do here.
|
||||
// So if a font has a glyph for a acute accent and one for "e" then we won't
|
||||
// get any encoding issues but we'll render "e" and then "´".
|
||||
// It's why we normalize the string. We use NFC to preserve the initial
|
||||
// string, (e.g. "²".normalize("NFC") === "²"
|
||||
// but "²".normalize("NFKC") === "2").
|
||||
//
|
||||
// TODO: it isn't a perfect solution, some chars like "ẹ́" will be
|
||||
// decomposed into two chars ("ẹ" and "´"), so we should detect such
|
||||
// situations and then use either FakeUnicodeFont or set the
|
||||
// /NeedAppearances flag.
|
||||
if (this.data.multiLine) {
|
||||
lineCount = value.split(/\r\n|\r|\n/).length;
|
||||
lines = value.split(/\r\n?|\n/).map(line => line.normalize("NFC"));
|
||||
lineCount = lines.length;
|
||||
} else {
|
||||
lines = [value.replace(/\r\n?|\n/, "").normalize("NFC")];
|
||||
}
|
||||
|
||||
const defaultPadding = 2;
|
||||
const hPadding = defaultPadding;
|
||||
const defaultPadding = 1;
|
||||
const defaultHPadding = 2;
|
||||
let totalHeight = this.data.rect[3] - this.data.rect[1];
|
||||
let totalWidth = this.data.rect[2] - this.data.rect[0];
|
||||
|
||||
@ -1985,23 +2048,107 @@ class WidgetAnnotation extends Annotation {
|
||||
);
|
||||
}
|
||||
|
||||
const font = await WidgetAnnotation._getFontData(
|
||||
let font = await WidgetAnnotation._getFontData(
|
||||
evaluator,
|
||||
task,
|
||||
this.data.defaultAppearanceData,
|
||||
this._fieldResources.mergedResources
|
||||
);
|
||||
const [defaultAppearance, fontSize] = this._computeFontSize(
|
||||
totalHeight - defaultPadding,
|
||||
totalWidth - 2 * hPadding,
|
||||
value,
|
||||
font,
|
||||
lineCount
|
||||
);
|
||||
|
||||
let defaultAppearance, fontSize, lineHeight;
|
||||
const encodedLines = [];
|
||||
let encodingError = false;
|
||||
for (const line of lines) {
|
||||
const encodedString = font.encodeString(line);
|
||||
if (encodedString.length > 1) {
|
||||
encodingError = true;
|
||||
}
|
||||
encodedLines.push(encodedString.join(""));
|
||||
}
|
||||
|
||||
if (encodingError && intent & RenderingIntentFlag.SAVE) {
|
||||
// We don't have a way to render the field, so we just rely on the
|
||||
// /NeedAppearances trick to let the different sofware correctly render
|
||||
// this pdf.
|
||||
return { needAppearances: true };
|
||||
}
|
||||
|
||||
// We check that the font is able to encode the string.
|
||||
if (encodingError && this._isOffscreenCanvasSupported) {
|
||||
// If it can't then we fallback on fake unicode font (mapped to sans-serif
|
||||
// for the rendering).
|
||||
// It means that a printed form can be rendered differently (it depends on
|
||||
// the sans-serif font) but at least we've something to render.
|
||||
// In an ideal world the associated font should correctly handle the
|
||||
// possible chars but a user can add a smiley or whatever.
|
||||
// We could try to embed a font but it means that we must have access
|
||||
// to the raw font file.
|
||||
const fontFamily = this.data.comb ? "monospace" : "sans-serif";
|
||||
const fakeUnicodeFont = new FakeUnicodeFont(evaluator.xref, fontFamily);
|
||||
const resources = fakeUnicodeFont.createFontResources(lines.join(""));
|
||||
const newFont = resources.getRaw("Font");
|
||||
|
||||
if (this._fieldResources.mergedResources.has("Font")) {
|
||||
const oldFont = this._fieldResources.mergedResources.get("Font");
|
||||
for (const key of newFont.getKeys()) {
|
||||
oldFont.set(key, newFont.getRaw(key));
|
||||
}
|
||||
} else {
|
||||
this._fieldResources.mergedResources.set("Font", newFont);
|
||||
}
|
||||
|
||||
const fontName = fakeUnicodeFont.fontName.name;
|
||||
font = await WidgetAnnotation._getFontData(
|
||||
evaluator,
|
||||
task,
|
||||
{ fontName, fontSize: 0 },
|
||||
resources
|
||||
);
|
||||
|
||||
for (let i = 0, ii = encodedLines.length; i < ii; i++) {
|
||||
encodedLines[i] = stringToUTF16String(lines[i]);
|
||||
}
|
||||
|
||||
const savedDefaultAppearance = Object.assign(
|
||||
Object.create(null),
|
||||
this.data.defaultAppearanceData
|
||||
);
|
||||
this.data.defaultAppearanceData.fontSize = 0;
|
||||
this.data.defaultAppearanceData.fontName = fontName;
|
||||
|
||||
[defaultAppearance, fontSize, lineHeight] = this._computeFontSize(
|
||||
totalHeight - 2 * defaultPadding,
|
||||
totalWidth - 2 * defaultHPadding,
|
||||
value,
|
||||
font,
|
||||
lineCount
|
||||
);
|
||||
|
||||
this.data.defaultAppearanceData = savedDefaultAppearance;
|
||||
} else {
|
||||
if (!this._isOffscreenCanvasSupported) {
|
||||
warn(
|
||||
"_getAppearance: OffscreenCanvas is not supported, annotation may not render correctly."
|
||||
);
|
||||
}
|
||||
|
||||
[defaultAppearance, fontSize, lineHeight] = this._computeFontSize(
|
||||
totalHeight - 2 * defaultPadding,
|
||||
totalWidth - 2 * defaultHPadding,
|
||||
value,
|
||||
font,
|
||||
lineCount
|
||||
);
|
||||
}
|
||||
|
||||
let descent = font.descent;
|
||||
if (isNaN(descent)) {
|
||||
descent = 0;
|
||||
descent = BASELINE_FACTOR * lineHeight;
|
||||
} else {
|
||||
descent = Math.max(
|
||||
BASELINE_FACTOR * lineHeight,
|
||||
Math.abs(descent) * fontSize
|
||||
);
|
||||
}
|
||||
|
||||
// Take into account the space we have to compute the default vertical
|
||||
@ -2010,59 +2157,64 @@ class WidgetAnnotation extends Annotation {
|
||||
Math.floor((totalHeight - fontSize) / 2),
|
||||
defaultPadding
|
||||
);
|
||||
const vPadding = defaultVPadding + Math.abs(descent) * fontSize;
|
||||
const alignment = this.data.textAlignment;
|
||||
|
||||
if (this.data.multiLine) {
|
||||
return this._getMultilineAppearance(
|
||||
defaultAppearance,
|
||||
value,
|
||||
encodedLines,
|
||||
font,
|
||||
fontSize,
|
||||
totalWidth,
|
||||
totalHeight,
|
||||
alignment,
|
||||
hPadding,
|
||||
vPadding,
|
||||
defaultHPadding,
|
||||
defaultVPadding,
|
||||
descent,
|
||||
lineHeight,
|
||||
annotationStorage
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: need to handle chars which are not in the font.
|
||||
const encodedString = font.encodeString(value).join("");
|
||||
|
||||
if (this.data.comb) {
|
||||
return this._getCombAppearance(
|
||||
defaultAppearance,
|
||||
font,
|
||||
encodedString,
|
||||
encodedLines[0],
|
||||
fontSize,
|
||||
totalWidth,
|
||||
hPadding,
|
||||
vPadding,
|
||||
totalHeight,
|
||||
defaultHPadding,
|
||||
defaultVPadding,
|
||||
descent,
|
||||
lineHeight,
|
||||
annotationStorage
|
||||
);
|
||||
}
|
||||
|
||||
const bottomPadding = defaultVPadding + descent;
|
||||
if (alignment === 0 || alignment > 2) {
|
||||
// Left alignment: nothing to do
|
||||
return (
|
||||
`/Tx BMC q ${colors}BT ` +
|
||||
defaultAppearance +
|
||||
` 1 0 0 1 ${hPadding} ${vPadding} Tm (${escapeString(
|
||||
encodedString
|
||||
)}) Tj` +
|
||||
` 1 0 0 1 ${numberToString(defaultHPadding)} ${numberToString(
|
||||
bottomPadding
|
||||
)} Tm (${escapeString(encodedLines[0])}) Tj` +
|
||||
" ET Q EMC"
|
||||
);
|
||||
}
|
||||
|
||||
const prevInfo = { shift: 0 };
|
||||
const renderedText = this._renderText(
|
||||
encodedString,
|
||||
encodedLines[0],
|
||||
font,
|
||||
fontSize,
|
||||
totalWidth,
|
||||
alignment,
|
||||
hPadding,
|
||||
vPadding
|
||||
prevInfo,
|
||||
defaultHPadding,
|
||||
bottomPadding
|
||||
);
|
||||
return (
|
||||
`/Tx BMC q ${colors}BT ` +
|
||||
@ -2105,6 +2257,9 @@ class WidgetAnnotation extends Annotation {
|
||||
|
||||
_computeFontSize(height, width, text, font, lineCount) {
|
||||
let { fontSize } = this.data.defaultAppearanceData;
|
||||
let lineHeight = (fontSize || 12) * LINE_FACTOR,
|
||||
numberOfLines = Math.round(height / lineHeight);
|
||||
|
||||
if (!fontSize) {
|
||||
// A zero value for size means that the font shall be auto-sized:
|
||||
// its size shall be computed as a function of the height of the
|
||||
@ -2115,8 +2270,12 @@ class WidgetAnnotation extends Annotation {
|
||||
if (lineCount === -1) {
|
||||
const textWidth = this._getTextWidth(text, font);
|
||||
fontSize = roundWithTwoDigits(
|
||||
Math.min(height / LINE_FACTOR, width / textWidth)
|
||||
Math.min(
|
||||
height / LINE_FACTOR,
|
||||
textWidth > width ? width / textWidth : Infinity
|
||||
)
|
||||
);
|
||||
numberOfLines = 1;
|
||||
} else {
|
||||
const lines = text.split(/\r\n?|\n/);
|
||||
const cachedLines = [];
|
||||
@ -2152,9 +2311,6 @@ class WidgetAnnotation extends Annotation {
|
||||
// a font size equal to 12 (this is the default font size in
|
||||
// Acrobat).
|
||||
// Then we'll adjust font size to what we have really.
|
||||
fontSize = 12;
|
||||
let lineHeight = fontSize * LINE_FACTOR;
|
||||
let numberOfLines = Math.round(height / lineHeight);
|
||||
numberOfLines = Math.max(numberOfLines, lineCount);
|
||||
|
||||
while (true) {
|
||||
@ -2177,10 +2333,24 @@ class WidgetAnnotation extends Annotation {
|
||||
fontColor,
|
||||
});
|
||||
}
|
||||
return [this._defaultAppearance, fontSize];
|
||||
|
||||
return [this._defaultAppearance, fontSize, height / numberOfLines];
|
||||
}
|
||||
|
||||
_renderText(text, font, fontSize, totalWidth, alignment, hPadding, vPadding) {
|
||||
_renderText(
|
||||
text,
|
||||
font,
|
||||
fontSize,
|
||||
totalWidth,
|
||||
alignment,
|
||||
prevInfo,
|
||||
hPadding,
|
||||
vPadding
|
||||
) {
|
||||
// TODO: we need to take into account (if possible) how the text
|
||||
// is rendered. For example in arabic, the cumulated width of some
|
||||
// glyphs isn't equal to the width of the rendered glyphs because
|
||||
// of ligatures.
|
||||
let shift;
|
||||
if (alignment === 1) {
|
||||
// Center
|
||||
@ -2193,10 +2363,11 @@ class WidgetAnnotation extends Annotation {
|
||||
} else {
|
||||
shift = hPadding;
|
||||
}
|
||||
shift = numberToString(shift);
|
||||
const shiftStr = numberToString(shift - prevInfo.shift);
|
||||
prevInfo.shift = shift;
|
||||
vPadding = numberToString(vPadding);
|
||||
|
||||
return `${shift} ${vPadding} Td (${escapeString(text)}) Tj`;
|
||||
return `${shiftStr} ${vPadding} Td (${escapeString(text)}) Tj`;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2296,32 +2467,39 @@ class TextWidgetAnnotation extends WidgetAnnotation {
|
||||
defaultAppearance,
|
||||
font,
|
||||
text,
|
||||
fontSize,
|
||||
width,
|
||||
height,
|
||||
hPadding,
|
||||
vPadding,
|
||||
descent,
|
||||
lineHeight,
|
||||
annotationStorage
|
||||
) {
|
||||
const combWidth = numberToString(width / this.data.maxLen);
|
||||
const combWidth = width / this.data.maxLen;
|
||||
// Empty or it has a trailing whitespace.
|
||||
const colors = this.getBorderAndBackgroundAppearances(annotationStorage);
|
||||
|
||||
const buf = [];
|
||||
const positions = font.getCharPositions(text);
|
||||
for (const [start, end] of positions) {
|
||||
buf.push(`(${escapeString(text.substring(start, end))}) Tj`);
|
||||
}
|
||||
|
||||
// Empty or it has a trailing whitespace.
|
||||
const colors = this.getBorderAndBackgroundAppearances(annotationStorage);
|
||||
const renderedComb = buf.join(` ${combWidth} 0 Td `);
|
||||
const renderedComb = buf.join(` ${numberToString(combWidth)} 0 Td `);
|
||||
return (
|
||||
`/Tx BMC q ${colors}BT ` +
|
||||
defaultAppearance +
|
||||
` 1 0 0 1 ${hPadding} ${vPadding} Tm ${renderedComb}` +
|
||||
` 1 0 0 1 ${numberToString(hPadding)} ${numberToString(
|
||||
vPadding + descent
|
||||
)} Tm ${renderedComb}` +
|
||||
" ET Q EMC"
|
||||
);
|
||||
}
|
||||
|
||||
_getMultilineAppearance(
|
||||
defaultAppearance,
|
||||
text,
|
||||
lines,
|
||||
font,
|
||||
fontSize,
|
||||
width,
|
||||
@ -2329,15 +2507,20 @@ class TextWidgetAnnotation extends WidgetAnnotation {
|
||||
alignment,
|
||||
hPadding,
|
||||
vPadding,
|
||||
descent,
|
||||
lineHeight,
|
||||
annotationStorage
|
||||
) {
|
||||
const lines = text.split(/\r\n?|\n/);
|
||||
const buf = [];
|
||||
const totalWidth = width - 2 * hPadding;
|
||||
for (const line of lines) {
|
||||
const prevInfo = { shift: 0 };
|
||||
for (let i = 0, ii = lines.length; i < ii; i++) {
|
||||
const line = lines[i];
|
||||
const chunks = this._splitLine(line, font, fontSize, totalWidth);
|
||||
for (const chunk of chunks) {
|
||||
const padding = buf.length === 0 ? hPadding : 0;
|
||||
for (let j = 0, jj = chunks.length; j < jj; j++) {
|
||||
const chunk = chunks[j];
|
||||
const vShift =
|
||||
i === 0 && j === 0 ? -vPadding - (lineHeight - descent) : -lineHeight;
|
||||
buf.push(
|
||||
this._renderText(
|
||||
chunk,
|
||||
@ -2345,29 +2528,28 @@ class TextWidgetAnnotation extends WidgetAnnotation {
|
||||
fontSize,
|
||||
width,
|
||||
alignment,
|
||||
padding,
|
||||
-fontSize // <0 because a line is below the previous one
|
||||
prevInfo,
|
||||
hPadding,
|
||||
vShift
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const renderedText = buf.join("\n");
|
||||
|
||||
// Empty or it has a trailing whitespace.
|
||||
const colors = this.getBorderAndBackgroundAppearances(annotationStorage);
|
||||
const renderedText = buf.join("\n");
|
||||
|
||||
return (
|
||||
`/Tx BMC q ${colors}BT ` +
|
||||
defaultAppearance +
|
||||
` 1 0 0 1 0 ${height} Tm ${renderedText}` +
|
||||
` 1 0 0 1 0 ${numberToString(height)} Tm ${renderedText}` +
|
||||
" ET Q EMC"
|
||||
);
|
||||
}
|
||||
|
||||
_splitLine(line, font, fontSize, width, cache = {}) {
|
||||
// TODO: need to handle chars which are not in the font.
|
||||
line = cache.line || font.encodeString(line).join("");
|
||||
line = cache.line || line;
|
||||
|
||||
const glyphs = cache.glyphs || font.charsToGlyphs(line);
|
||||
|
||||
@ -3031,9 +3213,9 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation {
|
||||
};
|
||||
}
|
||||
|
||||
async _getAppearance(evaluator, task, annotationStorage) {
|
||||
async _getAppearance(evaluator, task, intent, annotationStorage) {
|
||||
if (this.data.combo) {
|
||||
return super._getAppearance(evaluator, task, annotationStorage);
|
||||
return super._getAppearance(evaluator, task, intent, annotationStorage);
|
||||
}
|
||||
|
||||
let exportedValue, rotation;
|
||||
@ -3061,8 +3243,8 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation {
|
||||
exportedValue = [exportedValue];
|
||||
}
|
||||
|
||||
const defaultPadding = 2;
|
||||
const hPadding = defaultPadding;
|
||||
const defaultPadding = 1;
|
||||
const defaultHPadding = 2;
|
||||
let totalHeight = this.data.rect[3] - this.data.rect[1];
|
||||
let totalWidth = this.data.rect[2] - this.data.rect[0];
|
||||
|
||||
@ -3113,7 +3295,7 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation {
|
||||
|
||||
[defaultAppearance, fontSize] = this._computeFontSize(
|
||||
lineHeight,
|
||||
totalWidth - 2 * hPadding,
|
||||
totalWidth - 2 * defaultHPadding,
|
||||
value,
|
||||
font,
|
||||
-1
|
||||
@ -3159,9 +3341,9 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation {
|
||||
}
|
||||
buf.push("BT", defaultAppearance, `1 0 0 1 0 ${totalHeight} Tm`);
|
||||
|
||||
const prevInfo = { shift: 0 };
|
||||
for (let i = firstIndex; i < end; i++) {
|
||||
const { displayValue } = this.data.options[i];
|
||||
const hpadding = i === firstIndex ? hPadding : 0;
|
||||
const vpadding = i === firstIndex ? vPadding : 0;
|
||||
buf.push(
|
||||
this._renderText(
|
||||
@ -3170,7 +3352,8 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation {
|
||||
fontSize,
|
||||
totalWidth,
|
||||
0,
|
||||
hpadding,
|
||||
prevInfo,
|
||||
defaultHPadding,
|
||||
-lineHeight + vpadding
|
||||
)
|
||||
);
|
||||
@ -3326,6 +3509,26 @@ class FreeTextAnnotation extends MarkupAnnotation {
|
||||
super(parameters);
|
||||
|
||||
this.data.annotationType = AnnotationType.FREETEXT;
|
||||
this.setDefaultAppearance(parameters);
|
||||
if (!this.appearance && this._isOffscreenCanvasSupported) {
|
||||
const fakeUnicodeFont = new FakeUnicodeFont(
|
||||
parameters.xref,
|
||||
"sans-serif"
|
||||
);
|
||||
const fontData = this.data.defaultAppearanceData;
|
||||
this.appearance = fakeUnicodeFont.createAppearance(
|
||||
this._contents.str,
|
||||
this.rectangle,
|
||||
this.rotation,
|
||||
fontData.fontSize || 10,
|
||||
fontData.fontColor
|
||||
);
|
||||
this._streams.push(this.appearance, FakeUnicodeFont.toUnicodeStream);
|
||||
} else if (!this._isOffscreenCanvasSupported) {
|
||||
warn(
|
||||
"FreeTextAnnotation: OffscreenCanvas is not supported, annotation may not render correctly."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get hasTextContent() {
|
||||
@ -3341,22 +3544,27 @@ class FreeTextAnnotation extends MarkupAnnotation {
|
||||
freetext.set("Rect", rect);
|
||||
const da = `/Helv ${fontSize} Tf ${getPdfColor(color, /* isFill */ true)}`;
|
||||
freetext.set("DA", da);
|
||||
freetext.set("Contents", value);
|
||||
freetext.set(
|
||||
"Contents",
|
||||
isAscii(value) ? value : stringToUTF16BEString(value)
|
||||
);
|
||||
freetext.set("F", 4);
|
||||
freetext.set("Border", [0, 0, 0]);
|
||||
freetext.set("Rotate", rotation);
|
||||
|
||||
if (user) {
|
||||
freetext.set("T", stringToUTF8String(user));
|
||||
freetext.set("T", isAscii(user) ? user : stringToUTF16BEString(user));
|
||||
}
|
||||
|
||||
const n = new Dict(xref);
|
||||
freetext.set("AP", n);
|
||||
if (apRef || ap) {
|
||||
const n = new Dict(xref);
|
||||
freetext.set("AP", n);
|
||||
|
||||
if (apRef) {
|
||||
n.set("N", apRef);
|
||||
} else {
|
||||
n.set("N", ap);
|
||||
if (apRef) {
|
||||
n.set("N", apRef);
|
||||
} else {
|
||||
n.set("N", ap);
|
||||
}
|
||||
}
|
||||
|
||||
return freetext;
|
||||
@ -3404,7 +3612,12 @@ class FreeTextAnnotation extends MarkupAnnotation {
|
||||
let totalWidth = -Infinity;
|
||||
const encodedLines = [];
|
||||
for (let line of lines) {
|
||||
line = helv.encodeString(line).join("");
|
||||
const encoded = helv.encodeString(line);
|
||||
if (encoded.length > 1) {
|
||||
// The font doesn't contain all the chars.
|
||||
return null;
|
||||
}
|
||||
line = encoded.join("");
|
||||
encodedLines.push(line);
|
||||
let lineWidth = 0;
|
||||
const glyphs = helv.charsToGlyphs(line);
|
||||
@ -3454,7 +3667,7 @@ class FreeTextAnnotation extends MarkupAnnotation {
|
||||
appearanceStreamDict.set("Resources", resources);
|
||||
|
||||
if (rotation) {
|
||||
const matrix = WidgetAnnotation._getRotationMatrix(rotation, w, h);
|
||||
const matrix = getRotationMatrix(rotation, w, h);
|
||||
appearanceStreamDict.set("Matrix", matrix);
|
||||
}
|
||||
|
||||
@ -3897,7 +4110,7 @@ class InkAnnotation extends MarkupAnnotation {
|
||||
appearanceStreamDict.set("Length", appearance.length);
|
||||
|
||||
if (rotation) {
|
||||
const matrix = WidgetAnnotation._getRotationMatrix(rotation, w, h);
|
||||
const matrix = getRotationMatrix(rotation, w, h);
|
||||
appearanceStreamDict.set("Matrix", matrix);
|
||||
}
|
||||
|
||||
|
@ -572,6 +572,43 @@ function getNewAnnotationsMap(annotationStorage) {
|
||||
return newAnnotationsByPage.size > 0 ? newAnnotationsByPage : null;
|
||||
}
|
||||
|
||||
function stringToUTF16HexString(str) {
|
||||
const buf = [];
|
||||
for (let i = 0, ii = str.length; i < ii; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
buf.push(
|
||||
((char >> 8) & 0xff).toString(16).padStart(2, "0"),
|
||||
(char & 0xff).toString(16).padStart(2, "0")
|
||||
);
|
||||
}
|
||||
return buf.join("");
|
||||
}
|
||||
|
||||
function stringToUTF16String(str) {
|
||||
const buf = [];
|
||||
for (let i = 0, ii = str.length; i < ii; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
buf.push(
|
||||
String.fromCharCode((char >> 8) & 0xff),
|
||||
String.fromCharCode(char & 0xff)
|
||||
);
|
||||
}
|
||||
return buf.join("");
|
||||
}
|
||||
|
||||
function getRotationMatrix(rotation, width, height) {
|
||||
switch (rotation) {
|
||||
case 90:
|
||||
return [0, 1, -1, 0, width, 0];
|
||||
case 180:
|
||||
return [-1, 0, 0, -1, width, height];
|
||||
case 270:
|
||||
return [0, -1, 1, 0, 0, height];
|
||||
default:
|
||||
throw new Error("Invalid rotation");
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
collectActions,
|
||||
DocStats,
|
||||
@ -581,6 +618,7 @@ export {
|
||||
getInheritableProperty,
|
||||
getLookupTableFactory,
|
||||
getNewAnnotationsMap,
|
||||
getRotationMatrix,
|
||||
isWhiteSpace,
|
||||
log2,
|
||||
MissingDataException,
|
||||
@ -592,6 +630,8 @@ export {
|
||||
readUint16,
|
||||
readUint32,
|
||||
recoverJsURL,
|
||||
stringToUTF16HexString,
|
||||
stringToUTF16String,
|
||||
toRomanNumerals,
|
||||
validateCSSFont,
|
||||
XRefEntryException,
|
||||
|
@ -13,11 +13,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { escapePDFName, numberToString } from "./core_utils.js";
|
||||
import { OPS, warn } from "../shared/util.js";
|
||||
import { Dict, Name } from "./primitives.js";
|
||||
import {
|
||||
escapePDFName,
|
||||
getRotationMatrix,
|
||||
numberToString,
|
||||
stringToUTF16HexString,
|
||||
} from "./core_utils.js";
|
||||
import { LINE_DESCENT_FACTOR, LINE_FACTOR, OPS, warn } from "../shared/util.js";
|
||||
import { ColorSpace } from "./colorspace.js";
|
||||
import { EvaluatorPreprocessor } from "./evaluator.js";
|
||||
import { Name } from "./primitives.js";
|
||||
import { StringStream } from "./stream.js";
|
||||
|
||||
class DefaultAppearanceEvaluator extends EvaluatorPreprocessor {
|
||||
@ -101,4 +106,250 @@ function createDefaultAppearance({ fontSize, fontName, fontColor }) {
|
||||
)}`;
|
||||
}
|
||||
|
||||
export { createDefaultAppearance, getPdfColor, parseDefaultAppearance };
|
||||
class FakeUnicodeFont {
|
||||
constructor(xref, fontFamily) {
|
||||
this.xref = xref;
|
||||
this.widths = null;
|
||||
this.firstChar = Infinity;
|
||||
this.lastChar = -Infinity;
|
||||
this.fontFamily = fontFamily;
|
||||
|
||||
const canvas = new OffscreenCanvas(1, 1);
|
||||
this.ctxMeasure = canvas.getContext("2d");
|
||||
|
||||
if (!FakeUnicodeFont._fontNameId) {
|
||||
FakeUnicodeFont._fontNameId = 1;
|
||||
}
|
||||
this.fontName = Name.get(
|
||||
`InvalidPDFjsFont_${fontFamily}_${FakeUnicodeFont._fontNameId++}`
|
||||
);
|
||||
}
|
||||
|
||||
get toUnicodeRef() {
|
||||
if (!FakeUnicodeFont._toUnicodeRef) {
|
||||
const toUnicode = `/CIDInit /ProcSet findresource begin
|
||||
12 dict begin
|
||||
begincmap
|
||||
/CIDSystemInfo
|
||||
<< /Registry (Adobe)
|
||||
/Ordering (UCS) /Supplement 0 >> def
|
||||
/CMapName /Adobe-Identity-UCS def
|
||||
/CMapType 2 def
|
||||
1 begincodespacerange
|
||||
<0000> <FFFF>
|
||||
endcodespacerange
|
||||
1 beginbfrange
|
||||
<0000> <FFFF> <0000>
|
||||
endbfrange
|
||||
endcmap CMapName currentdict /CMap defineresource pop end end`;
|
||||
const toUnicodeStream = (FakeUnicodeFont.toUnicodeStream =
|
||||
new StringStream(toUnicode));
|
||||
const toUnicodeDict = new Dict(this.xref);
|
||||
toUnicodeStream.dict = toUnicodeDict;
|
||||
toUnicodeDict.set("Length", toUnicode.length);
|
||||
FakeUnicodeFont._toUnicodeRef =
|
||||
this.xref.getNewPersistentRef(toUnicodeStream);
|
||||
}
|
||||
|
||||
return FakeUnicodeFont._toUnicodeRef;
|
||||
}
|
||||
|
||||
get fontDescriptorRef() {
|
||||
if (!FakeUnicodeFont._fontDescriptorRef) {
|
||||
const fontDescriptor = new Dict(this.xref);
|
||||
fontDescriptor.set("Type", Name.get("FontDescriptor"));
|
||||
fontDescriptor.set("FontName", this.fontName);
|
||||
fontDescriptor.set("FontFamily", "MyriadPro Regular");
|
||||
fontDescriptor.set("FontBBox", [0, 0, 0, 0]);
|
||||
fontDescriptor.set("FontStretch", Name.get("Normal"));
|
||||
fontDescriptor.set("FontWeight", 400);
|
||||
fontDescriptor.set("ItalicAngle", 0);
|
||||
|
||||
FakeUnicodeFont._fontDescriptorRef =
|
||||
this.xref.getNewPersistentRef(fontDescriptor);
|
||||
}
|
||||
|
||||
return FakeUnicodeFont._fontDescriptorRef;
|
||||
}
|
||||
|
||||
get descendantFontRef() {
|
||||
const descendantFont = new Dict(this.xref);
|
||||
descendantFont.set("BaseFont", this.fontName);
|
||||
descendantFont.set("Type", Name.get("Font"));
|
||||
descendantFont.set("Subtype", Name.get("CIDFontType0"));
|
||||
descendantFont.set("CIDToGIDMap", Name.get("Identity"));
|
||||
descendantFont.set("FirstChar", this.firstChar);
|
||||
descendantFont.set("LastChar", this.lastChar);
|
||||
descendantFont.set("FontDescriptor", this.fontDescriptorRef);
|
||||
descendantFont.set("DW", 1000);
|
||||
|
||||
const widths = [];
|
||||
const chars = [...this.widths.entries()].sort();
|
||||
let currentChar = null;
|
||||
let currentWidths = null;
|
||||
for (const [char, width] of chars) {
|
||||
if (!currentChar) {
|
||||
currentChar = char;
|
||||
currentWidths = [width];
|
||||
continue;
|
||||
}
|
||||
if (char === currentChar + currentWidths.length) {
|
||||
currentWidths.push(width);
|
||||
} else {
|
||||
widths.push(currentChar, currentWidths);
|
||||
currentChar = char;
|
||||
currentWidths = [width];
|
||||
}
|
||||
}
|
||||
|
||||
if (currentChar) {
|
||||
widths.push(currentChar, currentWidths);
|
||||
}
|
||||
|
||||
descendantFont.set("W", widths);
|
||||
|
||||
const cidSystemInfo = new Dict(this.xref);
|
||||
cidSystemInfo.set("Ordering", "Identity");
|
||||
cidSystemInfo.set("Registry", "Adobe");
|
||||
cidSystemInfo.set("Supplement", 0);
|
||||
descendantFont.set("CIDSystemInfo", cidSystemInfo);
|
||||
|
||||
return this.xref.getNewPersistentRef(descendantFont);
|
||||
}
|
||||
|
||||
get baseFontRef() {
|
||||
const baseFont = new Dict(this.xref);
|
||||
baseFont.set("BaseFont", this.fontName);
|
||||
baseFont.set("Type", Name.get("Font"));
|
||||
baseFont.set("Subtype", Name.get("Type0"));
|
||||
baseFont.set("Encoding", Name.get("Identity-H"));
|
||||
baseFont.set("DescendantFonts", [this.descendantFontRef]);
|
||||
baseFont.set("ToUnicode", this.toUnicodeRef);
|
||||
|
||||
return this.xref.getNewPersistentRef(baseFont);
|
||||
}
|
||||
|
||||
get resources() {
|
||||
const resources = new Dict(this.xref);
|
||||
const font = new Dict(this.xref);
|
||||
font.set(this.fontName.name, this.baseFontRef);
|
||||
resources.set("Font", font);
|
||||
|
||||
return resources;
|
||||
}
|
||||
|
||||
_createContext() {
|
||||
this.widths = new Map();
|
||||
this.ctxMeasure.font = `1000px ${this.fontFamily}`;
|
||||
|
||||
return this.ctxMeasure;
|
||||
}
|
||||
|
||||
createFontResources(text) {
|
||||
const ctx = this._createContext();
|
||||
for (const line of text.split(/\r\n?|\n/)) {
|
||||
for (const char of line.split("")) {
|
||||
const code = char.charCodeAt(0);
|
||||
if (this.widths.has(code)) {
|
||||
continue;
|
||||
}
|
||||
const metrics = ctx.measureText(char);
|
||||
const width = Math.ceil(metrics.width);
|
||||
this.widths.set(code, width);
|
||||
this.firstChar = Math.min(code, this.firstChar);
|
||||
this.lastChar = Math.max(code, this.lastChar);
|
||||
}
|
||||
}
|
||||
|
||||
return this.resources;
|
||||
}
|
||||
|
||||
createAppearance(text, rect, rotation, fontSize, bgColor) {
|
||||
const ctx = this._createContext();
|
||||
const lines = [];
|
||||
let maxWidth = -Infinity;
|
||||
for (const line of text.split(/\r\n?|\n/)) {
|
||||
lines.push(line);
|
||||
// The line width isn't the sum of the char widths, because in some
|
||||
// languages, like arabic, it'd be wrong because of ligatures.
|
||||
const lineWidth = ctx.measureText(line).width;
|
||||
maxWidth = Math.max(maxWidth, lineWidth);
|
||||
for (const char of line.split("")) {
|
||||
const code = char.charCodeAt(0);
|
||||
let width = this.widths.get(code);
|
||||
if (width === undefined) {
|
||||
const metrics = ctx.measureText(char);
|
||||
width = Math.ceil(metrics.width);
|
||||
this.widths.set(code, width);
|
||||
this.firstChar = Math.min(code, this.firstChar);
|
||||
this.lastChar = Math.max(code, this.lastChar);
|
||||
}
|
||||
}
|
||||
}
|
||||
maxWidth *= fontSize / 1000;
|
||||
|
||||
const [x1, y1, x2, y2] = rect;
|
||||
let w = x2 - x1;
|
||||
let h = y2 - y1;
|
||||
|
||||
if (rotation % 180 !== 0) {
|
||||
[w, h] = [h, w];
|
||||
}
|
||||
|
||||
let hscale = 1;
|
||||
if (maxWidth > w) {
|
||||
hscale = w / maxWidth;
|
||||
}
|
||||
let vscale = 1;
|
||||
const lineHeight = LINE_FACTOR * fontSize;
|
||||
const lineDescent = LINE_DESCENT_FACTOR * fontSize;
|
||||
const maxHeight = lineHeight * lines.length;
|
||||
if (maxHeight > h) {
|
||||
vscale = h / maxHeight;
|
||||
}
|
||||
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(
|
||||
bgColor,
|
||||
/* isFill */ true
|
||||
)}`,
|
||||
`/${this.fontName.name} ${numberToString(newFontSize)} Tf`,
|
||||
];
|
||||
|
||||
const vShift = numberToString(lineHeight);
|
||||
for (const line of lines) {
|
||||
buffer.push(`0 -${vShift} Td <${stringToUTF16HexString(line)}> Tj`);
|
||||
}
|
||||
buffer.push("ET", "Q");
|
||||
const appearance = buffer.join("\n");
|
||||
|
||||
const appearanceStreamDict = new Dict(this.xref);
|
||||
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", this.resources);
|
||||
|
||||
if (rotation) {
|
||||
const matrix = getRotationMatrix(rotation, w, h);
|
||||
appearanceStreamDict.set("Matrix", matrix);
|
||||
}
|
||||
|
||||
const ap = new StringStream(appearance);
|
||||
ap.dict = appearanceStreamDict;
|
||||
|
||||
return ap;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
createDefaultAppearance,
|
||||
FakeUnicodeFont,
|
||||
getPdfColor,
|
||||
parseDefaultAppearance,
|
||||
};
|
||||
|
@ -93,6 +93,7 @@ const EXPORT_DATA_PROPERTIES = [
|
||||
"fallbackName",
|
||||
"fontMatrix",
|
||||
"fontType",
|
||||
"isInvalidPDFjsFont",
|
||||
"isType3Font",
|
||||
"italic",
|
||||
"loadedName",
|
||||
@ -952,13 +953,17 @@ class Font {
|
||||
this.type = type;
|
||||
this.subtype = subtype;
|
||||
|
||||
let fallbackName = "sans-serif";
|
||||
if (this.isMonospace) {
|
||||
fallbackName = "monospace";
|
||||
const matches = name.match(/^InvalidPDFjsFont_(.*)_\d+$/);
|
||||
this.isInvalidPDFjsFont = !!matches;
|
||||
if (this.isInvalidPDFjsFont) {
|
||||
this.fallbackName = matches[1];
|
||||
} else if (this.isMonospace) {
|
||||
this.fallbackName = "monospace";
|
||||
} else if (this.isSerifFont) {
|
||||
fallbackName = "serif";
|
||||
this.fallbackName = "serif";
|
||||
} else {
|
||||
this.fallbackName = "sans-serif";
|
||||
}
|
||||
this.fallbackName = fallbackName;
|
||||
|
||||
this.differences = properties.differences;
|
||||
this.widths = properties.widths;
|
||||
|
@ -625,6 +625,11 @@ class WorkerMessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
const needAppearances =
|
||||
acroFormRef &&
|
||||
acroForm instanceof Dict &&
|
||||
newRefs.some(ref => ref.needAppearances);
|
||||
|
||||
const xfa = (acroForm instanceof Dict && acroForm.get("XFA")) || null;
|
||||
let xfaDatasetsRef = null;
|
||||
let hasXfaDatasetsEntry = false;
|
||||
@ -632,15 +637,13 @@ class WorkerMessageHandler {
|
||||
for (let i = 0, ii = xfa.length; i < ii; i += 2) {
|
||||
if (xfa[i] === "datasets") {
|
||||
xfaDatasetsRef = xfa[i + 1];
|
||||
acroFormRef = null;
|
||||
hasXfaDatasetsEntry = true;
|
||||
}
|
||||
}
|
||||
if (xfaDatasetsRef === null) {
|
||||
xfaDatasetsRef = xref.getNewRef();
|
||||
xfaDatasetsRef = xref.getNewTemporaryRef();
|
||||
}
|
||||
} else if (xfa) {
|
||||
acroFormRef = null;
|
||||
// TODO: Support XFA streams.
|
||||
warn("Unsupported XFA type.");
|
||||
}
|
||||
@ -661,7 +664,7 @@ class WorkerMessageHandler {
|
||||
newXrefInfo = {
|
||||
rootRef: xref.trailer.getRaw("Root") || null,
|
||||
encryptRef: xref.trailer.getRaw("Encrypt") || null,
|
||||
newRef: xref.getNewRef(),
|
||||
newRef: xref.getNewTemporaryRef(),
|
||||
infoRef: xref.trailer.getRaw("Info") || null,
|
||||
info: infoObj,
|
||||
fileIds: xref.trailer.get("ID") || null,
|
||||
@ -669,20 +672,24 @@ class WorkerMessageHandler {
|
||||
filename,
|
||||
};
|
||||
}
|
||||
xref.resetNewRef();
|
||||
|
||||
return incrementalUpdate({
|
||||
originalData: stream.bytes,
|
||||
xrefInfo: newXrefInfo,
|
||||
newRefs,
|
||||
xref,
|
||||
hasXfa: !!xfa,
|
||||
xfaDatasetsRef,
|
||||
hasXfaDatasetsEntry,
|
||||
acroFormRef,
|
||||
acroForm,
|
||||
xfaData,
|
||||
});
|
||||
try {
|
||||
return incrementalUpdate({
|
||||
originalData: stream.bytes,
|
||||
xrefInfo: newXrefInfo,
|
||||
newRefs,
|
||||
xref,
|
||||
hasXfa: !!xfa,
|
||||
xfaDatasetsRef,
|
||||
hasXfaDatasetsEntry,
|
||||
needAppearances,
|
||||
acroFormRef,
|
||||
acroForm,
|
||||
xfaData,
|
||||
});
|
||||
} finally {
|
||||
xref.resetNewTemporaryRef();
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@ -46,7 +46,7 @@ function writeStream(stream, buffer, transform) {
|
||||
if (transform !== null) {
|
||||
string = transform.encryptString(string);
|
||||
}
|
||||
buffer.push(string, "\nendstream\n");
|
||||
buffer.push(string, "\nendstream");
|
||||
}
|
||||
|
||||
function writeArray(array, buffer, transform) {
|
||||
@ -150,54 +150,58 @@ function writeXFADataForAcroform(str, newRefs) {
|
||||
return buffer.join("");
|
||||
}
|
||||
|
||||
function updateXFA({
|
||||
xfaData,
|
||||
xfaDatasetsRef,
|
||||
hasXfaDatasetsEntry,
|
||||
acroFormRef,
|
||||
acroForm,
|
||||
newRefs,
|
||||
function updateAcroform({
|
||||
xref,
|
||||
xrefInfo,
|
||||
acroForm,
|
||||
acroFormRef,
|
||||
hasXfa,
|
||||
hasXfaDatasetsEntry,
|
||||
xfaDatasetsRef,
|
||||
needAppearances,
|
||||
newRefs,
|
||||
}) {
|
||||
if (xref === null) {
|
||||
if (hasXfa && !hasXfaDatasetsEntry && !xfaDatasetsRef) {
|
||||
warn("XFA - Cannot save it");
|
||||
}
|
||||
|
||||
if (!needAppearances && (!hasXfa || !xfaDatasetsRef)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasXfaDatasetsEntry) {
|
||||
if (!acroFormRef) {
|
||||
warn("XFA - Cannot save it");
|
||||
return;
|
||||
}
|
||||
// Clone the acroForm.
|
||||
const dict = new Dict(xref);
|
||||
for (const key of acroForm.getKeys()) {
|
||||
dict.set(key, acroForm.getRaw(key));
|
||||
}
|
||||
|
||||
if (hasXfa && !hasXfaDatasetsEntry) {
|
||||
// We've a XFA array which doesn't contain a datasets entry.
|
||||
// So we'll update the AcroForm dictionary to have an XFA containing
|
||||
// the datasets.
|
||||
const oldXfa = acroForm.get("XFA");
|
||||
const newXfa = oldXfa.slice();
|
||||
const newXfa = acroForm.get("XFA").slice();
|
||||
newXfa.splice(2, 0, "datasets");
|
||||
newXfa.splice(3, 0, xfaDatasetsRef);
|
||||
|
||||
acroForm.set("XFA", newXfa);
|
||||
|
||||
const encrypt = xref.encrypt;
|
||||
let transform = null;
|
||||
if (encrypt) {
|
||||
transform = encrypt.createCipherTransform(
|
||||
acroFormRef.num,
|
||||
acroFormRef.gen
|
||||
);
|
||||
}
|
||||
|
||||
const buffer = [`${acroFormRef.num} ${acroFormRef.gen} obj\n`];
|
||||
writeDict(acroForm, buffer, transform);
|
||||
buffer.push("\n");
|
||||
|
||||
acroForm.set("XFA", oldXfa);
|
||||
|
||||
newRefs.push({ ref: acroFormRef, data: buffer.join("") });
|
||||
dict.set("XFA", newXfa);
|
||||
}
|
||||
|
||||
if (needAppearances) {
|
||||
dict.set("NeedAppearances", true);
|
||||
}
|
||||
|
||||
const encrypt = xref.encrypt;
|
||||
let transform = null;
|
||||
if (encrypt) {
|
||||
transform = encrypt.createCipherTransform(acroFormRef.num, acroFormRef.gen);
|
||||
}
|
||||
|
||||
const buffer = [];
|
||||
writeObject(acroFormRef, dict, buffer, transform);
|
||||
|
||||
newRefs.push({ ref: acroFormRef, data: buffer.join("") });
|
||||
}
|
||||
|
||||
function updateXFA({ xfaData, xfaDatasetsRef, newRefs, xref }) {
|
||||
if (xfaData === null) {
|
||||
const datasets = xref.fetchIfRef(xfaDatasetsRef);
|
||||
xfaData = writeXFADataForAcroform(datasets.getString(), newRefs);
|
||||
@ -228,20 +232,28 @@ function incrementalUpdate({
|
||||
hasXfa = false,
|
||||
xfaDatasetsRef = null,
|
||||
hasXfaDatasetsEntry = false,
|
||||
needAppearances,
|
||||
acroFormRef = null,
|
||||
acroForm = null,
|
||||
xfaData = null,
|
||||
}) {
|
||||
updateAcroform({
|
||||
xref,
|
||||
acroForm,
|
||||
acroFormRef,
|
||||
hasXfa,
|
||||
hasXfaDatasetsEntry,
|
||||
xfaDatasetsRef,
|
||||
needAppearances,
|
||||
newRefs,
|
||||
});
|
||||
|
||||
if (hasXfa) {
|
||||
updateXFA({
|
||||
xfaData,
|
||||
xfaDatasetsRef,
|
||||
hasXfaDatasetsEntry,
|
||||
acroFormRef,
|
||||
acroForm,
|
||||
newRefs,
|
||||
xref,
|
||||
xrefInfo,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -42,18 +42,34 @@ class XRef {
|
||||
this._cacheMap = new Map(); // Prepare the XRef cache.
|
||||
this._pendingRefs = new RefSet();
|
||||
this.stats = new DocStats(pdfManager.msgHandler);
|
||||
this._newRefNum = null;
|
||||
this._newPersistentRefNum = null;
|
||||
this._newTemporaryRefNum = null;
|
||||
}
|
||||
|
||||
getNewRef() {
|
||||
if (this._newRefNum === null) {
|
||||
this._newRefNum = this.entries.length || 1;
|
||||
getNewPersistentRef(obj) {
|
||||
// When printing we don't care that much about the ref number by itself, it
|
||||
// can increase for ever and it allows to keep some re-usable refs.
|
||||
if (this._newPersistentRefNum === null) {
|
||||
this._newPersistentRefNum = this.entries.length || 1;
|
||||
}
|
||||
return Ref.get(this._newRefNum++, 0);
|
||||
const num = this._newPersistentRefNum++;
|
||||
this._cacheMap.set(num, obj);
|
||||
return Ref.get(num, 0);
|
||||
}
|
||||
|
||||
resetNewRef() {
|
||||
this._newRefNum = null;
|
||||
getNewTemporaryRef() {
|
||||
// When saving we want to have some minimal numbers.
|
||||
// Those refs are only created in order to be written in the final pdf
|
||||
// stream.
|
||||
if (this._newTemporaryRefNum === null) {
|
||||
this._newTemporaryRefNum = this.entries.length || 1;
|
||||
}
|
||||
return Ref.get(this._newTemporaryRefNum++, 0);
|
||||
}
|
||||
|
||||
resetNewTemporaryRef() {
|
||||
// Called once saving is finished.
|
||||
this._newTemporaryRefNum = null;
|
||||
}
|
||||
|
||||
setStartXRef(startXRef) {
|
||||
|
@ -2275,6 +2275,21 @@ class CanvasGraphics {
|
||||
|
||||
ctx.lineWidth = lineWidth;
|
||||
|
||||
if (font.isInvalidPDFjsFont) {
|
||||
const chars = [];
|
||||
let width = 0;
|
||||
for (const glyph of glyphs) {
|
||||
chars.push(glyph.unicode);
|
||||
width += glyph.width;
|
||||
}
|
||||
ctx.fillText(chars.join(""), 0, 0);
|
||||
current.x += width * widthAdvanceScale * textHScale;
|
||||
ctx.restore();
|
||||
this.compose();
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let x = 0,
|
||||
i;
|
||||
for (i = 0; i < glyphsLength; ++i) {
|
||||
|
@ -305,12 +305,7 @@ class FreeTextEditor extends AnnotationEditor {
|
||||
}
|
||||
const buffer = [];
|
||||
for (const div of divs) {
|
||||
const first = div.firstChild;
|
||||
if (first?.nodeName === "#text") {
|
||||
buffer.push(first.data);
|
||||
} else {
|
||||
buffer.push("");
|
||||
}
|
||||
buffer.push(div.innerText.replace(/\r\n?|\n/, ""));
|
||||
}
|
||||
return buffer.join("\n");
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ const FONT_IDENTITY_MATRIX = [0.001, 0, 0, 0.001, 0, 0];
|
||||
// the font size. Acrobat seems to use this value.
|
||||
const LINE_FACTOR = 1.35;
|
||||
const LINE_DESCENT_FACTOR = 0.35;
|
||||
const BASELINE_FACTOR = LINE_DESCENT_FACTOR / LINE_FACTOR;
|
||||
|
||||
/**
|
||||
* Refer to the `WorkerTransport.getRenderingIntent`-method in the API, to see
|
||||
@ -47,6 +48,7 @@ const RenderingIntentFlag = {
|
||||
ANY: 0x01,
|
||||
DISPLAY: 0x02,
|
||||
PRINT: 0x04,
|
||||
SAVE: 0x08,
|
||||
ANNOTATIONS_FORMS: 0x10,
|
||||
ANNOTATIONS_STORAGE: 0x20,
|
||||
ANNOTATIONS_DISABLE: 0x40,
|
||||
@ -1159,6 +1161,7 @@ export {
|
||||
arraysToBytes,
|
||||
assert,
|
||||
BaseException,
|
||||
BASELINE_FACTOR,
|
||||
bytesToString,
|
||||
CMapCompressionType,
|
||||
createPromiseCapability,
|
||||
|
@ -479,7 +479,30 @@ class Driver {
|
||||
enableXfa: task.enableXfa,
|
||||
styleElement: xfaStyleElement,
|
||||
});
|
||||
loadingTask.promise.then(
|
||||
let promise = loadingTask.promise;
|
||||
|
||||
if (task.save) {
|
||||
if (!task.annotationStorage) {
|
||||
promise = Promise.reject(
|
||||
new Error("Missing `annotationStorage` entry.")
|
||||
);
|
||||
} else {
|
||||
promise = loadingTask.promise.then(async doc => {
|
||||
for (const [key, value] of Object.entries(
|
||||
task.annotationStorage
|
||||
)) {
|
||||
doc.annotationStorage.setValue(key, value);
|
||||
}
|
||||
const data = await doc.saveDocument();
|
||||
await loadingTask.destroy();
|
||||
delete task.annotationStorage;
|
||||
|
||||
return getDocument(data).promise;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
promise.then(
|
||||
async doc => {
|
||||
if (task.enableXfa) {
|
||||
task.fontRules = "";
|
||||
|
2
test/pdfs/.gitignore
vendored
2
test/pdfs/.gitignore
vendored
@ -551,3 +551,5 @@
|
||||
!bug1795263.pdf
|
||||
!issue15597.pdf
|
||||
!bug1796741.pdf
|
||||
!textfields.pdf
|
||||
!freetext_no_appearance.pdf
|
||||
|
BIN
test/pdfs/freetext_no_appearance.pdf
Executable file
BIN
test/pdfs/freetext_no_appearance.pdf
Executable file
Binary file not shown.
BIN
test/pdfs/textfields.pdf
Executable file
BIN
test/pdfs/textfields.pdf
Executable file
Binary file not shown.
@ -6973,5 +6973,225 @@
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"print": true
|
||||
},
|
||||
{ "id": "ascii_print_textfields",
|
||||
"file": "pdfs/textfields.pdf",
|
||||
"md5": "5f743ca838ff9b7a286dbe52002860b7",
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"print": true,
|
||||
"annotationStorage": {
|
||||
"32R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"35R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"38R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"34R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"37R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"40R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"33R": {
|
||||
"value": "Hello World\nDlrow Olleh\nHello World"
|
||||
},
|
||||
"36R": {
|
||||
"value": "Hello World\nDlrow Olleh\nHello World"
|
||||
},
|
||||
"39R": {
|
||||
"value": "Hello World\nDlrow Olleh\nHello World"
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "id": "arabic_print_textfields",
|
||||
"file": "pdfs/textfields.pdf",
|
||||
"md5": "5f743ca838ff9b7a286dbe52002860b7",
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"print": true,
|
||||
"annotationStorage": {
|
||||
"32R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"35R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"38R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"34R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"37R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"40R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"33R": {
|
||||
"value": "مرحبا بالعالم\nملاعلاب ابحرم\nمرحبا بالعالم"
|
||||
},
|
||||
"36R": {
|
||||
"value": "مرحبا بالعالم\nملاعلاب ابحرم\nمرحبا بالعالم"
|
||||
},
|
||||
"39R": {
|
||||
"value": "مرحبا بالعالم\nملاعلاب ابحرم\nمرحبا بالعالم"
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "id": "ascii_save_print_textfields",
|
||||
"file": "pdfs/textfields.pdf",
|
||||
"md5": "5f743ca838ff9b7a286dbe52002860b7",
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"save": true,
|
||||
"print": true,
|
||||
"annotationStorage": {
|
||||
"32R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"35R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"38R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"34R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"37R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"40R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"33R": {
|
||||
"value": "Hello World\nDlrow Olleh\nHello World"
|
||||
},
|
||||
"36R": {
|
||||
"value": "Hello World\nDlrow Olleh\nHello World"
|
||||
},
|
||||
"39R": {
|
||||
"value": "Hello World\nDlrow Olleh\nHello World"
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "id": "arabic_save_print_textfields",
|
||||
"file": "pdfs/textfields.pdf",
|
||||
"md5": "5f743ca838ff9b7a286dbe52002860b7",
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"save": true,
|
||||
"print": true,
|
||||
"annotationStorage": {
|
||||
"32R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"35R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"38R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"34R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"37R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"40R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"33R": {
|
||||
"value": "مرحبا بالعالم\nملاعلاب ابحرم\nمرحبا بالعالم"
|
||||
},
|
||||
"36R": {
|
||||
"value": "مرحبا بالعالم\nملاعلاب ابحرم\nمرحبا بالعالم"
|
||||
},
|
||||
"39R": {
|
||||
"value": "مرحبا بالعالم\nملاعلاب ابحرم\nمرحبا بالعالم"
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "id": "freetext_no_appearance",
|
||||
"file": "pdfs/freetext_no_appearance.pdf",
|
||||
"md5": "1dc519c06f1dc6f6e594f168080dcde9",
|
||||
"rounds": 1,
|
||||
"type": "eq"
|
||||
},
|
||||
{ "id": "freetext_print_no_appearance",
|
||||
"file": "pdfs/freetext_no_appearance.pdf",
|
||||
"md5": "1dc519c06f1dc6f6e594f168080dcde9",
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"print": true
|
||||
},
|
||||
{
|
||||
"id": "tracemonkey-multi-lang-editors",
|
||||
"file": "pdfs/tracemonkey.pdf",
|
||||
"md5": "9a192d8b1a7dc652a19835f6f08098bd",
|
||||
"rounds": 1,
|
||||
"lastPage": 1,
|
||||
"type": "eq",
|
||||
"print": true,
|
||||
"annotationStorage": {
|
||||
"pdfjs_internal_editor_0": {
|
||||
"annotationType": 3,
|
||||
"color": [255, 0, 0],
|
||||
"fontSize": 10,
|
||||
"value": "Hello World",
|
||||
"pageIndex": 0,
|
||||
"rect": [67.5, 143, 119, 156.5],
|
||||
"rotation": 0
|
||||
},
|
||||
"pdfjs_internal_editor_1": {
|
||||
"annotationType": 3,
|
||||
"color": [0, 255, 0],
|
||||
"fontSize": 10,
|
||||
"value": "مرحبا بالعالم",
|
||||
"pageIndex": 0,
|
||||
"rect": [67.5, 243, 119, 256.5],
|
||||
"rotation": 0
|
||||
},
|
||||
"pdfjs_internal_editor_2": {
|
||||
"annotationType": 3,
|
||||
"color": [0, 0, 255],
|
||||
"fontSize": 10,
|
||||
"value": "你好世界",
|
||||
"pageIndex": 0,
|
||||
"rect": [67.5, 343, 119, 356.5],
|
||||
"rotation": 0
|
||||
},
|
||||
"pdfjs_internal_editor_3": {
|
||||
"annotationType": 3,
|
||||
"color": [255, 0, 255],
|
||||
"fontSize": 10,
|
||||
"value": "Hello World 你好世界 مرحبا بالعالم",
|
||||
"pageIndex": 0,
|
||||
"rect": [67.5, 443, 222, 456.5],
|
||||
"rotation": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "issue12233-arabic-print",
|
||||
"file": "pdfs/issue12233.pdf",
|
||||
"md5": "6099fc695fe018ce444752929d86f9c8",
|
||||
"link": true,
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"print": true,
|
||||
"annotationStorage": {
|
||||
"23R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -56,6 +56,9 @@ describe("annotation", function () {
|
||||
acroForm: new Dict(),
|
||||
},
|
||||
};
|
||||
this.evaluatorOptions = {
|
||||
isOffscreenCanvasSupported: false,
|
||||
};
|
||||
}
|
||||
|
||||
ensure(obj, prop, args) {
|
||||
@ -1611,11 +1614,12 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
expect(appearance).toEqual(
|
||||
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm" +
|
||||
" 2 3.04 Td (test\\\\print) Tj ET Q EMC"
|
||||
" 2 3.07 Td (test\\\\print) Tj ET Q EMC"
|
||||
);
|
||||
});
|
||||
|
||||
@ -1645,13 +1649,14 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
const utf16String =
|
||||
"\x30\x53\x30\x93\x30\x6b\x30\x61\x30\x6f\x4e\x16\x75\x4c\x30\x6e";
|
||||
expect(appearance).toEqual(
|
||||
"/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 0 Tm" +
|
||||
` 2 2 Td (${utf16String}) Tj ET Q EMC`
|
||||
` 2 3.07 Td (${utf16String}) Tj ET Q EMC`
|
||||
);
|
||||
});
|
||||
|
||||
@ -1728,11 +1733,12 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
expect(appearance).toEqual(
|
||||
"/Tx BMC q BT /Helv 5.92 Tf 0 g 1 0 0 1 0 0 Tm" +
|
||||
" 2 3.23 Td (test \\(print\\)) Tj ET Q EMC"
|
||||
" 2 3.07 Td (test \\(print\\)) Tj ET Q EMC"
|
||||
);
|
||||
});
|
||||
|
||||
@ -1762,13 +1768,14 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
const utf16String =
|
||||
"\x30\x53\x30\x93\x30\x6b\x30\x61\x30\x6f\x4e\x16\x75\x4c\x30\x6e";
|
||||
expect(appearance).toEqual(
|
||||
"/Tx BMC q BT /Goth 3.5 Tf 0 g 1 0 0 1 0 0 Tm" +
|
||||
` 2 2 Td (${utf16String}) Tj ET Q EMC`
|
||||
"/Tx BMC q BT /Goth 5.92 Tf 0 g 1 0 0 1 0 0 Tm" +
|
||||
` 2 3.07 Td (${utf16String}) Tj ET Q EMC`
|
||||
);
|
||||
});
|
||||
|
||||
@ -1795,6 +1802,7 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
expect(appearance).toEqual(null);
|
||||
@ -1827,17 +1835,18 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
expect(appearance).toEqual(
|
||||
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 10 Tm " +
|
||||
"2 -5 Td (a aa aaa ) Tj\n" +
|
||||
"0 -5 Td (aaaa aaaaa ) Tj\n" +
|
||||
"0 -5 Td (aaaaaa ) Tj\n" +
|
||||
"0 -5 Td (pneumonoultr) Tj\n" +
|
||||
"0 -5 Td (amicroscopi) Tj\n" +
|
||||
"0 -5 Td (csilicovolca) Tj\n" +
|
||||
"0 -5 Td (noconiosis) Tj ET Q EMC"
|
||||
"2 -6.93 Td (a aa aaa ) Tj\n" +
|
||||
"0 -8 Td (aaaa aaaaa ) Tj\n" +
|
||||
"0 -8 Td (aaaaaa ) Tj\n" +
|
||||
"0 -8 Td (pneumonoultr) Tj\n" +
|
||||
"0 -8 Td (amicroscopi) Tj\n" +
|
||||
"0 -8 Td (csilicovolca) Tj\n" +
|
||||
"0 -8 Td (noconiosis) Tj ET Q EMC"
|
||||
);
|
||||
});
|
||||
|
||||
@ -1868,12 +1877,13 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
expect(appearance).toEqual(
|
||||
"/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 10 Tm " +
|
||||
"2 -5 Td (\x30\x53\x30\x93\x30\x6b\x30\x61\x30\x6f) Tj\n" +
|
||||
"0 -5 Td (\x4e\x16\x75\x4c\x30\x6e) Tj ET Q EMC"
|
||||
"2 -6.93 Td (\x30\x53\x30\x93\x30\x6b\x30\x61\x30\x6f) Tj\n" +
|
||||
"0 -8 Td (\x4e\x16\x75\x4c\x30\x6e) Tj ET Q EMC"
|
||||
);
|
||||
});
|
||||
|
||||
@ -1890,25 +1900,25 @@ describe("annotation", function () {
|
||||
partialEvaluator.xref = xref;
|
||||
const expectedAppearance =
|
||||
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 10 Tm " +
|
||||
"2 -5 Td " +
|
||||
"2 -6.93 Td " +
|
||||
"(Lorem ipsum dolor sit amet, consectetur adipiscing elit.) Tj\n" +
|
||||
"0 -5 Td " +
|
||||
"0 -8 Td " +
|
||||
"(Aliquam vitae felis ac lectus bibendum ultricies quis non) Tj\n" +
|
||||
"0 -5 Td " +
|
||||
"0 -8 Td " +
|
||||
"( diam.) Tj\n" +
|
||||
"0 -5 Td " +
|
||||
"0 -8 Td " +
|
||||
"(Morbi id porttitor quam, a iaculis dui.) Tj\n" +
|
||||
"0 -5 Td " +
|
||||
"0 -8 Td " +
|
||||
"(Pellentesque habitant morbi tristique senectus et netus ) Tj\n" +
|
||||
"0 -5 Td " +
|
||||
"0 -8 Td " +
|
||||
"(et malesuada fames ac turpis egestas.) Tj\n" +
|
||||
"0 -5 Td () Tj\n" +
|
||||
"0 -5 Td () Tj\n" +
|
||||
"0 -5 Td " +
|
||||
"0 -8 Td () Tj\n" +
|
||||
"0 -8 Td () Tj\n" +
|
||||
"0 -8 Td " +
|
||||
"(Nulla consectetur, ligula in tincidunt placerat, velit ) Tj\n" +
|
||||
"0 -5 Td " +
|
||||
"0 -8 Td " +
|
||||
"(augue consectetur orci, sed mattis libero nunc ut massa.) Tj\n" +
|
||||
"0 -5 Td " +
|
||||
"0 -8 Td " +
|
||||
"(Etiam facilisis tempus interdum.) Tj ET Q EMC";
|
||||
|
||||
const annotation = await AnnotationFactory.create(
|
||||
@ -1933,8 +1943,10 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
|
||||
expect(appearance).toEqual(expectedAppearance);
|
||||
});
|
||||
|
||||
@ -1962,10 +1974,11 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
expect(appearance).toEqual(
|
||||
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 2 3.035 Tm" +
|
||||
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 2 3.07 Tm" +
|
||||
" (a) Tj 8 0 Td (a) Tj 8 0 Td (\\() Tj" +
|
||||
" 8 0 Td (a) Tj 8 0 Td (a) Tj" +
|
||||
" 8 0 Td (\\)) Tj 8 0 Td (a) Tj" +
|
||||
@ -2002,10 +2015,11 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
expect(appearance).toEqual(
|
||||
"/Tx BMC q BT /Goth 5 Tf 1 0 0 1 2 2 Tm" +
|
||||
"/Tx BMC q BT /Goth 5 Tf 1 0 0 1 2 3.07 Tm" +
|
||||
" (\x30\x53) Tj 8 0 Td (\x30\x93) Tj 8 0 Td (\x30\x6b) Tj" +
|
||||
" 8 0 Td (\x30\x61) Tj 8 0 Td (\x30\x6f) Tj" +
|
||||
" 8 0 Td (\x4e\x16) Tj 8 0 Td (\x75\x4c) Tj" +
|
||||
@ -2051,7 +2065,7 @@ describe("annotation", function () {
|
||||
expect(newData.data).toEqual(
|
||||
"2 0 obj\n<< /Length 74 /Subtype /Form /Resources " +
|
||||
"<< /Font << /Helv 314 0 R>>>> /BBox [0 0 32 10]>> stream\n" +
|
||||
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2 3.04 Td (hello world) Tj " +
|
||||
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2 3.07 Td (hello world) Tj " +
|
||||
"ET Q EMC\nendstream\nendobj\n"
|
||||
);
|
||||
});
|
||||
@ -2092,12 +2106,12 @@ describe("annotation", function () {
|
||||
"123 0 obj\n" +
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Tx /DA (/Helv 5 Tf) /DR " +
|
||||
"<< /Font << /Helv 314 0 R>>>> /Rect [0 0 32 10] " +
|
||||
"/V (hello world) /AP << /N 2 0 R>> /M (date) /MK << /R 90>>>>\nendobj\n"
|
||||
"/V (hello world) /MK << /R 90>> /AP << /N 2 0 R>> /M (date)>>\nendobj\n"
|
||||
);
|
||||
expect(newData.data).toEqual(
|
||||
"2 0 obj\n<< /Length 74 /Subtype /Form /Resources " +
|
||||
"<< /Font << /Helv 314 0 R>>>> /BBox [0 0 32 10] /Matrix [0 1 -1 0 32 0]>> stream\n" +
|
||||
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2 3.04 Td (hello world) Tj " +
|
||||
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2 2.94 Td (hello world) Tj " +
|
||||
"ET Q EMC\nendstream\nendobj\n"
|
||||
);
|
||||
});
|
||||
@ -2226,9 +2240,9 @@ describe("annotation", function () {
|
||||
`/V (\xfe\xff${utf16String}) /AP << /N 2 0 R>> /M (date)>>\nendobj\n`
|
||||
);
|
||||
expect(newData.data).toEqual(
|
||||
"2 0 obj\n<< /Length 76 /Subtype /Form /Resources " +
|
||||
"2 0 obj\n<< /Length 79 /Subtype /Form /Resources " +
|
||||
"<< /Font << /Helv 314 0 R /Goth 159 0 R>>>> /BBox [0 0 32 10]>> stream\n" +
|
||||
`/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 0 Tm 2 2 Td (${utf16String}) Tj ` +
|
||||
`/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 0 Tm 2 3.07 Td (${utf16String}) Tj ` +
|
||||
"ET Q EMC\nendstream\nendobj\n"
|
||||
);
|
||||
});
|
||||
@ -3457,6 +3471,7 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
expect(appearance).toEqual(
|
||||
@ -3501,6 +3516,7 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
expect(appearance).toEqual(
|
||||
@ -3549,6 +3565,7 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
expect(appearance).toEqual(
|
||||
@ -3605,7 +3622,7 @@ describe("annotation", function () {
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Ch /DA (/Helv 5 Tf) /DR " +
|
||||
"<< /Font << /Helv 314 0 R>>>> " +
|
||||
"/Rect [0 0 32 10] /Opt [(A) (B) (C)] /V (C) " +
|
||||
"/AP << /N 2 0 R>> /M (date) /MK << /R 270>>>>\nendobj\n"
|
||||
"/MK << /R 270>> /AP << /N 2 0 R>> /M (date)>>\nendobj\n"
|
||||
);
|
||||
expect(newData.data).toEqual(
|
||||
[
|
||||
@ -4052,7 +4069,6 @@ describe("annotation", function () {
|
||||
"ET\n" +
|
||||
"Q\n" +
|
||||
"endstream\n" +
|
||||
"\n" +
|
||||
"endobj\n"
|
||||
);
|
||||
});
|
||||
@ -4245,7 +4261,6 @@ describe("annotation", function () {
|
||||
"922 923 924 925 926 927 c\n" +
|
||||
"S\n" +
|
||||
"endstream\n" +
|
||||
"\n" +
|
||||
"endobj\n"
|
||||
);
|
||||
});
|
||||
@ -4309,7 +4324,6 @@ describe("annotation", function () {
|
||||
"922 923 924 925 926 927 c\n" +
|
||||
"S\n" +
|
||||
"endstream\n" +
|
||||
"\n" +
|
||||
"endobj\n"
|
||||
);
|
||||
});
|
||||
|
@ -66,6 +66,9 @@ describe("document", function () {
|
||||
}
|
||||
return value;
|
||||
},
|
||||
get evaluatorOptions() {
|
||||
return { isOffscreenCanvasSupported: false };
|
||||
},
|
||||
};
|
||||
const pdfDocument = new PDFDocument(pdfManager, stream);
|
||||
pdfDocument.xref = xref;
|
||||
|
@ -78,7 +78,8 @@ class XRefMock {
|
||||
constructor(array) {
|
||||
this._map = Object.create(null);
|
||||
this.stats = new DocStats({ send: () => {} });
|
||||
this._newRefNum = null;
|
||||
this._newTemporaryRefNum = null;
|
||||
this._newPersistentRefNum = null;
|
||||
this.stream = new NullStream();
|
||||
|
||||
for (const key in array) {
|
||||
@ -87,15 +88,24 @@ class XRefMock {
|
||||
}
|
||||
}
|
||||
|
||||
getNewRef() {
|
||||
if (this._newRefNum === null) {
|
||||
this._newRefNum = Object.keys(this._map).length || 1;
|
||||
getNewPersistentRef(obj) {
|
||||
if (this._newPersistentRefNum === null) {
|
||||
this._newPersistentRefNum = Object.keys(this._map).length || 1;
|
||||
}
|
||||
return Ref.get(this._newRefNum++, 0);
|
||||
const ref = Ref.get(this._newPersistentRefNum++, 0);
|
||||
this._map[ref.toString()] = obj;
|
||||
return ref;
|
||||
}
|
||||
|
||||
resetNewRef() {
|
||||
this.newRef = null;
|
||||
getNewTemporaryRef() {
|
||||
if (this._newTemporaryRefNum === null) {
|
||||
this._newTemporaryRefNum = Object.keys(this._map).length || 1;
|
||||
}
|
||||
return Ref.get(this._newTemporaryRefNum++, 0);
|
||||
}
|
||||
|
||||
resetNewTemporaryRef() {
|
||||
this._newTemporaryRefNum = null;
|
||||
}
|
||||
|
||||
fetch(ref) {
|
||||
|
@ -128,7 +128,7 @@ describe("Writer", function () {
|
||||
"/E (\\(hello\\\\world\\)) /F [1.23 4.5 6] " +
|
||||
"/G << /H 123 /I << /Length 8>> stream\n" +
|
||||
"a stream\n" +
|
||||
"endstream\n>> /J true /K false " +
|
||||
"endstream>> /J true /K false " +
|
||||
"/NullArr [null 10] /NullVal null>>";
|
||||
|
||||
expect(buffer.join("")).toEqual(expected);
|
||||
@ -194,6 +194,7 @@ describe("Writer", function () {
|
||||
"\n" +
|
||||
"789 0 obj\n" +
|
||||
"<< /XFA [(preamble) 123 0 R (datasets) 101112 0 R (postamble) 456 0 R]>>\n" +
|
||||
"endobj\n" +
|
||||
"101112 0 obj\n" +
|
||||
"<< /Type /EmbeddedFile /Length 20>>\n" +
|
||||
"stream\n" +
|
||||
@ -202,11 +203,11 @@ describe("Writer", function () {
|
||||
"endobj\n" +
|
||||
"131415 0 obj\n" +
|
||||
"<< /Size 131416 /Prev 314 /Type /XRef /Index [0 1 789 1 101112 1 131415 1] /W [1 1 2] /Length 16>> stream\n" +
|
||||
"\u0000\u0001ÿÿ\u0001\u0001\u0000\u0000\u0001T\u0000\u0000\u0001²\u0000\u0000\n" +
|
||||
"\u0000\u0001ÿÿ\u0001\u0001\u0000\u0000\u0001[\u0000\u0000\u0001¹\u0000\u0000\n" +
|
||||
"endstream\n" +
|
||||
"endobj\n" +
|
||||
"startxref\n" +
|
||||
"178\n" +
|
||||
"185\n" +
|
||||
"%%EOF\n";
|
||||
|
||||
expect(data).toEqual(expected);
|
||||
|
Loading…
Reference in New Issue
Block a user