Skip to content

Commit 3727b70

Browse files
committed
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).
1 parent 58b807d commit 3727b70

11 files changed

Lines changed: 354 additions & 59 deletions

src/core/obj_bin_transform_core.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ function compilePatternInfo(ir) {
294294
}
295295

296296
const nCoord = Math.floor(coords.length / 2);
297-
const nColor = Math.floor(colors.length / 3);
297+
const nColor = Math.floor(colors.length / 4);
298298
const nStop = colorStops.length;
299299
const nFigures = figures.length;
300300

@@ -312,7 +312,7 @@ function compilePatternInfo(ir) {
312312
const byteLen =
313313
20 +
314314
nCoord * 8 +
315-
nColor * 3 +
315+
nColor * 4 +
316316
nStop * 8 +
317317
(bbox ? 16 : 0) +
318318
(background ? 3 : 0) +
@@ -336,7 +336,7 @@ function compilePatternInfo(ir) {
336336
offset += nCoord * 8;
337337

338338
u8data.set(colors, offset);
339-
offset += nColor * 3;
339+
offset += nColor * 4;
340340

341341
for (const [pos, hex] of colorStops) {
342342
dataView.setFloat32(offset, pos, true);

src/core/pattern.js

Lines changed: 190 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
isNumberArray,
3030
lookupMatrix,
3131
lookupNormalRect,
32+
lookupRect,
3233
MissingDataException,
3334
} from "./core_utils.js";
3435
import { BaseStream } from "./base_stream.js";
@@ -63,6 +64,16 @@ class Pattern {
6364

6465
try {
6566
switch (type) {
67+
case ShadingType.FUNCTION_BASED:
68+
prepareWebGPU?.();
69+
return new FunctionBasedShading(
70+
dict,
71+
xref,
72+
res,
73+
pdfFunctionFactory,
74+
globalColorSpaceCache,
75+
localColorSpaceCache
76+
);
6677
case ShadingType.AXIAL:
6778
case ShadingType.RADIAL:
6879
return new RadialAxialShading(
@@ -312,6 +323,183 @@ class RadialAxialShading extends BaseShading {
312323
}
313324
}
314325

326+
// Helpers for MeshShading, which builds its mesh from a stream.
327+
function meshUpdateBounds(self) {
328+
let minX = self.coords[0][0],
329+
minY = self.coords[0][1],
330+
maxX = minX,
331+
maxY = minY;
332+
for (let i = 1, ii = self.coords.length; i < ii; i++) {
333+
const x = self.coords[i][0],
334+
y = self.coords[i][1];
335+
minX = minX > x ? x : minX;
336+
minY = minY > y ? y : minY;
337+
maxX = maxX < x ? x : maxX;
338+
maxY = maxY < y ? y : maxY;
339+
}
340+
self.bounds = [minX, minY, maxX, maxY];
341+
}
342+
343+
function meshPackData(self) {
344+
let i, j, ii;
345+
346+
const coords = self.coords;
347+
const coordsPacked = new Float32Array(coords.length * 2);
348+
for (i = 0, j = 0, ii = coords.length; i < ii; i++) {
349+
const xy = coords[i];
350+
coordsPacked[j++] = xy[0];
351+
coordsPacked[j++] = xy[1];
352+
}
353+
self.coords = coordsPacked;
354+
355+
// Stride 4 (RGB + 1 padding byte) so each color fits in one u32, letting
356+
// the WebGPU vertex shader read colors as array<u32> without repacking.
357+
const colors = self.colors;
358+
const colorsPacked = new Uint8Array(colors.length * 4);
359+
for (i = 0, j = 0, ii = colors.length; i < ii; i++) {
360+
const c = colors[i];
361+
colorsPacked[j++] = c[0];
362+
colorsPacked[j++] = c[1];
363+
colorsPacked[j++] = c[2];
364+
j++; // alpha — unused, stays 0
365+
}
366+
self.colors = colorsPacked;
367+
368+
// Store raw vertex indices (not byte offsets) so the GPU shader can
369+
// address coords / colors without knowing their strides, and so the
370+
// arrays are transferable Uint32Arrays.
371+
for (const figure of self.figures) {
372+
figure.coords = new Uint32Array(figure.coords);
373+
figure.colors = new Uint32Array(figure.colors);
374+
}
375+
}
376+
377+
// Type 1 shading: a 2-in, n-out function sampled over a rectangular domain.
378+
class FunctionBasedShading extends BaseShading {
379+
// Maximum grid steps per axis to avoid huge meshes.
380+
static MAX_STEP_COUNT = 512;
381+
382+
constructor(
383+
dict,
384+
xref,
385+
resources,
386+
pdfFunctionFactory,
387+
globalColorSpaceCache,
388+
localColorSpaceCache
389+
) {
390+
super();
391+
this.bbox = lookupNormalRect(dict.getArray("BBox"), null);
392+
393+
const cs = ColorSpaceUtils.parse({
394+
cs: dict.getRaw("CS") || dict.getRaw("ColorSpace"),
395+
xref,
396+
resources,
397+
pdfFunctionFactory,
398+
globalColorSpaceCache,
399+
localColorSpaceCache,
400+
});
401+
this.background = dict.has("Background")
402+
? cs.getRgb(dict.get("Background"), 0)
403+
: null;
404+
405+
const fnObj = dict.getRaw("Function");
406+
if (!fnObj) {
407+
throw new FormatError("FunctionBasedShading: missing /Function");
408+
}
409+
const fn = pdfFunctionFactory.create(fnObj, /* parseArray = */ true);
410+
411+
// Domain [x0, x1, y0, y1]; defaults to [0, 1, 0, 1].
412+
let x0 = 0,
413+
x1 = 1,
414+
y0 = 0,
415+
y1 = 1;
416+
const domainArr = lookupRect(dict.getArray("Domain"), null);
417+
if (domainArr) {
418+
[x0, x1, y0, y1] = domainArr;
419+
}
420+
421+
// Matrix maps shading (domain) space to user space; defaults to identity.
422+
const matrix = lookupMatrix(dict.getArray("Matrix"), IDENTITY_MATRIX);
423+
424+
// Transform the four domain corners to find the user-space bounding box.
425+
this.bounds = [Infinity, Infinity, -Infinity, -Infinity];
426+
Util.axialAlignedBoundingBox([x0, y0, x1, y1], matrix, this.bounds);
427+
428+
const bboxW = this.bounds[2] - this.bounds[0];
429+
const bboxH = this.bounds[3] - this.bounds[1];
430+
431+
// 1 step per user-space unit, capped for performance.
432+
const stepsX = MathClamp(
433+
Math.ceil(bboxW),
434+
1,
435+
FunctionBasedShading.MAX_STEP_COUNT
436+
);
437+
const stepsY = MathClamp(
438+
Math.ceil(bboxH),
439+
1,
440+
FunctionBasedShading.MAX_STEP_COUNT
441+
);
442+
443+
const verticesPerRow = stepsX + 1;
444+
const totalVertices = (stepsY + 1) * verticesPerRow;
445+
const coords = (this.coords = new Float32Array(totalVertices * 2));
446+
const colors = (this.colors = new Uint8ClampedArray(totalVertices * 4));
447+
448+
const xyBuf = new Float32Array(2);
449+
const colorBuf = new Float32Array(cs.numComps);
450+
const rangeX = (x1 - x0) / stepsX;
451+
const rangeY = (y1 - y0) / stepsY;
452+
const halfStepX = rangeX / 2;
453+
const halfStepY = rangeY / 2;
454+
let coordOffset = 0;
455+
let colorOffset = 0;
456+
for (let row = 0; row <= stepsY; row++) {
457+
const yDomain = y0 + rangeY * row;
458+
// Evaluate half a step inside at boundary vertices to avoid a spurious
459+
// strip for discontinuous functions; vertex positions stay unchanged.
460+
xyBuf[1] = row === stepsY ? yDomain - halfStepY : yDomain;
461+
for (let col = 0; col <= stepsX; col++) {
462+
const xDomain = x0 + rangeX * col;
463+
xyBuf[0] = col === stepsX ? xDomain - halfStepX : xDomain;
464+
fn(xyBuf, 0, colorBuf, 0);
465+
coords[coordOffset] = xDomain;
466+
coords[coordOffset + 1] = yDomain;
467+
Util.applyTransform(coords, matrix, coordOffset);
468+
coordOffset += 2;
469+
470+
cs.getRgbItem(colorBuf, 0, colors, colorOffset);
471+
colorOffset += 4; // alpha — unused, stays 0
472+
}
473+
}
474+
475+
const ps = new Uint32Array(totalVertices);
476+
for (let i = 0; i < totalVertices; i++) {
477+
ps[i] = i;
478+
}
479+
this.figures = [
480+
{
481+
type: MeshFigureType.LATTICE,
482+
coords: ps,
483+
colors: new Uint32Array(ps),
484+
verticesPerRow,
485+
},
486+
];
487+
}
488+
489+
getIR() {
490+
return [
491+
"Mesh",
492+
ShadingType.FUNCTION_BASED,
493+
this.coords,
494+
this.colors,
495+
this.figures,
496+
this.bounds,
497+
this.bbox,
498+
this.background,
499+
];
500+
}
501+
}
502+
315503
// All mesh shadings. For now, they will be presented as set of the triangles
316504
// to be drawn on the canvas and rgb color for each vertex.
317505
class MeshStreamReader {
@@ -920,55 +1108,11 @@ class MeshShading extends BaseShading {
9201108
}
9211109

9221110
_updateBounds() {
923-
let minX = this.coords[0][0],
924-
minY = this.coords[0][1],
925-
maxX = minX,
926-
maxY = minY;
927-
for (let i = 1, ii = this.coords.length; i < ii; i++) {
928-
const x = this.coords[i][0],
929-
y = this.coords[i][1];
930-
minX = minX > x ? x : minX;
931-
minY = minY > y ? y : minY;
932-
maxX = maxX < x ? x : maxX;
933-
maxY = maxY < y ? y : maxY;
934-
}
935-
this.bounds = [minX, minY, maxX, maxY];
1111+
meshUpdateBounds(this);
9361112
}
9371113

9381114
_packData() {
939-
let i, ii, j;
940-
941-
const coords = this.coords;
942-
const coordsPacked = new Float32Array(coords.length * 2);
943-
for (i = 0, j = 0, ii = coords.length; i < ii; i++) {
944-
const xy = coords[i];
945-
coordsPacked[j++] = xy[0];
946-
coordsPacked[j++] = xy[1];
947-
}
948-
this.coords = coordsPacked;
949-
950-
// Stride 4 (RGBA layout, alpha unused) so the buffer maps directly to
951-
// array<u32> in the WebGPU vertex shader without any repacking.
952-
const colors = this.colors;
953-
const colorsPacked = new Uint8Array(colors.length * 4);
954-
for (i = 0, j = 0, ii = colors.length; i < ii; i++) {
955-
const c = colors[i];
956-
colorsPacked[j++] = c[0];
957-
colorsPacked[j++] = c[1];
958-
colorsPacked[j++] = c[2];
959-
j++; // alpha — unused, stays 0
960-
}
961-
this.colors = colorsPacked;
962-
963-
// Store raw vertex indices (not byte offsets) so the GPU shader can
964-
// address coords / colors without knowing their strides, and so the
965-
// arrays are transferable Uint32Arrays.
966-
const figures = this.figures;
967-
for (i = 0, ii = figures.length; i < ii; i++) {
968-
const figure = figures[i];
969-
figure.coords = new Uint32Array(figure.coords);
970-
figure.colors = new Uint32Array(figure.colors);
971-
}
1115+
meshPackData(this);
9721116
}
9731117

9741118
getIR() {

src/display/obj_bin_transform_display.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -353,8 +353,8 @@ class PatternInfo {
353353
let offset = 20;
354354
const coords = new Float32Array(this.buffer, offset, nCoord * 2);
355355
offset += nCoord * 8;
356-
const colors = new Uint8Array(this.buffer, offset, nColor * 3);
357-
offset += nColor * 3;
356+
const colors = new Uint8Array(this.buffer, offset, nColor * 4);
357+
offset += nColor * 4;
358358
const stops = [];
359359
for (let i = 0; i < nStop; ++i) {
360360
const p = dataView.getFloat32(offset, true);

src/shared/obj_bin_transform_utils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class PATTERN_INFO {
6161

6262
static N_COORD = 4; // number of coordinate pairs
6363

64-
static N_COLOR = 8; // number of rgb triplets
64+
static N_COLOR = 8; // number of RGBA-stride color entries
6565

6666
static N_STOP = 12; // number of gradient stops
6767

test/pdfs/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,3 +895,4 @@
895895
!bug2025674.pdf
896896
!bug2026037.pdf
897897
!tiling_patterns_variations.pdf
898+
!function_based_shading.pdf
3.78 KB
Binary file not shown.

test/test_manifest.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14035,5 +14035,12 @@
1403514035
"md5": "2870c3136be00ddd975149b2c7d1e6df",
1403614036
"rounds": 1,
1403714037
"type": "eq"
14038+
},
14039+
{
14040+
"id": "function-based-shading",
14041+
"file": "pdfs/function_based_shading.pdf",
14042+
"md5": "7796f0131e7d6428c1bf24a73ff13f95",
14043+
"rounds": 1,
14044+
"type": "eq"
1403814045
}
1403914046
]

test/unit/clitests.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"node_stream_spec.js",
3737
"obj_bin_transform_spec.js",
3838
"parser_spec.js",
39+
"pattern_spec.js",
3940
"pdf.image_decoders_spec.js",
4041
"pdf.worker_spec.js",
4142
"pdf_find_controller_spec.js",

test/unit/jasmine-boot.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ async function initializePDFJS(callback) {
7979
"pdfjs-test/unit/network_utils_spec.js",
8080
"pdfjs-test/unit/obj_bin_transform_spec.js",
8181
"pdfjs-test/unit/parser_spec.js",
82+
"pdfjs-test/unit/pattern_spec.js",
8283
"pdfjs-test/unit/pdf.image_decoders_spec.js",
8384
"pdfjs-test/unit/pdf.worker_spec.js",
8485
"pdfjs-test/unit/pdf_find_controller_spec.js",

0 commit comments

Comments
 (0)