diff --git a/src/core/evaluator.js b/src/core/evaluator.js index d38fab6b0fe6b..85425374716e8 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -40,6 +40,7 @@ import { lookupMatrix, lookupNormalRect, } from "./core_utils.js"; +import { FontInfo, FontPathInfo } from "../shared/obj-bin-transform.js"; import { getEncoding, MacRomanEncoding, @@ -72,7 +73,6 @@ import { BaseStream } from "./base_stream.js"; import { bidi } from "./bidi.js"; import { ColorSpace } from "./colorspace.js"; import { ColorSpaceUtils } from "./colorspace_utils.js"; -import { FontInfo } from "../shared/obj-bin-transform.js"; import { getFontSubstitution } from "./font_substitutions.js"; import { getGlyphsUnicode } from "./glyphlist.js"; import { getMetrics } from "./metrics.js"; @@ -4660,11 +4660,8 @@ class PartialEvaluator { if (font.renderer.hasBuiltPath(fontChar)) { return; } - handler.send("commonobj", [ - glyphName, - "FontPath", - font.renderer.getPathJs(fontChar), - ]); + const buffer = FontPathInfo.write(font.renderer.getPathJs(fontChar)); + handler.send("commonobj", [glyphName, "FontPath", buffer], [buffer]); } catch (reason) { if (evaluatorOptions.ignoreErrors) { warn(`buildFontPaths - ignoring ${glyphName} glyph: "${reason}".`); diff --git a/src/core/font_renderer.js b/src/core/font_renderer.js index 23c8b218d3cdc..3d578b5fb1444 100644 --- a/src/core/font_renderer.js +++ b/src/core/font_renderer.js @@ -770,10 +770,6 @@ class Commands { restore() { this.currentTransform = this.transformStack.pop() || [1, 0, 0, 1, 0, 0]; } - - getSVG() { - return this.cmds.join(""); - } } class CompiledFont { @@ -836,7 +832,7 @@ class CompiledFont { this.compileGlyphImpl(code, cmds, glyphId); cmds.add("Z"); - return cmds.getSVG(); + return cmds.cmds; } compileGlyphImpl() { diff --git a/src/display/api.js b/src/display/api.js index 336472b47ba39..5556e253a00ea 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -45,6 +45,7 @@ import { StatTimer, } from "./display_utils.js"; import { FontFaceObject, FontLoader } from "./font_loader.js"; +import { FontInfo, FontPathInfo } from "../shared/obj-bin-transform.js"; import { getDataProp, getFactoryUrlProp, @@ -67,7 +68,6 @@ import { DOMCMapReaderFactory } from "display-cmap_reader_factory"; import { DOMFilterFactory } from "./filter_factory.js"; import { DOMStandardFontDataFactory } from "display-standard_fontdata_factory"; import { DOMWasmFactory } from "display-wasm_factory"; -import { FontInfo } from "../shared/obj-bin-transform.js"; import { GlobalWorkerOptions } from "./worker_options.js"; import { Metadata } from "./metadata.js"; import { OptionalContentConfig } from "./optional_content_config.js"; @@ -2803,6 +2803,8 @@ class WorkerTransport { } break; case "FontPath": + this.commonObjs.resolve(id, new FontPathInfo(exportedData)); + break; case "Image": case "Pattern": this.commonObjs.resolve(id, exportedData); diff --git a/src/display/font_loader.js b/src/display/font_loader.js index 7d48d8c71d13d..e1379d132eef2 100644 --- a/src/display/font_loader.js +++ b/src/display/font_loader.js @@ -435,7 +435,7 @@ class FontFaceObject { } catch (ex) { warn(`getPathGenerator - ignoring character: "${ex}".`); } - const path = new Path2D(cmds || ""); + const path = new Path2D(cmds?.getSVG() || ""); if (!this.fontExtraProperties) { // Remove the raw path-string, since we don't need it anymore. diff --git a/src/shared/obj-bin-transform.js b/src/shared/obj-bin-transform.js index f47f1769b8de4..dbd2d41cc63be 100644 --- a/src/shared/obj-bin-transform.js +++ b/src/shared/obj-bin-transform.js @@ -606,4 +606,79 @@ class FontInfo { } } -export { CssFontInfo, FontInfo, SystemFontInfo }; +class FontPathInfo { + static write(path) { + let lengthEstimate = 0; + const commands = []; + for (const cmd of path) { + const code = cmd.charCodeAt(0); + let args = null; + if (code === 67 || code === 76 || code === 77) { + args = cmd.slice(1).split(" "); + lengthEstimate += 1 + args.length * 8; + } else if (code === 90) { + lengthEstimate += 1; + } else { + throw new Error(`Invalid path command: ${cmd}`); + } + commands.push({ code, args }); + } + + const buffer = new ArrayBuffer(4 + lengthEstimate); + const view = new DataView(buffer); + let offset = 0; + + view.setUint32(offset, commands.length); + offset += 4; + for (const { code, args } of commands) { + view.setUint8(offset, code); + offset += 1; + if (args) { + for (const arg of args) { + view.setFloat64(offset, parseFloat(arg), true); + offset += 8; + } + } + } + + assert(offset === buffer.byteLength, "FontPathInfo.write: Buffer overflow"); + return buffer; + } + + #buffer; + + #view; + + constructor(buffer) { + this.#buffer = buffer; + this.#view = new DataView(this.#buffer); + } + + getSVG() { + const length = this.#view.getUint32(0); + const cmds = []; + let offset = 4; + for (let i = 0; i < length; i++) { + const code = String.fromCharCode(this.#view.getUint8(offset)); + offset += 1; + // eslint-disable-next-line no-nested-ternary + const numArgs = code === "M" || code === "L" ? 2 : code === "C" ? 6 : 0; + let args = null; + if (numArgs > 0) { + args = []; + for (let j = 0; j < numArgs; j++) { + args.push(this.#view.getFloat64(offset, true)); + offset += 8; + } + } + cmds.push(code + (args ? args.join(" ") : "")); + } + assert( + offset === this.#buffer.byteLength, + "FontPathInfo.toString: Buffer overflow" + ); + return cmds.join(""); + } +} + +export { CssFontInfo, FontInfo, FontPathInfo, SystemFontInfo }; diff --git a/test/unit/bin_font_info_spec.js b/test/unit/bin_font_info_spec.js deleted file mode 100644 index 9b46462ef5f1b..0000000000000 --- a/test/unit/bin_font_info_spec.js +++ /dev/null @@ -1,164 +0,0 @@ -/* Copyright 2025 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 { - CssFontInfo, - FontInfo, - SystemFontInfo, -} from "../../src/shared/obj-bin-transform.js"; - -const cssFontInfo = { - fontFamily: "Sample Family", - fontWeight: "not a number", - italicAngle: "angle", - uselessProp: "doesn't matter", -}; - -const systemFontInfo = { - guessFallback: false, - css: "some string", - loadedName: "another string", - baseFontName: "base name", - src: "source", - style: { - style: "normal", - weight: "400", - uselessProp: "doesn't matter", - }, - uselessProp: "doesn't matter", -}; - -const fontInfo = { - black: true, - bold: true, - disableFontFace: true, - fontExtraProperties: true, - isInvalidPDFjsFont: true, - isType3Font: true, - italic: true, - missingFile: true, - remeasure: true, - vertical: true, - ascent: 1, - defaultWidth: 1, - descent: 1, - bbox: [1, 1, 1, 1], - fontMatrix: [1, 1, 1, 1, 1, 1], - defaultVMetrics: [1, 1, 1], - fallbackName: "string", - loadedName: "string", - mimetype: "string", - name: "string", - data: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), - uselessProp: "something", -}; - -describe("font data serialization and deserialization", function () { - describe("CssFontInfo", function () { - it("must roundtrip correctly for CssFontInfo", function () { - const encoder = new TextEncoder(); - let sizeEstimate = 0; - for (const string of ["Sample Family", "not a number", "angle"]) { - sizeEstimate += 4 + encoder.encode(string).length; - } - const buffer = CssFontInfo.write(cssFontInfo); - expect(buffer.byteLength).toEqual(sizeEstimate); - const deserialized = new CssFontInfo(buffer); - expect(deserialized.fontFamily).toEqual("Sample Family"); - expect(deserialized.fontWeight).toEqual("not a number"); - expect(deserialized.italicAngle).toEqual("angle"); - expect(deserialized.uselessProp).toBeUndefined(); - }); - }); - - describe("SystemFontInfo", function () { - it("must roundtrip correctly for SystemFontInfo", function () { - const encoder = new TextEncoder(); - let sizeEstimate = 1 + 4; - for (const string of [ - "some string", - "another string", - "base name", - "source", - "normal", - "400", - ]) { - sizeEstimate += 4 + encoder.encode(string).length; - } - const buffer = SystemFontInfo.write(systemFontInfo); - expect(buffer.byteLength).toEqual(sizeEstimate); - const deserialized = new SystemFontInfo(buffer); - expect(deserialized.guessFallback).toEqual(false); - expect(deserialized.css).toEqual("some string"); - expect(deserialized.loadedName).toEqual("another string"); - expect(deserialized.baseFontName).toEqual("base name"); - expect(deserialized.src).toEqual("source"); - expect(deserialized.style.style).toEqual("normal"); - expect(deserialized.style.weight).toEqual("400"); - expect(deserialized.style.uselessProp).toBeUndefined(); - expect(deserialized.uselessProp).toBeUndefined(); - }); - }); - - describe("FontInfo", function () { - it("must roundtrip correctly for FontInfo", function () { - let sizeEstimate = 92; // fixed offset until the strings - const encoder = new TextEncoder(); - sizeEstimate += 4 + 4 * (4 + encoder.encode("string").length); - sizeEstimate += 4 + 4; // cssFontInfo and systemFontInfo - sizeEstimate += 4 + fontInfo.data.length; - const buffer = FontInfo.write(fontInfo); - expect(buffer.byteLength).toEqual(sizeEstimate); - const deserialized = new FontInfo({ data: buffer }); - expect(deserialized.black).toEqual(true); - expect(deserialized.bold).toEqual(true); - expect(deserialized.disableFontFace).toEqual(true); - expect(deserialized.fontExtraProperties).toEqual(true); - expect(deserialized.isInvalidPDFjsFont).toEqual(true); - expect(deserialized.isType3Font).toEqual(true); - expect(deserialized.italic).toEqual(true); - expect(deserialized.missingFile).toEqual(true); - expect(deserialized.remeasure).toEqual(true); - expect(deserialized.vertical).toEqual(true); - expect(deserialized.ascent).toEqual(1); - expect(deserialized.defaultWidth).toEqual(1); - expect(deserialized.descent).toEqual(1); - expect(deserialized.bbox).toEqual([1, 1, 1, 1]); - expect(deserialized.fontMatrix).toEqual([1, 1, 1, 1, 1, 1]); - expect(deserialized.defaultVMetrics).toEqual([1, 1, 1]); - expect(deserialized.fallbackName).toEqual("string"); - expect(deserialized.loadedName).toEqual("string"); - expect(deserialized.mimetype).toEqual("string"); - expect(deserialized.name).toEqual("string"); - expect(Array.from(deserialized.data)).toEqual([ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, - ]); - expect(deserialized.uselessProp).toBeUndefined(); - expect(deserialized.cssFontInfo).toBeNull(); - expect(deserialized.systemFontInfo).toBeNull(); - }); - - it("nesting should work as expected", function () { - const buffer = FontInfo.write({ - ...fontInfo, - cssFontInfo, - systemFontInfo, - }); - const deserialized = new FontInfo({ data: buffer }); - expect(deserialized.cssFontInfo.fontWeight).toEqual("not a number"); - expect(deserialized.systemFontInfo.src).toEqual("source"); - }); - }); -}); diff --git a/test/unit/clitests.json b/test/unit/clitests.json index 5533ddd5fab5f..2a5f7e25e19ba 100644 --- a/test/unit/clitests.json +++ b/test/unit/clitests.json @@ -10,7 +10,6 @@ "app_options_spec.js", "autolinker_spec.js", "bidi_spec.js", - "bin_font_info_spec.js", "canvas_factory_spec.js", "cff_parser_spec.js", "cmap_spec.js", @@ -33,6 +32,7 @@ "murmurhash3_spec.js", "network_utils_spec.js", "node_stream_spec.js", + "obj_bin_transform_spec.js", "parser_spec.js", "pdf.image_decoders_spec.js", "pdf.worker_spec.js", diff --git a/test/unit/jasmine-boot.js b/test/unit/jasmine-boot.js index e72990d3f13c7..73abbf9f0d6bd 100644 --- a/test/unit/jasmine-boot.js +++ b/test/unit/jasmine-boot.js @@ -53,7 +53,6 @@ async function initializePDFJS(callback) { "pdfjs-test/unit/app_options_spec.js", "pdfjs-test/unit/autolinker_spec.js", "pdfjs-test/unit/bidi_spec.js", - "pdfjs-test/unit/bin_font_info_spec.js", "pdfjs-test/unit/canvas_factory_spec.js", "pdfjs-test/unit/cff_parser_spec.js", "pdfjs-test/unit/cmap_spec.js", @@ -76,6 +75,7 @@ async function initializePDFJS(callback) { "pdfjs-test/unit/murmurhash3_spec.js", "pdfjs-test/unit/network_spec.js", "pdfjs-test/unit/network_utils_spec.js", + "pdfjs-test/unit/obj_bin_transform_spec.js", "pdfjs-test/unit/parser_spec.js", "pdfjs-test/unit/pdf.image_decoders_spec.js", "pdfjs-test/unit/pdf.worker_spec.js", diff --git a/test/unit/obj_bin_transform_spec.js b/test/unit/obj_bin_transform_spec.js new file mode 100644 index 0000000000000..00a8571a397b9 --- /dev/null +++ b/test/unit/obj_bin_transform_spec.js @@ -0,0 +1,196 @@ +/* Copyright 2025 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 { + CssFontInfo, + FontInfo, + FontPathInfo, + SystemFontInfo, +} from "../../src/shared/obj-bin-transform.js"; + +describe("obj-bin-transform", function () { + describe("Font data", function () { + const cssFontInfo = { + fontFamily: "Sample Family", + fontWeight: "not a number", + italicAngle: "angle", + uselessProp: "doesn't matter", + }; + + const systemFontInfo = { + guessFallback: false, + css: "some string", + loadedName: "another string", + baseFontName: "base name", + src: "source", + style: { + style: "normal", + weight: "400", + uselessProp: "doesn't matter", + }, + uselessProp: "doesn't matter", + }; + + const fontInfo = { + black: true, + bold: true, + disableFontFace: true, + fontExtraProperties: true, + isInvalidPDFjsFont: true, + isType3Font: true, + italic: true, + missingFile: true, + remeasure: true, + vertical: true, + ascent: 1, + defaultWidth: 1, + descent: 1, + bbox: [1, 1, 1, 1], + fontMatrix: [1, 1, 1, 1, 1, 1], + defaultVMetrics: [1, 1, 1], + fallbackName: "string", + loadedName: "string", + mimetype: "string", + name: "string", + data: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + uselessProp: "something", + }; + + describe("font data serialization and deserialization", function () { + describe("CssFontInfo", function () { + it("must roundtrip correctly for CssFontInfo", function () { + const encoder = new TextEncoder(); + let sizeEstimate = 0; + for (const string of ["Sample Family", "not a number", "angle"]) { + sizeEstimate += 4 + encoder.encode(string).length; + } + const buffer = CssFontInfo.write(cssFontInfo); + expect(buffer.byteLength).toEqual(sizeEstimate); + const deserialized = new CssFontInfo(buffer); + expect(deserialized.fontFamily).toEqual("Sample Family"); + expect(deserialized.fontWeight).toEqual("not a number"); + expect(deserialized.italicAngle).toEqual("angle"); + expect(deserialized.uselessProp).toBeUndefined(); + }); + }); + + describe("SystemFontInfo", function () { + it("must roundtrip correctly for SystemFontInfo", function () { + const encoder = new TextEncoder(); + let sizeEstimate = 1 + 4; + for (const string of [ + "some string", + "another string", + "base name", + "source", + "normal", + "400", + ]) { + sizeEstimate += 4 + encoder.encode(string).length; + } + const buffer = SystemFontInfo.write(systemFontInfo); + expect(buffer.byteLength).toEqual(sizeEstimate); + const deserialized = new SystemFontInfo(buffer); + expect(deserialized.guessFallback).toEqual(false); + expect(deserialized.css).toEqual("some string"); + expect(deserialized.loadedName).toEqual("another string"); + expect(deserialized.baseFontName).toEqual("base name"); + expect(deserialized.src).toEqual("source"); + expect(deserialized.style.style).toEqual("normal"); + expect(deserialized.style.weight).toEqual("400"); + expect(deserialized.style.uselessProp).toBeUndefined(); + expect(deserialized.uselessProp).toBeUndefined(); + }); + }); + + describe("FontInfo", function () { + it("must roundtrip correctly for FontInfo", function () { + let sizeEstimate = 92; // fixed offset until the strings + const encoder = new TextEncoder(); + sizeEstimate += 4 + 4 * (4 + encoder.encode("string").length); + sizeEstimate += 4 + 4; // cssFontInfo and systemFontInfo + sizeEstimate += 4 + fontInfo.data.length; + const buffer = FontInfo.write(fontInfo); + expect(buffer.byteLength).toEqual(sizeEstimate); + const deserialized = new FontInfo({ data: buffer }); + expect(deserialized.black).toEqual(true); + expect(deserialized.bold).toEqual(true); + expect(deserialized.disableFontFace).toEqual(true); + expect(deserialized.fontExtraProperties).toEqual(true); + expect(deserialized.isInvalidPDFjsFont).toEqual(true); + expect(deserialized.isType3Font).toEqual(true); + expect(deserialized.italic).toEqual(true); + expect(deserialized.missingFile).toEqual(true); + expect(deserialized.remeasure).toEqual(true); + expect(deserialized.vertical).toEqual(true); + expect(deserialized.ascent).toEqual(1); + expect(deserialized.defaultWidth).toEqual(1); + expect(deserialized.descent).toEqual(1); + expect(deserialized.bbox).toEqual([1, 1, 1, 1]); + expect(deserialized.fontMatrix).toEqual([1, 1, 1, 1, 1, 1]); + expect(deserialized.defaultVMetrics).toEqual([1, 1, 1]); + expect(deserialized.fallbackName).toEqual("string"); + expect(deserialized.loadedName).toEqual("string"); + expect(deserialized.mimetype).toEqual("string"); + expect(deserialized.name).toEqual("string"); + expect(Array.from(deserialized.data)).toEqual([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + ]); + expect(deserialized.uselessProp).toBeUndefined(); + expect(deserialized.cssFontInfo).toBeNull(); + expect(deserialized.systemFontInfo).toBeNull(); + }); + + it("nesting should work as expected", function () { + const buffer = FontInfo.write({ + ...fontInfo, + cssFontInfo, + systemFontInfo, + }); + const deserialized = new FontInfo({ data: buffer }); + expect(deserialized.cssFontInfo.fontWeight).toEqual("not a number"); + expect(deserialized.systemFontInfo.src).toEqual("source"); + }); + }); + }); + }); + + describe("FontPath data", function () { + const path = [ + "M0.214 0.27", + "L0.23 0.33", + "C0.248 0.395 0.265 0.47100000000000003 0.281 0.54", + "L0.28500000000000003 0.54", + "C0.302 0.47200000000000003 0.32 0.395 0.338 0.33", + "L0.353 0.27", + "L0.214 0.27", + "M0.423 0", + "L0.579 0", + "L0.375 0.652", + "L0.198 0.652", + "L-0.006 0", + "L0.14400000000000002 0", + "L0.184 0.155", + "L0.383 0.155", + "Z", + ]; + + it("should create a FontPathInfo instance from an array of path commands", function () { + const buffer = FontPathInfo.write(path); + const fontPathInfo = new FontPathInfo(buffer); + expect(fontPathInfo.getSVG()).toEqual(path.join("")); + }); + }); +});