From 3727b7095abf1eb23d5d5768e218dcbf7ce606d9 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Tue, 31 Mar 2026 14:03:06 +0200 Subject: [PATCH] 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). --- src/core/obj_bin_transform_core.js | 6 +- src/core/pattern.js | 236 +++++++++++++---- src/display/obj_bin_transform_display.js | 4 +- src/shared/obj_bin_transform_utils.js | 2 +- test/pdfs/.gitignore | 1 + test/pdfs/function_based_shading.pdf | 315 +++++++++++++++++++++++ test/test_manifest.json | 7 + test/unit/clitests.json | 1 + test/unit/jasmine-boot.js | 1 + test/unit/obj_bin_transform_spec.js | 14 +- test/unit/pattern_spec.js | 141 ++++++++++ 11 files changed, 669 insertions(+), 59 deletions(-) create mode 100644 test/pdfs/function_based_shading.pdf create mode 100644 test/unit/pattern_spec.js 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)); + }); + }); +});