/* 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, PatternInfo, SystemFontInfo, } from "../../src/shared/obj-bin-transform.js"; import { FeatureTest, MeshFigureType } from "../../src/shared/util.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("Pattern data", function () { const axialPatternIR = [ "RadialAxial", "axial", [0, 0, 100, 50], [ [0, "#ff0000"], [0.5, "#00ff00"], [1, "#0000ff"], ], [10, 20], [90, 40], null, null, ]; const radialPatternIR = [ "RadialAxial", "radial", [5, 5, 95, 45], [ [0, "#ffff00"], [0.3, "#ff00ff"], [0.7, "#00ffff"], [1, "#ffffff"], ], [25, 25], [75, 35], 5, 25, ]; const meshPatternIR = [ "Mesh", 4, new Float32Array([ 0, 0, 50, 0, 100, 0, 0, 50, 50, 50, 100, 50, 0, 100, 50, 100, 100, 100, ]), new Uint8Array([ 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 0, 128, 128, 128, 255, 0, 255, 0, 255, 255, 255, 128, 0, 128, 0, 128, ]), [ { type: MeshFigureType.TRIANGLES, coords: new Int32Array([0, 2, 4, 6, 8, 10, 12, 14, 16]), colors: new Int32Array([0, 2, 4, 6, 8, 10, 12, 14, 16]), }, { type: MeshFigureType.LATTICE, coords: new Int32Array([0, 2, 4, 6, 8, 10]), colors: new Int32Array([0, 2, 4, 6, 8, 10]), verticesPerRow: 3, }, ], [0, 0, 100, 100], [0, 0, 100, 100], [128, 128, 128], ]; describe("Pattern serialization and deserialization", function () { it("must serialize and deserialize axial gradients correctly", function () { const buffer = PatternInfo.write(axialPatternIR); expect(buffer).toBeInstanceOf(ArrayBuffer); expect(buffer.byteLength).toBeGreaterThan(0); const patternInfo = new PatternInfo(buffer); const reconstructedIR = patternInfo.getIR(); expect(reconstructedIR[0]).toEqual("RadialAxial"); expect(reconstructedIR[1]).toEqual("axial"); expect(reconstructedIR[2]).toEqual([0, 0, 100, 50]); expect(reconstructedIR[3]).toEqual([ [0, "#ff0000"], [0.5, "#00ff00"], [1, "#0000ff"], ]); expect(reconstructedIR[4]).toEqual([10, 20]); expect(reconstructedIR[5]).toEqual([90, 40]); expect(reconstructedIR[6]).toBeNull(); expect(reconstructedIR[7]).toBeNull(); }); it("must serialize and deserialize radial gradients correctly", function () { const buffer = PatternInfo.write(radialPatternIR); expect(buffer).toBeInstanceOf(ArrayBuffer); expect(buffer.byteLength).toBeGreaterThan(0); const patternInfo = new PatternInfo(buffer); const reconstructedIR = patternInfo.getIR(); expect(reconstructedIR[0]).toEqual("RadialAxial"); expect(reconstructedIR[1]).toEqual("radial"); expect(reconstructedIR[2]).toEqual([5, 5, 95, 45]); expect(reconstructedIR[3]).toEqual([ [0, "#ffff00"], jasmine.objectContaining([jasmine.any(Number), "#ff00ff"]), jasmine.objectContaining([jasmine.any(Number), "#00ffff"]), [1, "#ffffff"], ]); expect(reconstructedIR[4]).toEqual([25, 25]); expect(reconstructedIR[5]).toEqual([75, 35]); expect(reconstructedIR[6]).toEqual(5); expect(reconstructedIR[7]).toEqual(25); }); it("must serialize and deserialize mesh patterns with figures correctly", function () { const buffer = PatternInfo.write(meshPatternIR); expect(buffer).toBeInstanceOf(ArrayBuffer); expect(buffer.byteLength).toBeGreaterThan(0); const patternInfo = new PatternInfo(buffer); const reconstructedIR = patternInfo.getIR(); expect(reconstructedIR[0]).toEqual("Mesh"); expect(reconstructedIR[1]).toEqual(4); expect(reconstructedIR[2]).toBeInstanceOf(Float32Array); expect(Array.from(reconstructedIR[2])).toEqual( Array.from(meshPatternIR[2]) ); expect(reconstructedIR[3]).toBeInstanceOf(Uint8Array); expect(Array.from(reconstructedIR[3])).toEqual( Array.from(meshPatternIR[3]) ); expect(reconstructedIR[4].length).toEqual(2); const fig1 = reconstructedIR[4][0]; expect(fig1.type).toEqual(MeshFigureType.TRIANGLES); expect(fig1.coords).toBeInstanceOf(Int32Array); expect(Array.from(fig1.coords)).toEqual([ 0, 2, 4, 6, 8, 10, 12, 14, 16, ]); expect(fig1.colors).toBeInstanceOf(Int32Array); expect(Array.from(fig1.colors)).toEqual([ 0, 2, 4, 6, 8, 10, 12, 14, 16, ]); expect(fig1.verticesPerRow).toBeUndefined(); const fig2 = reconstructedIR[4][1]; expect(fig2.type).toEqual(MeshFigureType.LATTICE); expect(fig2.coords).toBeInstanceOf(Int32Array); expect(Array.from(fig2.coords)).toEqual([0, 2, 4, 6, 8, 10]); expect(fig2.colors).toBeInstanceOf(Int32Array); expect(Array.from(fig2.colors)).toEqual([0, 2, 4, 6, 8, 10]); expect(fig2.verticesPerRow).toEqual(3); expect(reconstructedIR[5]).toEqual([0, 0, 100, 100]); expect(reconstructedIR[6]).toEqual([0, 0, 100, 100]); expect(reconstructedIR[7]).toBeInstanceOf(Uint8Array); expect(Array.from(reconstructedIR[7])).toEqual([128, 128, 128]); }); it("must handle mesh patterns with no figures", function () { const noFiguresIR = [ "Mesh", 4, new Float32Array([0, 0, 10, 10]), new Uint8Array([255, 0, 0]), [], [0, 0, 10, 10], [0, 0, 10, 10], null, ]; const buffer = PatternInfo.write(noFiguresIR); const patternInfo = new PatternInfo(buffer); const reconstructedIR = patternInfo.getIR(); expect(reconstructedIR[4]).toEqual([]); expect(reconstructedIR[7]).toBeNull(); // background should be null }); it("must preserve figure data integrity across serialization", function () { const buffer = PatternInfo.write(meshPatternIR); const patternInfo = new PatternInfo(buffer); const reconstructedIR = patternInfo.getIR(); // Verify data integrity by checking exact values const originalFig = meshPatternIR[4][0]; const reconstructedFig = reconstructedIR[4][0]; for (let i = 0; i < originalFig.coords.length; i++) { expect(reconstructedFig.coords[i]).toEqual(originalFig.coords[i]); } for (let i = 0; i < originalFig.colors.length; i++) { expect(reconstructedFig.colors[i]).toEqual(originalFig.colors[i]); } }); it("must calculate correct buffer sizes for different pattern types", function () { const axialBuffer = PatternInfo.write(axialPatternIR); const radialBuffer = PatternInfo.write(radialPatternIR); const meshBuffer = PatternInfo.write(meshPatternIR); expect(axialBuffer.byteLength).toBeLessThan(radialBuffer.byteLength); expect(meshBuffer.byteLength).toBeGreaterThan(axialBuffer.byteLength); expect(meshBuffer.byteLength).toBeGreaterThan(radialBuffer.byteLength); }); it("must handle figures with different type enums correctly", function () { const customFiguresIR = [ "Mesh", 6, new Float32Array([0, 0, 10, 10]), new Uint8Array([255, 128, 64]), [ { type: MeshFigureType.PATCH, coords: new Int32Array([0, 2]), colors: new Int32Array([0, 2]), }, { type: MeshFigureType.TRIANGLES, coords: new Int32Array([0]), colors: new Int32Array([0]), }, ], [0, 0, 10, 10], null, null, ]; const buffer = PatternInfo.write(customFiguresIR); const patternInfo = new PatternInfo(buffer); const reconstructedIR = patternInfo.getIR(); expect(reconstructedIR[4].length).toEqual(2); expect(reconstructedIR[4][0].type).toEqual(MeshFigureType.PATCH); expect(reconstructedIR[4][1].type).toEqual(MeshFigureType.TRIANGLES); }); it("must handle mesh patterns with different background values", function () { const meshWithBgIR = [ "Mesh", 4, new Float32Array([0, 0, 10, 10]), new Uint8Array([255, 0, 0]), [], [0, 0, 10, 10], [0, 0, 10, 10], new Uint8Array([255, 128, 64]), ]; const buffer = PatternInfo.write(meshWithBgIR); const patternInfo = new PatternInfo(buffer); const reconstructedIR = patternInfo.getIR(); expect(reconstructedIR[7]).toBeInstanceOf(Uint8Array); expect(Array.from(reconstructedIR[7])).toEqual([255, 128, 64]); const meshNoBgIR = [ "Mesh", 5, new Float32Array([0, 0, 5, 5]), new Uint8Array([0, 255, 0]), [], [0, 0, 5, 5], null, null, ]; const buffer2 = PatternInfo.write(meshNoBgIR); const patternInfo2 = new PatternInfo(buffer2); const reconstructedIR2 = patternInfo2.getIR(); expect(reconstructedIR2[7]).toBeNull(); }); it("must calculate bounds correctly from coordinates", function () { const customMeshIR = [ "Mesh", 4, new Float32Array([-10, -5, 20, 15, 0, 30]), new Uint8Array([255, 0, 0, 0, 255, 0, 0, 0, 255]), [], null, null, null, ]; const buffer = PatternInfo.write(customMeshIR); const patternInfo = new PatternInfo(buffer); const reconstructedIR = patternInfo.getIR(); expect(reconstructedIR[5]).toEqual([-10, -5, 20, 30]); expect(reconstructedIR[7]).toBeNull(); }); }); }); describe("FontPath data", function () { const path = FeatureTest.isFloat16ArraySupported ? new Float16Array([ 0.214, 0.27, 0.23, 0.33, 0.248, 0.395, 0.265, 0.471, 0.281, 0.54, 0.285, 0.54, 0.302, 0.472, 0.32, 0.395, 0.338, 0.33, 0.353, 0.27, 0.214, 0.27, 0.423, 0, 0.579, 0, 0.375, 0.652, 0.198, 0.652, -0.006, 0, 0.144, 0, 0.184, 0.155, 0.383, 0.155, ]) : new Float32Array([ 0.214, 0.27, 0.23, 0.33, 0.248, 0.395, 0.265, 0.471, 0.281, 0.54, 0.285, 0.54, 0.302, 0.472, 0.32, 0.395, 0.338, 0.33, 0.353, 0.27, 0.214, 0.27, 0.423, 0, 0.579, 0, 0.375, 0.652, 0.198, 0.652, -0.006, 0, 0.144, 0, 0.184, 0.155, 0.383, 0.155, ]); 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.path).toEqual(path); }); }); });