From 2bbf32d83d9e3b944e550412314dd13aa9966b25 Mon Sep 17 00:00:00 2001 From: greymoth-jp <246701683+greymoth-jp@users.noreply.github.com> Date: Sat, 4 Jul 2026 21:35:29 +0900 Subject: [PATCH] Do not drop the character after U+FFFE or U+FFFF in Font.prototype.encodeString encodeString has the same surrogate-pair guard that encodeToXmlString had before #21526: `unicode > 0xd7ff && (unicode < 0xe000 || unicode > 0xfffd)`. That predicate is also true for U+FFFE and U+FFFF, which are single UTF-16 code units, not surrogate pairs. The extra `i++` then steps over the character that follows them, so it is silently dropped from the font-encoded output used when saving or printing a PDF. For example, encoding a string that is U+FFFF followed by "A", with a font that has a glyph for both, returns an encoded result ending in "A" on this branch but drops the "A" on master. Same fix as #21526: the correct test for a real surrogate pair is `unicode > 0xffff`, since codePointAt only returns a value at or above 0x10000 for an actual pair. This keeps existing behavior for real surrogate pairs (e.g. emoji) and the U+FFFD boundary, and only stops the character after U+FFFE/U+FFFF from being dropped. Added test/unit/fonts_spec.js, since Font.prototype.encodeString had no direct unit test. It calls the method on a minimal fake `this` (only toUnicode/cMap are read), since building a full Font requires a complete properties/font-file setup that this bug doesn't depend on. --- src/core/fonts.js | 2 +- test/unit/clitests.json | 1 + test/unit/fonts_spec.js | 44 +++++++++++++++++++++++++++++++++++++++ test/unit/jasmine-boot.js | 1 + 4 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 test/unit/fonts_spec.js diff --git a/src/core/fonts.js b/src/core/fonts.js index c1a213fed..23f3ebf74 100644 --- a/src/core/fonts.js +++ b/src/core/fonts.js @@ -3710,7 +3710,7 @@ class Font { for (let i = 0, ii = str.length; i < ii; i++) { const unicode = str.codePointAt(i); - if (unicode > 0xd7ff && (unicode < 0xe000 || unicode > 0xfffd)) { + if (unicode > 0xffff) { // unicode is represented by two uint16 i++; } diff --git a/test/unit/clitests.json b/test/unit/clitests.json index 7f392695c..f47635abc 100644 --- a/test/unit/clitests.json +++ b/test/unit/clitests.json @@ -26,6 +26,7 @@ "event_utils_spec.js", "fetch_stream_spec.js", "font_substitutions_spec.js", + "fonts_spec.js", "image_utils_spec.js", "message_handler_spec.js", "metadata_spec.js", diff --git a/test/unit/fonts_spec.js b/test/unit/fonts_spec.js new file mode 100644 index 000000000..2f4d2cffa --- /dev/null +++ b/test/unit/fonts_spec.js @@ -0,0 +1,44 @@ +/* Copyright 2026 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Font } from "../../src/core/fonts.js"; +import { IdentityToUnicodeMap } from "../../src/core/to_unicode_map.js"; + +describe("Font", () => { + describe("encodeString", () => { + // `encodeString` only reads `this.toUnicode` and `this.cMap`, so a + // full `Font` (which needs a complete properties/font-file setup) isn't + // necessary to exercise it in isolation. + function encodeString(str, { cMap = null } = {}) { + const fakeFont = { + toUnicode: new IdentityToUnicodeMap(0, 0x10ffff), + cMap, + }; + return Font.prototype.encodeString.call(fakeFont, str); + } + + it("should keep the character after U+FFFE or U+FFFF", () => { + expect(encodeString("￿A")).toEqual(["\xffA"]); + expect(encodeString("￾B")).toEqual(["\xfeB"]); + }); + + it("should still treat a real surrogate pair as one code point", () => { + // U+1F602 ("😂") is genuinely represented by a surrogate pair; the + // character after it must still be kept, and the pair itself must + // not be split into its two unpaired halves. + expect(encodeString("😂C")).toEqual(["\x02C"]); + }); + }); +}); diff --git a/test/unit/jasmine-boot.js b/test/unit/jasmine-boot.js index a0d1842e3..836a9b52d 100644 --- a/test/unit/jasmine-boot.js +++ b/test/unit/jasmine-boot.js @@ -72,6 +72,7 @@ async function initializePDFJS(callback) { "pdfjs-test/unit/event_utils_spec.js", "pdfjs-test/unit/fetch_stream_spec.js", "pdfjs-test/unit/font_substitutions_spec.js", + "pdfjs-test/unit/fonts_spec.js", "pdfjs-test/unit/image_utils_spec.js", "pdfjs-test/unit/message_handler_spec.js", "pdfjs-test/unit/metadata_spec.js",