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.
This commit is contained in:
greymoth-jp 2026-07-04 21:35:29 +09:00
parent 36835d919d
commit 2bbf32d83d
4 changed files with 47 additions and 1 deletions

View File

@ -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++;
}

View File

@ -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",

44
test/unit/fonts_spec.js Normal file
View File

@ -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"]);
});
});
});

View File

@ -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",