in src/core/annotation.js [2270:2543]
async _getAppearance(evaluator, task, intent, annotationStorage) {
if (this.data.password) {
return null;
}
const storageEntry = annotationStorage?.get(this.data.id);
let value, rotation;
if (storageEntry) {
value = storageEntry.formattedValue || storageEntry.value;
rotation = storageEntry.rotation;
}
if (
rotation === undefined &&
value === undefined &&
!this._needAppearances
) {
if (!this._hasValueFromXFA || this.appearance) {
// The annotation hasn't been rendered so use the appearance.
return null;
}
}
// Empty or it has a trailing whitespace.
const colors = this.getBorderAndBackgroundAppearances(annotationStorage);
if (value === undefined) {
// The annotation has its value in XFA datasets but not in the V field.
value = this.data.fieldValue;
if (!value) {
return `/Tx BMC q ${colors}Q EMC`;
}
}
if (Array.isArray(value) && value.length === 1) {
value = value[0];
}
assert(typeof value === "string", "Expected `value` to be a string.");
value = value.trimEnd();
if (this.data.combo) {
// The value can be one of the exportValue or any other values.
const option = this.data.options.find(
({ exportValue }) => value === exportValue
);
value = option?.displayValue || value;
}
if (value === "") {
// the field is empty: nothing to render
return `/Tx BMC q ${colors}Q EMC`;
}
if (rotation === undefined) {
rotation = this.rotation;
}
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) {
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 = 1;
const defaultHPadding = 2;
let { width: totalWidth, height: totalHeight } = this;
if (rotation === 90 || rotation === 270) {
[totalWidth, totalHeight] = [totalHeight, totalWidth];
}
if (!this._defaultAppearance) {
// The DA is required and must be a string.
// If there is no font named Helvetica in the resource dictionary,
// the evaluator will fall back to a default font.
// Doing so prevents exceptions and allows saving/printing
// the file as expected.
this.data.defaultAppearanceData = parseDefaultAppearance(
(this._defaultAppearance = "/Helvetica 0 Tf 0 g")
);
}
let font = await WidgetAnnotation._getFontData(
evaluator,
task,
this.data.defaultAppearanceData,
this._fieldResources.mergedResources
);
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 = 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
// padding.
const defaultVPadding = Math.min(
Math.floor((totalHeight - fontSize) / 2),
defaultPadding
);
const alignment = this.data.textAlignment;
if (this.data.multiLine) {
return this._getMultilineAppearance(
defaultAppearance,
encodedLines,
font,
fontSize,
totalWidth,
totalHeight,
alignment,
defaultHPadding,
defaultVPadding,
descent,
lineHeight,
annotationStorage
);
}
if (this.data.comb) {
return this._getCombAppearance(
defaultAppearance,
font,
encodedLines[0],
fontSize,
totalWidth,
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 ${numberToString(defaultHPadding)} ${numberToString(
bottomPadding
)} Tm (${escapeString(encodedLines[0])}) Tj` +
" ET Q EMC"
);
}
const prevInfo = { shift: 0 };
const renderedText = this._renderText(
encodedLines[0],
font,
fontSize,
totalWidth,
alignment,
prevInfo,
defaultHPadding,
bottomPadding
);
return (
`/Tx BMC q ${colors}BT ` +
defaultAppearance +
` 1 0 0 1 0 0 Tm ${renderedText}` +
" ET Q EMC"
);
}