1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-26 01:58:06 +02:00

[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:
Calixte Denizet 2022-10-18 17:07:47 +02:00
parent f7449563ef
commit 3ca03603c2
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);
}