Serialize pattern data into ArrayBuffer

Follow up on https://github.com/mozilla/pdf.js/pull/20197,
This serializes pattern data into an ArrayBuffer which is
then transferred from the worker to the main thread.

It sets up the stage for us to eventually switch to a
SharedArrayBuffer in the future.
This commit is contained in:
Aditi 2025-10-03 16:20:27 +05:30
parent 30fdf16071
commit fa631806bf
7 changed files with 741 additions and 172 deletions

View File

@ -40,6 +40,7 @@ import {
lookupMatrix, lookupMatrix,
lookupNormalRect, lookupNormalRect,
} from "./core_utils.js"; } from "./core_utils.js";
import { FontInfo, PatternInfo } from "../shared/obj-bin-transform.js";
import { import {
getEncoding, getEncoding,
MacRomanEncoding, MacRomanEncoding,
@ -72,7 +73,6 @@ import { BaseStream } from "./base_stream.js";
import { bidi } from "./bidi.js"; import { bidi } from "./bidi.js";
import { ColorSpace } from "./colorspace.js"; import { ColorSpace } from "./colorspace.js";
import { ColorSpaceUtils } from "./colorspace_utils.js"; import { ColorSpaceUtils } from "./colorspace_utils.js";
import { FontInfo } from "../shared/obj-bin-transform.js";
import { getFontSubstitution } from "./font_substitutions.js"; import { getFontSubstitution } from "./font_substitutions.js";
import { getGlyphsUnicode } from "./glyphlist.js"; import { getGlyphsUnicode } from "./glyphlist.js";
import { getMetrics } from "./metrics.js"; import { getMetrics } from "./metrics.js";
@ -1517,7 +1517,10 @@ class PartialEvaluator {
localShadingPatternCache.set(shading, id); localShadingPatternCache.set(shading, id);
if (this.parsingType3Font) { 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 { } else {
this.handler.send("obj", [id, this.pageIndex, "Pattern", patternIR]); this.handler.send("obj", [id, this.pageIndex, "Pattern", patternIR]);
} }

View File

@ -45,6 +45,7 @@ import {
StatTimer, StatTimer,
} from "./display_utils.js"; } from "./display_utils.js";
import { FontFaceObject, FontLoader } from "./font_loader.js"; import { FontFaceObject, FontLoader } from "./font_loader.js";
import { FontInfo, PatternInfo } from "../shared/obj-bin-transform.js";
import { import {
getDataProp, getDataProp,
getFactoryUrlProp, getFactoryUrlProp,
@ -67,7 +68,6 @@ import { DOMCMapReaderFactory } from "display-cmap_reader_factory";
import { DOMFilterFactory } from "./filter_factory.js"; import { DOMFilterFactory } from "./filter_factory.js";
import { DOMStandardFontDataFactory } from "display-standard_fontdata_factory"; import { DOMStandardFontDataFactory } from "display-standard_fontdata_factory";
import { DOMWasmFactory } from "display-wasm_factory"; import { DOMWasmFactory } from "display-wasm_factory";
import { FontInfo } from "../shared/obj-bin-transform.js";
import { GlobalWorkerOptions } from "./worker_options.js"; import { GlobalWorkerOptions } from "./worker_options.js";
import { Metadata } from "./metadata.js"; import { Metadata } from "./metadata.js";
import { OptionalContentConfig } from "./optional_content_config.js"; import { OptionalContentConfig } from "./optional_content_config.js";
@ -2804,9 +2804,12 @@ class WorkerTransport {
break; break;
case "FontPath": case "FontPath":
case "Image": case "Image":
case "Pattern":
this.commonObjs.resolve(id, exportedData); this.commonObjs.resolve(id, exportedData);
break; break;
case "Pattern":
const pattern = new PatternInfo(exportedData);
this.commonObjs.resolve(id, pattern.getIR());
break;
default: default:
throw new Error(`Got unknown common object type ${type}`); throw new Error(`Got unknown common object type ${type}`);
} }

View File

@ -13,7 +13,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { assert } from "./util.js"; import { assert, MeshFigureType } from "./util.js";
class CssFontInfo { class CssFontInfo {
#buffer; #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 };

View File

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

View File

@ -10,7 +10,6 @@
"app_options_spec.js", "app_options_spec.js",
"autolinker_spec.js", "autolinker_spec.js",
"bidi_spec.js", "bidi_spec.js",
"bin_font_info_spec.js",
"canvas_factory_spec.js", "canvas_factory_spec.js",
"cff_parser_spec.js", "cff_parser_spec.js",
"cmap_spec.js", "cmap_spec.js",
@ -33,6 +32,7 @@
"murmurhash3_spec.js", "murmurhash3_spec.js",
"network_utils_spec.js", "network_utils_spec.js",
"node_stream_spec.js", "node_stream_spec.js",
"obj_bin_transform_spec.js",
"parser_spec.js", "parser_spec.js",
"pdf.image_decoders_spec.js", "pdf.image_decoders_spec.js",
"pdf.worker_spec.js", "pdf.worker_spec.js",

View File

@ -53,7 +53,6 @@ async function initializePDFJS(callback) {
"pdfjs-test/unit/app_options_spec.js", "pdfjs-test/unit/app_options_spec.js",
"pdfjs-test/unit/autolinker_spec.js", "pdfjs-test/unit/autolinker_spec.js",
"pdfjs-test/unit/bidi_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/canvas_factory_spec.js",
"pdfjs-test/unit/cff_parser_spec.js", "pdfjs-test/unit/cff_parser_spec.js",
"pdfjs-test/unit/cmap_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/murmurhash3_spec.js",
"pdfjs-test/unit/network_spec.js", "pdfjs-test/unit/network_spec.js",
"pdfjs-test/unit/network_utils_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/parser_spec.js",
"pdfjs-test/unit/pdf.image_decoders_spec.js", "pdfjs-test/unit/pdf.image_decoders_spec.js",
"pdfjs-test/unit/pdf.worker_spec.js", "pdfjs-test/unit/pdf.worker_spec.js",

View File

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