From ae842e1c3a75b34344e5c37ca09e497bc49a0d24 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Thu, 27 Jan 2022 22:51:30 +0100 Subject: [PATCH] [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. --- src/core/annotation.js | 98 +++++++++++++++++++++++++---------- src/core/fonts.js | 16 ++++++ src/core/metrics.js | 89 ++++++++++++++++++++++++++++++- test/pdfs/.gitignore | 1 + test/pdfs/issue14502.pdf | Bin 0 -> 7255 bytes test/test_manifest.json | 21 ++++++++ test/unit/annotation_spec.js | 23 ++++---- test/unit/api_spec.js | 6 ++- 8 files changed, 214 insertions(+), 40 deletions(-) create mode 100755 test/pdfs/issue14502.pdf diff --git a/src/core/annotation.js b/src/core/annotation.js index 601d3cbe8..b63bb274d 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -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 = []; diff --git a/src/core/fonts.js b/src/core/fonts.js index a1c694455..555a052ed 100644 --- a/src/core/fonts.js +++ b/src/core/fonts.js @@ -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; diff --git a/src/core/metrics.js b/src/core/metrics.js index 2d8650142..c61c86080 100644 --- a/src/core/metrics.js +++ b/src/core/metrics.js @@ -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 }; diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index fcbab5b3d..d1d37ef26 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -508,3 +508,4 @@ !issue14415.pdf !issue14307.pdf !issue14497.pdf +!issue14502.pdf diff --git a/test/pdfs/issue14502.pdf b/test/pdfs/issue14502.pdf new file mode 100755 index 0000000000000000000000000000000000000000..2b8a0f168ebe032e34d626fe99cf195641249121 GIT binary patch literal 7255 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 /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" ); }); diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 86c63675f..293c4563c 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -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, });