Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 35 additions & 28 deletions src/core/function.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,34 +100,34 @@ function toNumberArray(arr) {
return arr;
}

class PDFFunction {
static getSampleArray(size, outputSize, bps, stream) {
let length = outputSize;
for (const s of size) {
length *= s;
}
function getSampleArray(size, outputSize, bps, stream) {
let length = outputSize;
for (const s of size) {
length *= s;
}

const array = new Array(length);
let codeSize = 0;
let codeBuf = 0;
// 32 is a valid bps so shifting won't work
const sampleMul = 1.0 / (2.0 ** bps - 1);

const strBytes = stream.getBytes((length * bps + 7) / 8);
let strIdx = 0;
for (let i = 0; i < length; i++) {
while (codeSize < bps) {
codeBuf <<= 8;
codeBuf |= strBytes[strIdx++];
codeSize += 8;
}
codeSize -= bps;
array[i] = (codeBuf >> codeSize) * sampleMul;
codeBuf &= (1 << codeSize) - 1;
const array = new Array(length);
let codeSize = 0;
let codeBuf = 0;
// 32 is a valid bps so shifting won't work
const sampleMul = 1.0 / (2.0 ** bps - 1);

const strBytes = stream.getBytes((length * bps + 7) / 8);
let strIdx = 0;
for (let i = 0; i < length; i++) {
while (codeSize < bps) {
codeBuf <<= 8;
codeBuf |= strBytes[strIdx++];
codeSize += 8;
}
return array;
codeSize -= bps;
array[i] = (codeBuf >> codeSize) * sampleMul;
codeBuf &= (1 << codeSize) - 1;
}
return array;
}

class PDFFunction {
static parse(factory, fn) {
const dict = fn.dict || fn;
const typeNum = dict.get("FunctionType");
Expand Down Expand Up @@ -194,7 +194,7 @@ class PDFFunction {

const decode = toNumberArray(dict.getArray("Decode")) || range;

const samples = this.getSampleArray(size, outputSize, bps, fn);
const samples = getSampleArray(size, outputSize, bps, fn);
// const mask = 2 ** bps - 1;

return function constructSampledFn(src, srcOffset, dest, destOffset) {
Expand Down Expand Up @@ -271,6 +271,8 @@ class PDFFunction {
const c0 = toNumberArray(dict.getArray("C0")) || [0];
const c1 = toNumberArray(dict.getArray("C1")) || [1];
const n = dict.get("N");
const domain = toNumberArray(dict.getArray("Domain")) || [0, 1];
const range = toNumberArray(dict.getArray("Range"));

const diff = [];
for (let i = 0, ii = c0.length; i < ii; ++i) {
Expand All @@ -279,10 +281,15 @@ class PDFFunction {
const length = diff.length;

return function constructInterpolatedFn(src, srcOffset, dest, destOffset) {
const x = n === 1 ? src[srcOffset] : src[srcOffset] ** n;
const clampedX = MathClamp(src[srcOffset], domain[0], domain[1]);
const x = n === 1 ? clampedX : clampedX ** n;

for (let j = 0; j < length; ++j) {
dest[destOffset + j] = c0[j] + x * diff[j];
let v = c0[j] + x * diff[j];
if (range) {
v = MathClamp(v, range[2 * j], range[2 * j + 1]);
}
dest[destOffset + j] = v;
}
};
}
Expand Down Expand Up @@ -381,4 +388,4 @@ function isPDFFunction(v) {
return fnDict.has("FunctionType");
}

export { FunctionType, isPDFFunction, PDFFunctionFactory };
export { FunctionType, getSampleArray, isPDFFunction, PDFFunctionFactory };
87 changes: 82 additions & 5 deletions src/core/pattern.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
MissingDataException,
} from "./core_utils.js";
import { BaseStream } from "./base_stream.js";
import { buildFunctionBasedWgslShader } from "./postscript/wgsl_compiler.js";
import { ColorSpaceUtils } from "./colorspace_utils.js";
import { MathClamp } from "../shared/math_clamp.js";

Expand All @@ -46,13 +47,16 @@ const ShadingType = {
};

class Pattern {
// eslint-disable-next-line no-unused-private-class-members
static #hasGPU = false;

constructor() {
unreachable("Cannot initialize Pattern.");
}

static get hasGPU() {
return this.#hasGPU;
}

static setOptions({ hasGPU }) {
this.#hasGPU = hasGPU;
}
Expand Down Expand Up @@ -410,7 +414,6 @@ class FunctionBasedShading extends BaseShading {
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,
Expand All @@ -429,17 +432,51 @@ class FunctionBasedShading extends BaseShading {
this.bounds = [Infinity, Infinity, -Infinity, -Infinity];
Util.axialAlignedBoundingBox([x0, y0, x1, y1], matrix, this.bounds);

// When the GPU is available and the colorspace is DeviceGray or DeviceRGB,
// try to compile the function to WGSL and skip CPU mesh generation.
this.wgslShader = null;
this.wgslMatrix = null;
this.wgslDomain = null;
if (
Pattern.hasGPU &&
(cs.name === "DeviceGray" || cs.name === "DeviceRGB")
) {
try {
this.wgslShader = buildFunctionBasedWgslShader(xref, fnObj);
} catch {}
}

if (this.wgslShader) {
this.wgslDomain = [x0, x1, y0, y1];
const [a, b, c, d, e, f] = matrix;
this.wgslMatrix =
a === 1 && b === 0 && c === 0 && d === 1 && e === 0 && f === 0
? null
: [a, b, c, d, e, f];
// GPU renders directly; no CPU mesh needed.
this.coords = new Float32Array(0);
this.colors = new Uint8ClampedArray(0);
this.figures = [];
return;
}

// CPU fallback: evaluate the function over a lattice mesh.
const fn = pdfFunctionFactory.create(fnObj, /* parseArray = */ true);

const bboxW = this.bounds[2] - this.bounds[0];
const bboxH = this.bounds[3] - this.bounds[1];
const [minStepsX, minStepsY] = FunctionBasedShading.#minStepsFromFn(
fnObj,
xref
);

// 1 step per user-space unit, capped for performance.
const stepsX = MathClamp(
Math.ceil(bboxW),
Math.max(Math.ceil(bboxW), minStepsX),
1,
FunctionBasedShading.MAX_STEP_COUNT
);
const stepsY = MathClamp(
Math.ceil(bboxH),
Math.max(Math.ceil(bboxH), minStepsY),
1,
FunctionBasedShading.MAX_STEP_COUNT
);
Expand Down Expand Up @@ -490,6 +527,43 @@ class FunctionBasedShading extends BaseShading {
];
}

// Return [minStepsX, minStepsY] based on the function's intrinsic sampling
// density. For Type-0 (sampled) functions we need enough steps per sample
// cell so that Gouraud (linear triangle) shading faithfully approximates
// the bilinear interpolation — one step per cell is not enough because the
// bilinear cross-term causes visible errors for high-contrast patterns. A
// minimum of 32 ensures each cell is covered by multiple triangles, which
// makes the approximation visually accurate. We also keep at least
// Size[i]-1 so that the mesh is never coarser than the sample grid. For
// all other function types 32 provides smooth results for curved gradients.
static #minStepsFromFnObj(fnObj) {
const dict = fnObj?.dict || fnObj;
if (dict?.get?.("FunctionType") === 0) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use the FunctionType.SAMPLED enum here instead of 0.

const size = dict.getArray("Size");
if (Array.isArray(size) && size.length >= 2) {
return [Math.max(32, size[0] - 1), Math.max(32, size[1] - 1)];
}
}
return [32, 32];
}

static #minStepsFromFn(fnObj, xref) {
const obj = xref.fetchIfRef(fnObj);
if (Array.isArray(obj)) {
let minX = 1,
minY = 1;
for (const fn of obj) {
const [x, y] = FunctionBasedShading.#minStepsFromFnObj(
xref.fetchIfRef(fn)
);
minX = Math.max(minX, x);
minY = Math.max(minY, y);
}
return [minX, minY];
}
return FunctionBasedShading.#minStepsFromFnObj(obj);
}

getIR() {
return [
"Mesh",
Expand All @@ -500,6 +574,9 @@ class FunctionBasedShading extends BaseShading {
this.bounds,
this.bbox,
this.background,
this.wgslShader, // [8] compiled WGSL shader string, or null
this.wgslMatrix, // [9] domain→user-space matrix [a,b,c,d,e,f], or null
this.wgslDomain, // [10] shading domain [x0,x1,y0,y1], or null
];
}
}
Expand Down
Loading
Loading