diff --git a/src/core/evaluator.js b/src/core/evaluator.js index d38fab6b0fe6b..33893205a8744 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -40,6 +40,7 @@ import { lookupMatrix, lookupNormalRect, } from "./core_utils.js"; +import { FontInfo, PatternInfo } 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"; @@ -1517,7 +1517,10 @@ class PartialEvaluator { localShadingPatternCache.set(shading, id); if (this.parsingType3Font) { - this.handler.send("commonobj", [id, "Pattern", patternIR]); + const transfers = []; + const patternBuffer = PatternInfo.write(patternIR); + transfers.push(patternBuffer); + this.handler.send("commonobj", [id, "Pattern", patternBuffer], transfers); } else { this.handler.send("obj", [id, this.pageIndex, "Pattern", patternIR]); } diff --git a/src/display/api.js b/src/display/api.js index 336472b47ba39..149ceb2375cd7 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, PatternInfo } 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"; @@ -2804,9 +2804,12 @@ class WorkerTransport { break; case "FontPath": case "Image": - case "Pattern": this.commonObjs.resolve(id, exportedData); break; + case "Pattern": + const pattern = new PatternInfo(exportedData); + this.commonObjs.resolve(id, pattern.getIR()); + break; default: throw new Error(`Got unknown common object type ${type}`); } diff --git a/src/shared/obj-bin-transform.js b/src/shared/obj-bin-transform.js index f47f1769b8de4..2deeb3f611eac 100644 --- a/src/shared/obj-bin-transform.js +++ b/src/shared/obj-bin-transform.js @@ -13,7 +13,7 @@ * limitations under the License. */ -import { assert } from "./util.js"; +import { assert, MeshFigureType } from "./util.js"; class CssFontInfo { #buffer; @@ -606,4 +606,279 @@ class FontInfo { } } -export { CssFontInfo, FontInfo, SystemFontInfo }; +class PatternInfo { + static #KIND = 0; // 1=axial, 2=radial, 3=mesh + + static #HAS_BBOX = 1; // 0/1 + + static #HAS_BACKGROUND = 2; // 0/1 (background for mesh patterns) + + static #SHADING_TYPE = 3; // shadingType (only for mesh patterns) + + static #N_COORD = 4; // number of coordinate pairs + + static #N_COLOR = 8; // number of rgb triplets + + static #N_STOP = 12; // number of gradient stops + + static #N_FIGURES = 16; // number of figures + + constructor(buffer) { + this.buffer = buffer; + this.view = new DataView(buffer); + this.data = new Uint8Array(buffer); + } + + static write(ir) { + let kind, + bbox = null, + coords = [], + colors = [], + colorStops = [], + figures = [], + shadingType = null, // only needed for mesh patterns + background = null; // background for mesh patterns + + switch (ir[0]) { + case "RadialAxial": + kind = ir[1] === "axial" ? 1 : 2; + bbox = ir[2]; + colorStops = ir[3]; + if (kind === 1) { + coords.push(...ir[4], ...ir[5]); + } else { + coords.push(ir[4][0], ir[4][1], ir[6], ir[5][0], ir[5][1], ir[7]); + } + break; + case "Mesh": + kind = 3; + shadingType = ir[1]; + coords = ir[2]; + colors = ir[3]; + figures = ir[4] || []; + bbox = ir[6]; + background = ir[7]; + break; + default: + throw new Error(`Unsupported pattern type: ${ir[0]}`); + } + + const nCoord = Math.floor(coords.length / 2); + const nColor = Math.floor(colors.length / 3); + const nStop = colorStops.length; + const nFigures = figures.length; + + let figuresSize = 0; + for (const figure of figures) { + figuresSize += 1; + figuresSize = Math.ceil(figuresSize / 4) * 4; // Ensure 4-byte alignment + figuresSize += 4 + figure.coords.length * 4; + figuresSize += 4 + figure.colors.length * 4; + if (figure.verticesPerRow !== undefined) { + figuresSize += 4; + } + } + + const byteLen = + 20 + + nCoord * 8 + + nColor * 3 + + nStop * 8 + + (bbox ? 16 : 0) + + (background ? 3 : 0) + + figuresSize; + const buffer = new ArrayBuffer(byteLen); + const dataView = new DataView(buffer); + const u8data = new Uint8Array(buffer); + + dataView.setUint8(PatternInfo.#KIND, kind); + dataView.setUint8(PatternInfo.#HAS_BBOX, bbox ? 1 : 0); + dataView.setUint8(PatternInfo.#HAS_BACKGROUND, background ? 1 : 0); + dataView.setUint8(PatternInfo.#SHADING_TYPE, shadingType); // Only for mesh pattern, null otherwise + dataView.setUint32(PatternInfo.#N_COORD, nCoord, true); + dataView.setUint32(PatternInfo.#N_COLOR, nColor, true); + dataView.setUint32(PatternInfo.#N_STOP, nStop, true); + dataView.setUint32(PatternInfo.#N_FIGURES, nFigures, true); + + let offset = 20; + const coordsView = new Float32Array(buffer, offset, nCoord * 2); + coordsView.set(coords); + offset += nCoord * 8; + + u8data.set(colors, offset); + offset += nColor * 3; + + for (const [pos, hex] of colorStops) { + dataView.setFloat32(offset, pos, true); + offset += 4; + dataView.setUint32(offset, parseInt(hex.slice(1), 16), true); + offset += 4; + } + if (bbox) { + for (const v of bbox) { + dataView.setFloat32(offset, v, true); + offset += 4; + } + } + + if (background) { + u8data.set(background, offset); + offset += 3; + } + + for (let i = 0; i < figures.length; i++) { + const figure = figures[i]; + dataView.setUint8(offset, figure.type); + offset += 1; + // Ensure 4-byte alignment + offset = Math.ceil(offset / 4) * 4; + dataView.setUint32(offset, figure.coords.length, true); + offset += 4; + const figureCoordsView = new Int32Array( + buffer, + offset, + figure.coords.length + ); + figureCoordsView.set(figure.coords); + offset += figure.coords.length * 4; + dataView.setUint32(offset, figure.colors.length, true); + offset += 4; + const colorsView = new Int32Array(buffer, offset, figure.colors.length); + colorsView.set(figure.colors); + offset += figure.colors.length * 4; + + if (figure.verticesPerRow !== undefined) { + dataView.setUint32(offset, figure.verticesPerRow, true); + offset += 4; + } + } + return buffer; + } + + getIR() { + const dataView = this.view; + const kind = this.data[PatternInfo.#KIND]; + const hasBBox = !!this.data[PatternInfo.#HAS_BBOX]; + const hasBackground = !!this.data[PatternInfo.#HAS_BACKGROUND]; + const nCoord = dataView.getUint32(PatternInfo.#N_COORD, true); + const nColor = dataView.getUint32(PatternInfo.#N_COLOR, true); + const nStop = dataView.getUint32(PatternInfo.#N_STOP, true); + const nFigures = dataView.getUint32(PatternInfo.#N_FIGURES, true); + + let offset = 20; + const coords = new Float32Array(this.buffer, offset, nCoord * 2); + offset += nCoord * 8; + const colors = new Uint8Array(this.buffer, offset, nColor * 3); + offset += nColor * 3; + const stops = []; + for (let i = 0; i < nStop; ++i) { + const p = dataView.getFloat32(offset, true); + offset += 4; + const rgb = dataView.getUint32(offset, true); + offset += 4; + stops.push([p, `#${rgb.toString(16).padStart(6, "0")}`]); + } + let bbox = null; + if (hasBBox) { + bbox = []; + for (let i = 0; i < 4; ++i) { + bbox.push(dataView.getFloat32(offset, true)); + offset += 4; + } + } + + let background = null; + if (hasBackground) { + background = new Uint8Array(this.buffer, offset, 3); + offset += 3; + } + + const figures = []; + for (let i = 0; i < nFigures; ++i) { + const type = dataView.getUint8(offset); + offset += 1; + // Ensure 4-byte alignment + offset = Math.ceil(offset / 4) * 4; + + const coordsLength = dataView.getUint32(offset, true); + offset += 4; + const figureCoords = new Int32Array(this.buffer, offset, coordsLength); + offset += coordsLength * 4; + + const colorsLength = dataView.getUint32(offset, true); + offset += 4; + const figureColors = new Int32Array(this.buffer, offset, colorsLength); + offset += colorsLength * 4; + + const figure = { + type, + coords: figureCoords, + colors: figureColors, + }; + + if (type === MeshFigureType.LATTICE) { + figure.verticesPerRow = dataView.getUint32(offset, true); + offset += 4; + } + + figures.push(figure); + } + + if (kind === 1) { + // axial + return [ + "RadialAxial", + "axial", + bbox, + stops, + Array.from(coords.slice(0, 2)), + Array.from(coords.slice(2, 4)), + null, + null, + ]; + } + if (kind === 2) { + return [ + "RadialAxial", + "radial", + bbox, + stops, + [coords[0], coords[1]], + [coords[3], coords[4]], + coords[2], + coords[5], + ]; + } + if (kind === 3) { + const shadingType = this.data[PatternInfo.#SHADING_TYPE]; + let bounds = null; + if (coords.length > 0) { + let minX = coords[0], + maxX = coords[0]; + let minY = coords[1], + maxY = coords[1]; + for (let i = 0; i < coords.length; i += 2) { + const x = coords[i], + y = coords[i + 1]; + minX = minX > x ? x : minX; + minY = minY > y ? y : minY; + maxX = maxX < x ? x : maxX; + maxY = maxY < y ? y : maxY; + } + bounds = [minX, minY, maxX, maxY]; + } + return [ + "Mesh", + shadingType, + coords, + colors, + figures, + bounds, + bbox, + background, + ]; + } + throw new Error(`Unsupported pattern kind: ${kind}`); + } +} +export { CssFontInfo, FontInfo, PatternInfo, 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..6378ed9b8a5b9 --- /dev/null +++ b/test/unit/obj_bin_transform_spec.js @@ -0,0 +1,452 @@ +/* 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, + PatternInfo, + SystemFontInfo, +} from "../../src/shared/obj-bin-transform.js"; +import { MeshFigureType } from "../../src/shared/util.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"); + }); + }); +}); + +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(); + }); +});