Merge pull request #15587 from calixteman/save_unicode

[Annotation] Fix printing/saving for annotations containing some non-ascii chars and with no fonts to handle them (bug 1666824)
This commit is contained in:
calixteman 2022-11-10 20:57:34 +01:00 committed by GitHub
commit 592d92424e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1117 additions and 287 deletions

View File

@ -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);
}

View File

@ -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,

View File

@ -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,
};

View File

@ -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;

View File

@ -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();
}
});
}
);

View File

@ -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,
});
}

View File

@ -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) {

View File

@ -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) {

View File

@ -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");
}

View File

@ -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,

View File

@ -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 = "";

View File

@ -551,3 +551,5 @@
!bug1795263.pdf
!issue15597.pdf
!bug1796741.pdf
!textfields.pdf
!freetext_no_appearance.pdf

Binary file not shown.

BIN
test/pdfs/textfields.pdf Executable file

Binary file not shown.

View File

@ -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": "مرحبا بالعالم"
}
}
}
]

View File

@ -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"
);
});

View File

@ -66,6 +66,9 @@ describe("document", function () {
}
return value;
},
get evaluatorOptions() {
return { isOffscreenCanvasSupported: false };
},
};
const pdfDocument = new PDFDocument(pdfManager, stream);
pdfDocument.xref = xref;

View File

@ -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) {

View File

@ -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);