Skip to content

Commit f33f816

Browse files
authored
Merge pull request #21012 from calixteman/shading_function
Add support for function-based shadings (bug 1254066)
2 parents 58b807d + 3727b70 commit f33f816

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)