[api-minor] Annotations - Adjust the font size in text field in considering the total width (bug 1721335)

- it aims to fix #14502 and bug 1721335;
 - Acrobat and Pdfium do the same;
 - it'll avoid to have truncated data when printed;
 - change the factor to compute font size in using field height: lineHeight = 1.35*fontSize
  - this is the value used by Acrobat.
 - in order to not have truncated strings on the bottom, add few basic metrics for standard fonts.
This commit is contained in:
Calixte Denizet 2022-01-27 22:51:30 +01:00
parent 8f6965b197
commit ae842e1c3a
8 changed files with 214 additions and 40 deletions

View File

@ -1523,13 +1523,15 @@ class WidgetAnnotation extends Annotation {
);
}
const font = await this._getFontData(evaluator, task);
const [defaultAppearance, fontSize] = this._computeFontSize(
totalHeight,
totalHeight - defaultPadding,
totalWidth - 2 * hPadding,
value,
font,
lineCount
);
const font = await this._getFontData(evaluator, task);
let descent = font.descent;
if (isNaN(descent)) {
descent = 0;
@ -1618,34 +1620,84 @@ class WidgetAnnotation extends Annotation {
return initialState.font;
}
_computeFontSize(height, lineCount) {
_getTextWidth(text, font) {
return (
font
.charsToGlyphs(text)
.reduce((width, glyph) => width + glyph.width, 0) / 1000
);
}
_computeFontSize(height, width, text, font, lineCount) {
let { fontSize } = this.data.defaultAppearanceData;
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
// annotation rectangle (see 12.7.3.3).
const roundWithOneDigit = x => Math.round(x * 10) / 10;
const roundWithTwoDigits = x => Math.floor(x * 100) / 100;
// Represent the percentage of the height of a single-line field over
// the font size.
// Acrobat seems to use this value.
const LINE_FACTOR = 1.35;
// Represent the percentage of the font size over the height
// of a single-line field.
const FONT_FACTOR = 0.8;
if (lineCount === -1) {
fontSize = roundWithOneDigit(FONT_FACTOR * height);
const textWidth = this._getTextWidth(text, font);
fontSize = roundWithTwoDigits(
Math.min(height / LINE_FACTOR, width / textWidth)
);
} else {
const lines = text.split(/\r\n?|\n/);
const cachedLines = [];
for (const line of lines) {
const encoded = font.encodeString(line).join("");
const glyphs = font.charsToGlyphs(encoded);
const positions = font.getCharPositions(encoded);
cachedLines.push({
line: encoded,
glyphs,
positions,
});
}
const isTooBig = fsize => {
// Return true when the text doesn't fit the given height.
let totalHeight = 0;
for (const cache of cachedLines) {
const chunks = this._splitLine(null, font, fsize, width, cache);
totalHeight += chunks.length * fsize;
if (totalHeight > height) {
return true;
}
}
return false;
};
// Hard to guess how many lines there are.
// The field may have been sized to have 10 lines
// and the user entered only 1 so if we get font size from
// height and number of lines then we'll get something too big.
// So we compute a fake number of lines based on height and
// a font size equal to 10.
// 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 = 10;
let lineHeight = fontSize / FONT_FACTOR;
fontSize = 12;
let lineHeight = fontSize * LINE_FACTOR;
let numberOfLines = Math.round(height / lineHeight);
numberOfLines = Math.max(numberOfLines, lineCount);
lineHeight = height / numberOfLines;
fontSize = roundWithOneDigit(FONT_FACTOR * lineHeight);
while (true) {
lineHeight = height / numberOfLines;
fontSize = roundWithTwoDigits(lineHeight / LINE_FACTOR);
if (isTooBig(fontSize)) {
numberOfLines++;
continue;
}
break;
}
}
const { fontName, fontColor } = this.data.defaultAppearanceData;
@ -1660,13 +1712,7 @@ class WidgetAnnotation extends Annotation {
_renderText(text, font, fontSize, totalWidth, alignment, hPadding, vPadding) {
// We need to get the width of the text in order to align it correctly
const glyphs = font.charsToGlyphs(text);
const scale = fontSize / 1000;
let width = 0;
for (const glyph of glyphs) {
width += glyph.width * scale;
}
const width = this._getTextWidth(text, font) * fontSize;
let shift;
if (alignment === 1) {
// Center
@ -1803,7 +1849,7 @@ class TextWidgetAnnotation extends WidgetAnnotation {
hPadding,
vPadding
) {
const lines = text.split(/\r\n|\r|\n/);
const lines = text.split(/\r\n?|\n/);
const buf = [];
const totalWidth = width - 2 * hPadding;
for (const line of lines) {
@ -1833,18 +1879,18 @@ class TextWidgetAnnotation extends WidgetAnnotation {
);
}
_splitLine(line, font, fontSize, width) {
_splitLine(line, font, fontSize, width, cache = {}) {
// TODO: need to handle chars which are not in the font.
line = font.encodeString(line).join("");
line = cache.line || font.encodeString(line).join("");
const glyphs = font.charsToGlyphs(line);
const glyphs = cache.glyphs || font.charsToGlyphs(line);
if (glyphs.length <= 1) {
// Nothing to split
return [line];
}
const positions = font.getCharPositions(line);
const positions = cache.positions || font.getCharPositions(line);
const scale = fontSize / 1000;
const chunks = [];

View File

@ -59,6 +59,7 @@ import {
import { IdentityToUnicodeMap, ToUnicodeMap } from "./to_unicode_map.js";
import { CFFFont } from "./cff_font.js";
import { FontRendererFactory } from "./font_renderer.js";
import { getFontBasicMetrics } from "./metrics.js";
import { GlyfTable } from "./glyf.js";
import { IdentityCMap } from "./cmap.js";
import { OpenTypeFileBuilder } from "./opentype_file_builder.js";
@ -1074,6 +1075,21 @@ class Font {
);
fontName = stdFontMap[fontName] || nonStdFontMap[fontName] || fontName;
const fontBasicMetricsMap = getFontBasicMetrics();
const metrics = fontBasicMetricsMap[fontName];
if (metrics) {
if (isNaN(this.ascent)) {
this.ascent = metrics.ascent / PDF_GLYPH_SPACE_UNITS;
}
if (isNaN(this.descent)) {
this.descent = metrics.descent / PDF_GLYPH_SPACE_UNITS;
}
if (isNaN(this.capHeight)) {
this.capHeight = metrics.capHeight / PDF_GLYPH_SPACE_UNITS;
}
}
this.bold = fontName.search(/bold/gi) !== -1;
this.italic =
fontName.search(/oblique/gi) !== -1 || fontName.search(/italic/gi) !== -1;

View File

@ -2967,4 +2967,91 @@ const getMetrics = getLookupTableFactory(function (t) {
});
});
export { getMetrics };
const getFontBasicMetrics = getLookupTableFactory(function (t) {
t.Courier = {
ascent: 629,
descent: -157,
capHeight: 562,
xHeight: -426,
};
t["Courier-Bold"] = {
ascent: 629,
descent: -157,
capHeight: 562,
xHeight: 439,
};
t["Courier-Oblique"] = {
ascent: 629,
descent: -157,
capHeight: 562,
xHeight: 426,
};
t["Courier-BoldOblique"] = {
ascent: 629,
descent: -157,
capHeight: 562,
xHeight: 426,
};
t.Helvetica = {
ascent: 718,
descent: -207,
capHeight: 718,
xHeight: 523,
};
t["Helvetica-Bold"] = {
ascent: 718,
descent: -207,
capHeight: 718,
xHeight: 532,
};
t["Helvetica-Oblique"] = {
ascent: 718,
descent: -207,
capHeight: 718,
xHeight: 523,
};
t["Helvetica-BoldOblique"] = {
ascent: 718,
descent: -207,
capHeight: 718,
xHeight: 532,
};
t["Times-Roman"] = {
ascent: 683,
descent: -217,
capHeight: 662,
xHeight: 450,
};
t["Times-Bold"] = {
ascent: 683,
descent: -217,
capHeight: 676,
xHeight: 461,
};
t["Times-Italic"] = {
ascent: 683,
descent: -217,
capHeight: 653,
xHeight: 441,
};
t["Times-BoldItalic"] = {
ascent: 683,
descent: -217,
capHeight: 669,
xHeight: 462,
};
t.Symbol = {
ascent: Math.NaN,
descent: Math.NaN,
capHeight: Math.NaN,
xHeight: Math.NaN,
};
t.ZapfDingbats = {
ascent: Math.NaN,
descent: Math.NaN,
capHeight: Math.NaN,
xHeight: Math.NaN,
};
});
export { getFontBasicMetrics, getMetrics };

View File

@ -508,3 +508,4 @@
!issue14415.pdf
!issue14307.pdf
!issue14497.pdf
!issue14502.pdf

BIN
test/pdfs/issue14502.pdf Executable file

Binary file not shown.

View File

@ -6250,5 +6250,26 @@
"md5": "7f795a92caa612117b6928f8bb4c5b65",
"rounds": 1,
"type": "text"
},
{ "id": "issue14052",
"file": "pdfs/issue14502.pdf",
"md5": "7085cdc31243ab0b979f0c5fa151e491",
"rounds": 1,
"type": "eq",
"print": true,
"annotationStorage": {
"27R": {
"value": "Hello PDF.js World"
},
"28R": {
"value": "PDF.js PDF.js PDF.js PDF.js\nPDF.js PDF.js PDF.js PDF.js\nPDF.js PDF.js PDF.js PDF.js\nPDF.js PDF.js PDF.js PDF.js"
},
"29R": {
"value": "PDF.js"
},
"30R": {
"value": "PDF.js PDF.js PDF.js"
}
}
}
]

View File

@ -1614,7 +1614,7 @@ describe("annotation", function () {
);
expect(appearance).toEqual(
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm" +
" 2.00 2.00 Td (test\\\\print) Tj ET Q EMC"
" 2.00 3.04 Td (test\\\\print) Tj ET Q EMC"
);
});
@ -1732,8 +1732,8 @@ describe("annotation", function () {
annotationStorage
);
expect(appearance).toEqual(
"/Tx BMC q BT /Helv 8 Tf 0 g 1 0 0 1 0 0 Tm" +
" 2.00 2.00 Td (test \\(print\\)) Tj ET Q EMC"
"/Tx BMC q BT /Helv 5.92 Tf 0 g 1 0 0 1 0 0 Tm" +
" 2.00 3.23 Td (test \\(print\\)) Tj ET Q EMC"
);
});
@ -1768,7 +1768,7 @@ describe("annotation", function () {
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 8 Tf 0 g 1 0 0 1 0 0 Tm" +
"/Tx BMC q BT /Goth 3.5 Tf 0 g 1 0 0 1 0 0 Tm" +
` 2.00 2.00 Td (${utf16String}) Tj ET Q EMC`
);
});
@ -1966,7 +1966,7 @@ describe("annotation", function () {
annotationStorage
);
expect(appearance).toEqual(
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 2 2 Tm" +
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 2 3.035 Tm" +
" (a) Tj 8.00 0 Td (a) Tj 8.00 0 Td (\\() Tj" +
" 8.00 0 Td (a) Tj 8.00 0 Td (a) Tj" +
" 8.00 0 Td (\\)) Tj 8.00 0 Td (a) Tj" +
@ -2052,7 +2052,7 @@ describe("annotation", function () {
expect(newData.data).toEqual(
"2 0 obj\n<< /Length 77 /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.00 2.00 Td (hello world) Tj " +
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2.00 3.04 Td (hello world) Tj " +
"ET Q EMC\nendstream\nendobj\n"
);
});
@ -3377,7 +3377,7 @@ describe("annotation", function () {
);
expect(appearance).toEqual(
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm" +
" 2.00 2.00 Td (a value) Tj ET Q EMC"
" 2.00 3.04 Td (a value) Tj ET Q EMC"
);
});
@ -3388,6 +3388,7 @@ describe("annotation", function () {
const choiceWidgetRef = Ref.get(123, 0);
const xref = new XRefMock([
{ ref: choiceWidgetRef, data: choiceWidgetDict },
fontRefObj,
]);
partialEvaluator.xref = xref;
const task = new WorkerTask("test save");
@ -3409,7 +3410,7 @@ describe("annotation", function () {
expect(data.length).toEqual(2);
const [oldData, newData] = data;
expect(oldData.ref).toEqual(Ref.get(123, 0));
expect(newData.ref).toEqual(Ref.get(1, 0));
expect(newData.ref).toEqual(Ref.get(2, 0));
oldData.data = oldData.data.replace(/\(D:\d+\)/, "(date)");
expect(oldData.data).toEqual(
@ -3417,13 +3418,13 @@ 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 1 0 R>> /M (date)>>\nendobj\n"
"/AP << /N 2 0 R>> /M (date)>>\nendobj\n"
);
expect(newData.data).toEqual(
"1 0 obj\n" +
"2 0 obj\n" +
"<< /Length 67 /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.00 2.00 Td (C) Tj ET Q EMC\n" +
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2.00 3.04 Td (C) Tj ET Q EMC\n" +
"endstream\nendobj\n"
);
});

View File

@ -2010,8 +2010,10 @@ page 1 / 3`);
});
expect(styles[fontName]).toEqual({
fontFamily: "serif",
ascent: NaN,
descent: NaN,
// `useSystemFonts` has a different value in web environments
// and in Node.js.
ascent: isNodeJS ? NaN : 0.683,
descent: isNodeJS ? NaN : -0.217,
vertical: false,
});