From e1651fc4e205432a912ee125a187f4f64af70de4 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 19 Aug 2026 15:01:00 +0200 Subject: [PATCH 1/3] Vertically center the text in single-line and comb text fields (bug 2055455) Improve the formula for centering glyphs in text fields and use the same shift for the single-line and the comb paths. --- src/core/annotation.js | 27 ++++++++++++++++++--------- test/pdfs/.gitignore | 1 + test/pdfs/bug2055455.pdf | Bin 0 -> 1518 bytes test/test_manifest.json | 9 +++++++++ test/unit/annotation_spec.js | 22 +++++++++++----------- 5 files changed, 39 insertions(+), 20 deletions(-) create mode 100644 test/pdfs/bug2055455.pdf diff --git a/src/core/annotation.js b/src/core/annotation.js index 5934f251952b0..3c271feace0bb 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -2586,6 +2586,21 @@ class WidgetAnnotation extends Annotation { ); const alignment = this.data.textAlignment; + let { ascent: fontAscent, descent: fontDescent } = font; + if ( + isNaN(fontAscent) || + isNaN(fontDescent) || + (!fontAscent && !fontDescent) + ) { + fontAscent = LINE_FACTOR - LINE_DESCENT_FACTOR; + fontDescent = LINE_DESCENT_FACTOR; + } else { + fontDescent = Math.abs(fontDescent); + } + const vShift = + (totalHeight - (fontAscent + fontDescent) * fontSize) / 2 + + fontDescent * fontSize; + if (this.data.multiLine) { return this._getMultilineAppearance( defaultAppearance, @@ -2610,14 +2625,14 @@ class WidgetAnnotation extends Annotation { encodedLines[0], fontSize, totalWidth, - totalHeight, + vShift, alignment, bidi(lines[0]).dir === "rtl", annotationStorage ); } - const bottomPadding = defaultVPadding + descent; + const bottomPadding = vShift; if (alignment === 0 || alignment > 2) { // Left alignment: nothing to do return ( @@ -2971,7 +2986,7 @@ class TextWidgetAnnotation extends WidgetAnnotation { text, fontSize, width, - height, + vShift, alignment, isRTL, annotationStorage @@ -3009,12 +3024,6 @@ class TextWidgetAnnotation extends WidgetAnnotation { } const renderedComb = buf.join(" "); - // Vertically center the glyphs within the field: comb fields are mostly - // filled with uppercase letters and/or digits, hence we use the cap height - // (with a fallback on the ascent or the font size) to center them. - const vShift = - (height - (font.capHeight || font.ascent || 1) * fontSize) / 2; - return ( `/Tx BMC q ${colors}BT ` + defaultAppearance + diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 8148a5d3202a4..141d407b92ff7 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -952,3 +952,4 @@ !function_based_shading_cmyk.pdf !large_jpeg_downscale.pdf !bug1898053_minimal.pdf +!bug2055455.pdf diff --git a/test/pdfs/bug2055455.pdf b/test/pdfs/bug2055455.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e694450980f620b2ae2154685cb397d63bf05a5d GIT binary patch literal 1518 zcmb_c+iu!G5PkPo%nMTT5Z1fK7o-S@T-;VoXbLE)gz~U4i*W@uS}zLiPxK%9GyR!< zL}%6pjH60@C^DFx%kG&oXSVZj6JPLd=yZPl{_)e{;6m~8)w#R`HvP6yz^z<@EZVWNw;&}-=1^n;YP8OeD|SL^@V0 z46V<-SMy9>6+4)@7|(n7bzQq(ZZNEb9v$ES02`~a*y;o^V=;pPV$SmdBW59mKt#XK z4FHt62XLqI)GWd8lfN`t$+hEleR#!~caCr&z=Qp+=Z3(eZoe-;E0~Z2A3yAh;KYvH z)SJ{S`HizGn&SeCWRvZSv8+$^R#lEGS*ps3oas#Ik$MF-l56VC!D1_pGhLeNrPSaD z!0u#Y@s!QY5^E1ETeo>q%(EPu`jq9vyv+87Y8!-dqsl~Q8&hZqh^PinwBGY{#NJ3? z1h)+kSiF*H3107T#+B{k1s4J?f(SVk9(Gf3Z085mT$T*;bfv(x3BxiW8IT_s9Y!mg zh6>%hS6R9=$UHi~)QcW&$0kOt9|2`@6dJYv6u%WlgM1__RoIPqibJr&H>!#~fYq)A zGggU#StSFnl0j%?fY7IKRwA`4bq?$i&LVt$MxnENxkFU}ugYHl7k(uATwparIBT>L zcmUG{f=tgFs7bE>3pD}{twy~D=%iS`)R3cyP}hL#el0-sy};!n`bWTYG3)`ynOf2G zpG+PbDK#H%HcD!l)0i66yk+O_gjDV^O|(^+{d1fGpt&NgR?q%U+G(|L=%#So!v_N2 z_k38u!NxI}K9C=6sLHmDdssy)P6Rj(uC9L+>>> /BBox [0 0 32 10] /Length 74>> stream\n" + - "/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2 3.07 Td (hello world) Tj " + + "/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2 3.72 Td (hello world) Tj " + "ET Q EMC\nendstream\nendobj\n" ); }); @@ -2327,8 +2327,8 @@ describe("annotation", function () { ); expect(newData.data).toEqual( "2 0 obj\n<< /Subtype /Form /Resources " + - "<< /Font << /Helv 314 0 R>>>> /BBox [0 0 10 32] /Matrix [0 1 -1 0 32 0] /Length 74>> stream\n" + - "/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2 2.94 Td (hello world) Tj " + + "<< /Font << /Helv 314 0 R>>>> /BBox [0 0 10 32] /Matrix [0 1 -1 0 32 0] /Length 75>> stream\n" + + "/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2 14.72 Td (hello world) Tj " + "ET Q EMC\nendstream\nendobj\n" ); }); @@ -2388,7 +2388,7 @@ describe("annotation", function () { // generated data with the PDF.js library. const flateStream = new FlateStream(new StringStream(compressedStream)); expect(flateStream.getString()).toEqual( - `/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2 3.07 Td (${value}) Tj ET Q EMC` + `/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2 3.72 Td (${value}) Tj ET Q EMC` ); }); @@ -2513,7 +2513,7 @@ describe("annotation", function () { expect(newData.data).toEqual( "2 0 obj\n<< /Subtype /Form /Resources " + "<< /Font << /Helv 314 0 R /Goth 159 0 R>>>> /BBox [0 0 32 10] /Length 79>> stream\n" + - `/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 0 Tm 2 3.07 Td (${utf16String}) Tj ` + + `/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 0 Tm 2 3.38 Td (${utf16String}) Tj ` + "ET Q EMC\nendstream\nendobj\n" ); }); From d4d1a8d5d6c9a91e15af79ee430497961517324c Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Thu, 20 Aug 2026 11:01:05 +0200 Subject: [PATCH 2/3] Remove a few unnecessary checks after calling `BaseStream.prototype.getBytes` The `getBytes` methods will *always* return a Uint8Array[1], and remember that even an empty TypedArray is truthy (i.e. `!!new Uint8Array(0) === true`). Hence we can remove a few pointless checks from the `DecryptStream` and `RunLengthStream` classes, which should improve code readability a tiny bit. --- [1] See [stream.js](https://github.com/mozilla/pdf.js/blob/e051279385c5cf141935037b340e757081052990/src/core/stream.js#L45-L51), [chunked_stream.js](https://github.com/mozilla/pdf.js/blob/e051279385c5cf141935037b340e757081052990/src/core/chunked_stream.js#L184-L193), and [decode_stream.js](https://github.com/mozilla/pdf.js/blob/e051279385c5cf141935037b340e757081052990/src/core/decode_stream.js#L86-L110). --- src/core/decrypt_stream.js | 4 ++-- src/core/run_length_stream.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/decrypt_stream.js b/src/core/decrypt_stream.js index e8ac379af18ca..ea2659e6066f9 100644 --- a/src/core/decrypt_stream.js +++ b/src/core/decrypt_stream.js @@ -30,12 +30,12 @@ class DecryptStream extends DecodeStream { readBlock() { let chunk = this.#nextChunk ?? this.stream.getBytes(chunkSize); - if (!chunk?.length) { + if (!chunk.length) { this.eof = true; return; } this.#nextChunk = this.stream.getBytes(chunkSize); - const hasMoreData = this.#nextChunk?.length > 0; + const hasMoreData = this.#nextChunk.length > 0; const decrypt = this.decrypt; chunk = decrypt(chunk, !hasMoreData); diff --git a/src/core/run_length_stream.js b/src/core/run_length_stream.js index db664b733620a..30c54719f59da 100644 --- a/src/core/run_length_stream.js +++ b/src/core/run_length_stream.js @@ -29,7 +29,7 @@ class RunLengthStream extends DecodeStream { // (in addition to the second byte from the header), n = 129 through 255 - // duplicate the second byte from the header (257 - n) times, n = 128 - end. const repeatHeader = this.stream.getBytes(2); - if (!repeatHeader || repeatHeader.length < 2 || repeatHeader[0] === 128) { + if (repeatHeader.length < 2 || repeatHeader[0] === 128) { this.eof = true; return; } From ea5ecb012e4816ef14ca164e0960166a92eae5db Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Thu, 20 Aug 2026 14:19:23 +0200 Subject: [PATCH 3/3] Cache the `TextDecoder`/`TextEncoder` instances used when compiling/decompiling font-data This avoids creating "duplicate" `TextDecoder`/`TextEncoder` instances. For example, when rendering all 1310 pages of the `pdf.pdf` document (from the test-suite) this patch reduces the number of `TextDecoder`/`TextEncoder` instances from `73` *each* to just a single one each. *Note:* Compared to PR 21801 the reduction is obviously much smaller, however it still seems worthwhile to avoid unnecessary object creation. --- src/core/obj_bin_transform_core.js | 7 +++--- src/display/obj_bin_transform_display.js | 27 +++++++++--------------- src/shared/obj_bin_transform_utils.js | 14 +++++++++++- test/unit/obj_bin_transform_spec.js | 7 +++--- 4 files changed, 31 insertions(+), 24 deletions(-) diff --git a/src/core/obj_bin_transform_core.js b/src/core/obj_bin_transform_core.js index 8704bc062b2ca..25dbbf63bce56 100644 --- a/src/core/obj_bin_transform_core.js +++ b/src/core/obj_bin_transform_core.js @@ -17,12 +17,13 @@ import { assert, FeatureTest } from "../shared/util.js"; import { CSS_FONT_INFO, FONT_INFO, + InfoUtils, PATTERN_INFO, SYSTEM_FONT_INFO, } from "../shared/obj_bin_transform_utils.js"; function compileCssFontInfo(info) { - const encoder = new TextEncoder(); + const { encoder } = InfoUtils; const encodedStrings = {}; let stringsLength = 0; for (const prop of CSS_FONT_INFO.strings) { @@ -48,7 +49,7 @@ function compileCssFontInfo(info) { } function compileSystemFontInfo(info) { - const encoder = new TextEncoder(); + const { encoder } = InfoUtils; const encodedStrings = {}; let stringsLength = 0; for (const prop of SYSTEM_FONT_INFO.strings) { @@ -106,7 +107,7 @@ function compileFontInfo(font) { ? compileCssFontInfo(font.cssFontInfo) : null; - const encoder = new TextEncoder(); + const { encoder } = InfoUtils; const encodedStrings = {}; let stringsLength = 0; for (const prop of FONT_INFO.strings) { diff --git a/src/display/obj_bin_transform_display.js b/src/display/obj_bin_transform_display.js index 9e21c7bb02a72..a584198f2eee0 100644 --- a/src/display/obj_bin_transform_display.js +++ b/src/display/obj_bin_transform_display.js @@ -17,6 +17,7 @@ import { assert, BBOX_INIT, FeatureTest, Util } from "../shared/util.js"; import { CSS_FONT_INFO, FONT_INFO, + InfoUtils, PATTERN_INFO, SYSTEM_FONT_INFO, } from "../shared/obj_bin_transform_utils.js"; @@ -24,8 +25,6 @@ import { class CssFontInfo { #buffer; - #decoder = new TextDecoder(); - #view; constructor(buffer) { @@ -35,14 +34,13 @@ class CssFontInfo { #readString(index) { assert(index < CSS_FONT_INFO.strings.length, "Invalid string index"); + const { decoder } = InfoUtils; let offset = 0; for (let i = 0; i < index; i++) { offset += this.#view.getUint32(offset) + 4; } const length = this.#view.getUint32(offset); - return this.#decoder.decode( - new Uint8Array(this.#buffer, offset + 4, length) - ); + return decoder.decode(new Uint8Array(this.#buffer, offset + 4, length)); } get fontFamily() { @@ -61,8 +59,6 @@ class CssFontInfo { class SystemFontInfo { #buffer; - #decoder = new TextDecoder(); - #view; constructor(buffer) { @@ -76,14 +72,13 @@ class SystemFontInfo { #readString(index) { assert(index < SYSTEM_FONT_INFO.strings.length, "Invalid string index"); + const { decoder } = InfoUtils; let offset = 5; for (let i = 0; i < index; i++) { offset += this.#view.getUint32(offset) + 4; } const length = this.#view.getUint32(offset); - return this.#decoder.decode( - new Uint8Array(this.#buffer, offset + 4, length) - ); + return decoder.decode(new Uint8Array(this.#buffer, offset + 4, length)); } get css() { @@ -103,15 +98,16 @@ class SystemFontInfo { } get style() { + const { decoder } = InfoUtils; let offset = 1; offset += 4 + this.#view.getUint32(offset); const styleLength = this.#view.getUint32(offset); - const style = this.#decoder.decode( + const style = decoder.decode( new Uint8Array(this.#buffer, offset + 4, styleLength) ); offset += 4 + styleLength; const weightLength = this.#view.getUint32(offset); - const weight = this.#decoder.decode( + const weight = decoder.decode( new Uint8Array(this.#buffer, offset + 4, weightLength) ); return { style, weight }; @@ -121,8 +117,6 @@ class SystemFontInfo { class FontInfo { #buffer; - #decoder = new TextDecoder(); - #view; constructor({ buffer, extra }) { @@ -242,14 +236,13 @@ class FontInfo { #readString(index) { assert(index < FONT_INFO.strings.length, "Invalid string index"); + const { decoder } = InfoUtils; let offset = FONT_INFO.OFFSET_STRINGS + 4; for (let i = 0; i < index; i++) { offset += this.#view.getUint32(offset) + 4; } const length = this.#view.getUint32(offset); - return this.#decoder.decode( - new Uint8Array(this.#buffer, offset + 4, length) - ); + return decoder.decode(new Uint8Array(this.#buffer, offset + 4, length)); } get fallbackName() { diff --git a/src/shared/obj_bin_transform_utils.js b/src/shared/obj_bin_transform_utils.js index 9682485efbcb0..1dd39a9d26416 100644 --- a/src/shared/obj_bin_transform_utils.js +++ b/src/shared/obj_bin_transform_utils.js @@ -13,6 +13,8 @@ * limitations under the License. */ +import { shadow } from "./util.js"; + class CSS_FONT_INFO { static strings = ["fontFamily", "fontWeight", "italicAngle"]; } @@ -68,4 +70,14 @@ class PATTERN_INFO { static N_FIGURES = 16; // number of figures } -export { CSS_FONT_INFO, FONT_INFO, PATTERN_INFO, SYSTEM_FONT_INFO }; +class InfoUtils { + static get decoder() { + return shadow(this, "decoder", new TextDecoder()); + } + + static get encoder() { + return shadow(this, "encoder", new TextEncoder()); + } +} + +export { CSS_FONT_INFO, FONT_INFO, InfoUtils, PATTERN_INFO, SYSTEM_FONT_INFO }; diff --git a/test/unit/obj_bin_transform_spec.js b/test/unit/obj_bin_transform_spec.js index a96c08c8e0c31..7094157d6db32 100644 --- a/test/unit/obj_bin_transform_spec.js +++ b/test/unit/obj_bin_transform_spec.js @@ -28,6 +28,7 @@ import { SystemFontInfo, } from "../../src/display/obj_bin_transform_display.js"; import { FeatureTest } from "../../src/shared/util.js"; +import { InfoUtils } from "../../src/shared/obj_bin_transform_utils.js"; describe("obj_bin_transform", function () { describe("Font data", function () { @@ -80,7 +81,7 @@ describe("obj_bin_transform", function () { describe("font data serialization and deserialization", function () { describe("CssFontInfo", function () { it("must roundtrip correctly for CssFontInfo", function () { - const encoder = new TextEncoder(); + const { encoder } = InfoUtils; let sizeEstimate = 0; for (const string of ["Sample Family", "not a number", "angle"]) { sizeEstimate += 4 + encoder.encode(string).length; @@ -97,7 +98,7 @@ describe("obj_bin_transform", function () { describe("SystemFontInfo", function () { it("must roundtrip correctly for SystemFontInfo", function () { - const encoder = new TextEncoder(); + const { encoder } = InfoUtils; let sizeEstimate = 1 + 4; for (const string of [ "some string", @@ -127,7 +128,7 @@ describe("obj_bin_transform", function () { describe("FontInfo", function () { it("must roundtrip correctly for FontInfo", function () { let sizeEstimate = 92; // fixed offset until the strings - const encoder = new TextEncoder(); + const { encoder } = InfoUtils; sizeEstimate += 4 + 4 * (4 + encoder.encode("string").length); sizeEstimate += 4 + 4; // cssFontInfo and systemFontInfo sizeEstimate += 4 + fontInfo.data.length;