pdf.js.mirror/test/unit/obj_bin_transform_spec.js
Calixte Denizet 3727b7095a Add support for function-based shadings (bug 1254066)
It fixes #5046.
We just generate a mesh for the pattern rectangle where the color of each vertex is computed from the function.
Since the mesh is generated in the worker we don't really take into account the current transform when it's drawn.
That being said, there are maybe some possible improvements in using directly the gpu for the shading creation
which could then take into account the current transform, but it could only work with ps function we can convert
ino wgsl language and simple enough color spaces (gray and rgb).
2026-03-31 20:46:01 +02:00

493 lines
17 KiB
JavaScript

/* 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 {
compileCssFontInfo,
compileFontInfo,
compileFontPathInfo,
compilePatternInfo,
compileSystemFontInfo,
} from "../../src/core/obj_bin_transform_core.js";
import {
CssFontInfo,
FontInfo,
FontPathInfo,
PatternInfo,
SystemFontInfo,
} from "../../src/display/obj_bin_transform_display.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 = compileCssFontInfo(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 = compileSystemFontInfo(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 = compileFontInfo(fontInfo);
expect(buffer.byteLength).toEqual(sizeEstimate);
const deserialized = new FontInfo({ 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 = compileFontInfo({
...fontInfo,
cssFontInfo,
systemFontInfo,
});
const deserialized = new FontInfo({ 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, 0, 255, 0, 0, 0, 0, 255, 0, 255, 255, 0, 0, 128, 128, 128,
0, 255, 0, 255, 0, 0, 255, 255, 0, 255, 128, 0, 0, 128, 0, 128, 0,
]),
[
{
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 = compilePatternInfo(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 = compilePatternInfo(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 = compilePatternInfo(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, 0, 10, 10],
[0, 0, 10, 10],
null,
];
const buffer = compilePatternInfo(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 = compilePatternInfo(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 = compilePatternInfo(axialPatternIR);
const radialBuffer = compilePatternInfo(radialPatternIR);
const meshBuffer = compilePatternInfo(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, 0]),
[
{
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 = compilePatternInfo(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, 0, 10, 10],
[0, 0, 10, 10],
new Uint8Array([255, 128, 64]),
];
const buffer = compilePatternInfo(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, 0, 5, 5],
null,
null,
];
const buffer2 = compilePatternInfo(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, 0, 255, 0, 0, 0, 0, 255, 0]),
[],
null,
null,
null,
];
const buffer = compilePatternInfo(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 = compileFontPathInfo(path);
const fontPathInfo = new FontPathInfo(buffer);
expect(fontPathInfo.path).toEqual(path);
});
});
});