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 000000000..2b8a0f168 Binary files /dev/null and b/test/pdfs/issue14502.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index e91c3f6fe..61c8f04db 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -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" + } + } } ] diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 8767e5aca..39d126b52 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -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" ); }); 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, });