diff --git a/src/core/obj_bin_transform_core.js b/src/core/obj_bin_transform_core.js index 85db79410..76ec76b81 100644 --- a/src/core/obj_bin_transform_core.js +++ b/src/core/obj_bin_transform_core.js @@ -294,7 +294,7 @@ function compilePatternInfo(ir) { } const nCoord = Math.floor(coords.length / 2); - const nColor = Math.floor(colors.length / 3); + const nColor = Math.floor(colors.length / 4); const nStop = colorStops.length; const nFigures = figures.length; @@ -312,7 +312,7 @@ function compilePatternInfo(ir) { const byteLen = 20 + nCoord * 8 + - nColor * 3 + + nColor * 4 + nStop * 8 + (bbox ? 16 : 0) + (background ? 3 : 0) + @@ -336,7 +336,7 @@ function compilePatternInfo(ir) { offset += nCoord * 8; u8data.set(colors, offset); - offset += nColor * 3; + offset += nColor * 4; for (const [pos, hex] of colorStops) { dataView.setFloat32(offset, pos, true); diff --git a/src/core/pattern.js b/src/core/pattern.js index 5974fded4..e2a71f182 100644 --- a/src/core/pattern.js +++ b/src/core/pattern.js @@ -29,6 +29,7 @@ import { isNumberArray, lookupMatrix, lookupNormalRect, + lookupRect, MissingDataException, } from "./core_utils.js"; import { BaseStream } from "./base_stream.js"; @@ -63,6 +64,16 @@ class Pattern { try { switch (type) { + case ShadingType.FUNCTION_BASED: + prepareWebGPU?.(); + return new FunctionBasedShading( + dict, + xref, + res, + pdfFunctionFactory, + globalColorSpaceCache, + localColorSpaceCache + ); case ShadingType.AXIAL: case ShadingType.RADIAL: return new RadialAxialShading( @@ -312,6 +323,183 @@ class RadialAxialShading extends BaseShading { } } +// Helpers for MeshShading, which builds its mesh from a stream. +function meshUpdateBounds(self) { + let minX = self.coords[0][0], + minY = self.coords[0][1], + maxX = minX, + maxY = minY; + for (let i = 1, ii = self.coords.length; i < ii; i++) { + const x = self.coords[i][0], + y = self.coords[i][1]; + minX = minX > x ? x : minX; + minY = minY > y ? y : minY; + maxX = maxX < x ? x : maxX; + maxY = maxY < y ? y : maxY; + } + self.bounds = [minX, minY, maxX, maxY]; +} + +function meshPackData(self) { + let i, j, ii; + + const coords = self.coords; + const coordsPacked = new Float32Array(coords.length * 2); + for (i = 0, j = 0, ii = coords.length; i < ii; i++) { + const xy = coords[i]; + coordsPacked[j++] = xy[0]; + coordsPacked[j++] = xy[1]; + } + self.coords = coordsPacked; + + // Stride 4 (RGB + 1 padding byte) so each color fits in one u32, letting + // the WebGPU vertex shader read colors as array without repacking. + const colors = self.colors; + const colorsPacked = new Uint8Array(colors.length * 4); + for (i = 0, j = 0, ii = colors.length; i < ii; i++) { + const c = colors[i]; + colorsPacked[j++] = c[0]; + colorsPacked[j++] = c[1]; + colorsPacked[j++] = c[2]; + j++; // alpha — unused, stays 0 + } + self.colors = colorsPacked; + + // Store raw vertex indices (not byte offsets) so the GPU shader can + // address coords / colors without knowing their strides, and so the + // arrays are transferable Uint32Arrays. + for (const figure of self.figures) { + figure.coords = new Uint32Array(figure.coords); + figure.colors = new Uint32Array(figure.colors); + } +} + +// Type 1 shading: a 2-in, n-out function sampled over a rectangular domain. +class FunctionBasedShading extends BaseShading { + // Maximum grid steps per axis to avoid huge meshes. + static MAX_STEP_COUNT = 512; + + constructor( + dict, + xref, + resources, + pdfFunctionFactory, + globalColorSpaceCache, + localColorSpaceCache + ) { + super(); + this.bbox = lookupNormalRect(dict.getArray("BBox"), null); + + const cs = ColorSpaceUtils.parse({ + cs: dict.getRaw("CS") || dict.getRaw("ColorSpace"), + xref, + resources, + pdfFunctionFactory, + globalColorSpaceCache, + localColorSpaceCache, + }); + this.background = dict.has("Background") + ? cs.getRgb(dict.get("Background"), 0) + : null; + + const fnObj = dict.getRaw("Function"); + if (!fnObj) { + throw new FormatError("FunctionBasedShading: missing /Function"); + } + const fn = pdfFunctionFactory.create(fnObj, /* parseArray = */ true); + + // Domain [x0, x1, y0, y1]; defaults to [0, 1, 0, 1]. + let x0 = 0, + x1 = 1, + y0 = 0, + y1 = 1; + const domainArr = lookupRect(dict.getArray("Domain"), null); + if (domainArr) { + [x0, x1, y0, y1] = domainArr; + } + + // Matrix maps shading (domain) space to user space; defaults to identity. + const matrix = lookupMatrix(dict.getArray("Matrix"), IDENTITY_MATRIX); + + // Transform the four domain corners to find the user-space bounding box. + this.bounds = [Infinity, Infinity, -Infinity, -Infinity]; + Util.axialAlignedBoundingBox([x0, y0, x1, y1], matrix, this.bounds); + + const bboxW = this.bounds[2] - this.bounds[0]; + const bboxH = this.bounds[3] - this.bounds[1]; + + // 1 step per user-space unit, capped for performance. + const stepsX = MathClamp( + Math.ceil(bboxW), + 1, + FunctionBasedShading.MAX_STEP_COUNT + ); + const stepsY = MathClamp( + Math.ceil(bboxH), + 1, + FunctionBasedShading.MAX_STEP_COUNT + ); + + const verticesPerRow = stepsX + 1; + const totalVertices = (stepsY + 1) * verticesPerRow; + const coords = (this.coords = new Float32Array(totalVertices * 2)); + const colors = (this.colors = new Uint8ClampedArray(totalVertices * 4)); + + const xyBuf = new Float32Array(2); + const colorBuf = new Float32Array(cs.numComps); + const rangeX = (x1 - x0) / stepsX; + const rangeY = (y1 - y0) / stepsY; + const halfStepX = rangeX / 2; + const halfStepY = rangeY / 2; + let coordOffset = 0; + let colorOffset = 0; + for (let row = 0; row <= stepsY; row++) { + const yDomain = y0 + rangeY * row; + // Evaluate half a step inside at boundary vertices to avoid a spurious + // strip for discontinuous functions; vertex positions stay unchanged. + xyBuf[1] = row === stepsY ? yDomain - halfStepY : yDomain; + for (let col = 0; col <= stepsX; col++) { + const xDomain = x0 + rangeX * col; + xyBuf[0] = col === stepsX ? xDomain - halfStepX : xDomain; + fn(xyBuf, 0, colorBuf, 0); + coords[coordOffset] = xDomain; + coords[coordOffset + 1] = yDomain; + Util.applyTransform(coords, matrix, coordOffset); + coordOffset += 2; + + cs.getRgbItem(colorBuf, 0, colors, colorOffset); + colorOffset += 4; // alpha — unused, stays 0 + } + } + + const ps = new Uint32Array(totalVertices); + for (let i = 0; i < totalVertices; i++) { + ps[i] = i; + } + this.figures = [ + { + type: MeshFigureType.LATTICE, + coords: ps, + colors: new Uint32Array(ps), + verticesPerRow, + }, + ]; + } + + getIR() { + return [ + "Mesh", + ShadingType.FUNCTION_BASED, + this.coords, + this.colors, + this.figures, + this.bounds, + this.bbox, + this.background, + ]; + } +} + // All mesh shadings. For now, they will be presented as set of the triangles // to be drawn on the canvas and rgb color for each vertex. class MeshStreamReader { @@ -920,55 +1108,11 @@ class MeshShading extends BaseShading { } _updateBounds() { - let minX = this.coords[0][0], - minY = this.coords[0][1], - maxX = minX, - maxY = minY; - for (let i = 1, ii = this.coords.length; i < ii; i++) { - const x = this.coords[i][0], - y = this.coords[i][1]; - minX = minX > x ? x : minX; - minY = minY > y ? y : minY; - maxX = maxX < x ? x : maxX; - maxY = maxY < y ? y : maxY; - } - this.bounds = [minX, minY, maxX, maxY]; + meshUpdateBounds(this); } _packData() { - let i, ii, j; - - const coords = this.coords; - const coordsPacked = new Float32Array(coords.length * 2); - for (i = 0, j = 0, ii = coords.length; i < ii; i++) { - const xy = coords[i]; - coordsPacked[j++] = xy[0]; - coordsPacked[j++] = xy[1]; - } - this.coords = coordsPacked; - - // Stride 4 (RGBA layout, alpha unused) so the buffer maps directly to - // array in the WebGPU vertex shader without any repacking. - const colors = this.colors; - const colorsPacked = new Uint8Array(colors.length * 4); - for (i = 0, j = 0, ii = colors.length; i < ii; i++) { - const c = colors[i]; - colorsPacked[j++] = c[0]; - colorsPacked[j++] = c[1]; - colorsPacked[j++] = c[2]; - j++; // alpha — unused, stays 0 - } - this.colors = colorsPacked; - - // Store raw vertex indices (not byte offsets) so the GPU shader can - // address coords / colors without knowing their strides, and so the - // arrays are transferable Uint32Arrays. - const figures = this.figures; - for (i = 0, ii = figures.length; i < ii; i++) { - const figure = figures[i]; - figure.coords = new Uint32Array(figure.coords); - figure.colors = new Uint32Array(figure.colors); - } + meshPackData(this); } getIR() { diff --git a/src/display/obj_bin_transform_display.js b/src/display/obj_bin_transform_display.js index 465cdc9d3..a2db53d7e 100644 --- a/src/display/obj_bin_transform_display.js +++ b/src/display/obj_bin_transform_display.js @@ -353,8 +353,8 @@ class PatternInfo { 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 colors = new Uint8Array(this.buffer, offset, nColor * 4); + offset += nColor * 4; const stops = []; for (let i = 0; i < nStop; ++i) { const p = dataView.getFloat32(offset, true); diff --git a/src/shared/obj_bin_transform_utils.js b/src/shared/obj_bin_transform_utils.js index b959374df..9682485ef 100644 --- a/src/shared/obj_bin_transform_utils.js +++ b/src/shared/obj_bin_transform_utils.js @@ -61,7 +61,7 @@ class PATTERN_INFO { static N_COORD = 4; // number of coordinate pairs - static N_COLOR = 8; // number of rgb triplets + static N_COLOR = 8; // number of RGBA-stride color entries static N_STOP = 12; // number of gradient stops diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 7ec95079f..5dd5fd73c 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -895,3 +895,4 @@ !bug2025674.pdf !bug2026037.pdf !tiling_patterns_variations.pdf +!function_based_shading.pdf diff --git a/test/pdfs/function_based_shading.pdf b/test/pdfs/function_based_shading.pdf new file mode 100644 index 000000000..116024db5 --- /dev/null +++ b/test/pdfs/function_based_shading.pdf @@ -0,0 +1,315 @@ +%PDF-1.4 +%âãÏÓ +14 0 obj +<< + /FunctionType 4 + /Domain [0 1 0 1] + /Range [0 1] + /Length 7 +>> +stream +{ pop } +endstream +endobj + +15 0 obj +<< + /FunctionType 4 + /Domain [0 1 0 1] + /Range [0 1] + /Length 13 +>> +stream +{ add 2 div } +endstream +endobj + +16 0 obj +<< + /FunctionType 4 + /Domain [0 1 0 1] + /Range [0 1 0 1 0 1] + /Length 5 +>> +stream +{ 0 } +endstream +endobj + +17 0 obj +<< + /FunctionType 4 + /Domain [0 1 0 1] + /Range [0 1] + /Length 65 +>> +stream +{ 0.5 sub exch 0.5 sub dup mul exch dup mul add sqrt 2 sqrt mul } +endstream +endobj + +18 0 obj +<< + /FunctionType 4 + /Domain [0 100 0 100] + /Range [0 1] + /Length 15 +>> +stream +{ pop 100 div } +endstream +endobj + +19 0 obj +<< + /FunctionType 4 + /Domain [0 1 0 1] + /Range [0 1 0 1 0 1] + /Length 14 +>> +stream +{ 2 copy mul } +endstream +endobj + +20 0 obj +<< + /FunctionType 4 + /Domain [0 1 0 1] + /Range [0 1] + /Length 42 +>> +stream +{ 4 mul floor exch 4 mul floor add 2 mod } +endstream +endobj + +21 0 obj +<< + /FunctionType 4 + /Domain [0 1 0 1] + /Range [0 1] + /Length 58 +>> +stream +{ dup mul exch dup mul add sqrt 1440 mul sin 1 add 2 div } +endstream +endobj + +22 0 obj +<< + /FunctionType 4 + /Domain [0 1 0 1] + /Range [0 1 0 1 0 1] + /Length 101 +>> +stream +{ 2 copy add 90 mul sin dup mul 3 1 roll 180 mul sin dup mul exch 180 mul sin dup mul 3 1 roll exch } +endstream +endobj + +5 0 obj +<< + /ShadingType 1 + /ColorSpace /DeviceGray + /Domain [0 1 0 1] + /Matrix [170 0 0 170 30 582] + /Function 14 0 R +>> +endobj + +6 0 obj +<< + /ShadingType 1 + /ColorSpace /DeviceGray + /Domain [0 1 0 1] + /Matrix [170 0 0 170 230 582] + /Function 15 0 R +>> +endobj + +7 0 obj +<< + /ShadingType 1 + /ColorSpace /DeviceRGB + /Domain [0 1 0 1] + /Matrix [170 0 0 170 430 582] + /Function 16 0 R +>> +endobj + +8 0 obj +<< + /ShadingType 1 + /ColorSpace /DeviceGray + /Domain [0 1 0 1] + /Matrix [170 0 0 170 30 382] + /BBox [30 382 200 552] + /Function 17 0 R +>> +endobj + +9 0 obj +<< + /ShadingType 1 + /ColorSpace /DeviceGray + /Domain [0 100 0 100] + /Matrix [1.7 0 0 1.7 230 382] + /Function 18 0 R +>> +endobj + +10 0 obj +<< + /ShadingType 1 + /ColorSpace /DeviceRGB + /Domain [0 1 0 1] + /Matrix [85 85 -85 85 515 382] + /Function 19 0 R +>> +endobj + +11 0 obj +<< + /ShadingType 1 + /ColorSpace /DeviceGray + /Domain [0 1 0 1] + /Matrix [170 0 0 170 30 182] + /Function 20 0 R +>> +endobj + +12 0 obj +<< + /ShadingType 1 + /ColorSpace /DeviceGray + /Domain [0 1 0 1] + /Matrix [170 0 0 170 230 182] + /Function 21 0 R +>> +endobj + +13 0 obj +<< + /ShadingType 1 + /ColorSpace /DeviceRGB + /Domain [0 1 0 1] + /Matrix [170 0 0 170 430 182] + /Function 22 0 R +>> +endobj + +4 0 obj +<< + /Length 312 +>> +stream +q +30 582 170 170 re W n +/SH1 sh +Q +q +230 582 170 170 re W n +/SH2 sh +Q +q +430 582 170 170 re W n +/SH3 sh +Q +q +30 382 170 170 re W n +/SH4 sh +Q +q +230 382 170 170 re W n +/SH5 sh +Q +q +430 382 170 170 re W n +/SH6 sh +Q +q +30 182 170 170 re W n +/SH7 sh +Q +q +230 182 170 170 re W n +/SH8 sh +Q +q +430 182 170 170 re W n +/SH9 sh +Q +endstream +endobj + +3 0 obj +<< + /Type /Page + /Parent 2 0 R + /MediaBox [0 0 612 792] + /Contents 4 0 R + /Resources << + /Shading << + /SH1 5 0 R + /SH2 6 0 R + /SH3 7 0 R + /SH4 8 0 R + /SH5 9 0 R + /SH6 10 0 R + /SH7 11 0 R + /SH8 12 0 R + /SH9 13 0 R + >> + >> +>> +endobj + +2 0 obj +<< + /Type /Pages + /Kids [3 0 R] + /Count 1 +>> +endobj + +1 0 obj +<< + /Type /Catalog + /Pages 2 0 R +>> +endobj + +xref +0 23 +0000000000 65535 f +0000003285 00000 n +0000003221 00000 n +0000002942 00000 n +0000002577 00000 n +0000001325 00000 n +0000001460 00000 n +0000001596 00000 n +0000001731 00000 n +0000001891 00000 n +0000002031 00000 n +0000002168 00000 n +0000002304 00000 n +0000002441 00000 n +0000000015 00000 n +0000000128 00000 n +0000000248 00000 n +0000000367 00000 n +0000000539 00000 n +0000000665 00000 n +0000000794 00000 n +0000000943 00000 n +0000001108 00000 n +trailer +<< + /Size 23 + /Root 1 0 R +>> +startxref +3339 +%%EOF diff --git a/test/test_manifest.json b/test/test_manifest.json index 8b5919f13..98d6d0192 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -14035,5 +14035,12 @@ "md5": "2870c3136be00ddd975149b2c7d1e6df", "rounds": 1, "type": "eq" + }, + { + "id": "function-based-shading", + "file": "pdfs/function_based_shading.pdf", + "md5": "7796f0131e7d6428c1bf24a73ff13f95", + "rounds": 1, + "type": "eq" } ] diff --git a/test/unit/clitests.json b/test/unit/clitests.json index 461185d9d..230ed4e51 100644 --- a/test/unit/clitests.json +++ b/test/unit/clitests.json @@ -36,6 +36,7 @@ "node_stream_spec.js", "obj_bin_transform_spec.js", "parser_spec.js", + "pattern_spec.js", "pdf.image_decoders_spec.js", "pdf.worker_spec.js", "pdf_find_controller_spec.js", diff --git a/test/unit/jasmine-boot.js b/test/unit/jasmine-boot.js index 7c8b2563e..7d63428bb 100644 --- a/test/unit/jasmine-boot.js +++ b/test/unit/jasmine-boot.js @@ -79,6 +79,7 @@ async function initializePDFJS(callback) { "pdfjs-test/unit/network_utils_spec.js", "pdfjs-test/unit/obj_bin_transform_spec.js", "pdfjs-test/unit/parser_spec.js", + "pdfjs-test/unit/pattern_spec.js", "pdfjs-test/unit/pdf.image_decoders_spec.js", "pdfjs-test/unit/pdf.worker_spec.js", "pdfjs-test/unit/pdf_find_controller_spec.js", diff --git a/test/unit/obj_bin_transform_spec.js b/test/unit/obj_bin_transform_spec.js index 8f3b11077..199df449d 100644 --- a/test/unit/obj_bin_transform_spec.js +++ b/test/unit/obj_bin_transform_spec.js @@ -215,8 +215,8 @@ describe("obj_bin_transform", function () { 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, + 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, ]), [ { @@ -335,7 +335,7 @@ describe("obj_bin_transform", function () { "Mesh", 4, new Float32Array([0, 0, 10, 10]), - new Uint8Array([255, 0, 0]), + new Uint8Array([255, 0, 0, 0]), [], [0, 0, 10, 10], [0, 0, 10, 10], @@ -383,7 +383,7 @@ describe("obj_bin_transform", function () { "Mesh", 6, new Float32Array([0, 0, 10, 10]), - new Uint8Array([255, 128, 64]), + new Uint8Array([255, 128, 64, 0]), [ { type: MeshFigureType.PATCH, @@ -415,7 +415,7 @@ describe("obj_bin_transform", function () { "Mesh", 4, new Float32Array([0, 0, 10, 10]), - new Uint8Array([255, 0, 0]), + new Uint8Array([255, 0, 0, 0]), [], [0, 0, 10, 10], [0, 0, 10, 10], @@ -432,7 +432,7 @@ describe("obj_bin_transform", function () { "Mesh", 5, new Float32Array([0, 0, 5, 5]), - new Uint8Array([0, 255, 0]), + new Uint8Array([0, 255, 0, 0]), [], [0, 0, 5, 5], null, @@ -451,7 +451,7 @@ describe("obj_bin_transform", function () { "Mesh", 4, new Float32Array([-10, -5, 20, 15, 0, 30]), - new Uint8Array([255, 0, 0, 0, 255, 0, 0, 0, 255]), + new Uint8Array([255, 0, 0, 0, 0, 255, 0, 0, 0, 0, 255, 0]), [], null, null, diff --git a/test/unit/pattern_spec.js b/test/unit/pattern_spec.js new file mode 100644 index 000000000..e85f9f9f2 --- /dev/null +++ b/test/unit/pattern_spec.js @@ -0,0 +1,141 @@ +/* Copyright 2026 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 { Dict, Name } from "../../src/core/primitives.js"; +import { + GlobalColorSpaceCache, + LocalColorSpaceCache, +} from "../../src/core/image_utils.js"; +import { compilePatternInfo } from "../../src/core/obj_bin_transform_core.js"; +import { Pattern } from "../../src/core/pattern.js"; +import { PatternInfo } from "../../src/display/obj_bin_transform_display.js"; + +describe("pattern", function () { + describe("FunctionBasedShading", function () { + function createFunctionBasedShading({ + colorSpace = "DeviceRGB", + domain = [0, 1, 0, 1], + matrix = [2, 0, 0, 3, 10, 20], + background = null, + fn = (src, srcOffset, dest, destOffset) => { + dest[destOffset] = src[srcOffset]; + dest[destOffset + 1] = src[srcOffset + 1]; + dest[destOffset + 2] = 0; + }, + } = {}) { + const dict = new Dict(); + dict.set("ShadingType", 1); + dict.set("ColorSpace", Name.get(colorSpace)); + dict.set("Domain", domain); + dict.set("Matrix", matrix); + if (background) { + dict.set("Background", background); + } + dict.set("Function", { + fn, + }); + + const pdfFunctionFactory = { + create(fnObj) { + return fnObj.fn; + }, + }; + const xref = { + fetchIfRef(obj) { + return obj; + }, + }; + + return Pattern.parseShading( + dict, + xref, + /* res = */ null, + pdfFunctionFactory, + new GlobalColorSpaceCache(), + new LocalColorSpaceCache() + ); + } + + it("must convert Type 1 shading into packed mesh IR", function () { + const shading = createFunctionBasedShading(); + const ir = shading.getIR(); + + expect(ir[0]).toEqual("Mesh"); + expect(ir[1]).toEqual(1); + expect(ir[2]).toBeInstanceOf(Float32Array); + expect(ir[2].length).toEqual(24); + expect(Array.from(ir[2].slice(0, 6))).toEqual([10, 20, 11, 20, 12, 20]); + expect(Array.from(ir[2].slice(-6))).toEqual([10, 23, 11, 23, 12, 23]); + + expect(ir[3]).toBeInstanceOf(Uint8ClampedArray); + expect(ir[3].length).toEqual(48); + expect(Array.from(ir[3].slice(0, 12))).toEqual([ + 0, 0, 0, 0, 128, 0, 0, 0, 191, 0, 0, 0, + ]); + expect(Array.from(ir[3].slice(-12))).toEqual([ + 0, 212, 0, 0, 128, 212, 0, 0, 191, 212, 0, 0, + ]); + + expect(ir[4]).toEqual([ + jasmine.objectContaining({ + verticesPerRow: 3, + }), + ]); + expect(ir[4][0].coords).toBeInstanceOf(Uint32Array); + expect(Array.from(ir[4][0].coords)).toEqual([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + ]); + expect(ir[4][0].colors).toBeInstanceOf(Uint32Array); + expect(Array.from(ir[4][0].colors)).toEqual([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + ]); + expect(ir[5]).toEqual([10, 20, 12, 23]); + expect(ir[6]).toBeNull(); + expect(ir[7]).toBeNull(); + }); + + it("must keep mesh colors intact through binary serialization", function () { + const shading = createFunctionBasedShading({ + background: [0.25, 0.5, 0.75], + }); + const buffer = compilePatternInfo(shading.getIR()); + const reconstructedIR = new PatternInfo(buffer).getIR(); + + expect(reconstructedIR[0]).toEqual("Mesh"); + expect(reconstructedIR[1]).toEqual(1); + expect(Array.from(reconstructedIR[2])).toEqual( + Array.from(shading.coords) + ); + expect(Array.from(reconstructedIR[3])).toEqual( + Array.from(shading.colors) + ); + expect(Array.from(reconstructedIR[7])).toEqual([64, 128, 191]); + }); + + it("must sample the upper and right edges half a step inside", function () { + const shading = createFunctionBasedShading({ + colorSpace: "DeviceGray", + fn(src, srcOffset, dest, destOffset) { + dest[destOffset] = + src[srcOffset] === 1 || src[srcOffset + 1] === 1 ? 1 : 0; + }, + }); + const [, , , colors] = shading.getIR(); + + expect(colors.length).toEqual(48); + expect(Array.from(colors)).toEqual(new Array(48).fill(0)); + }); + }); +});