mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-07-28 18:07:21 +02:00
Convert shading colors in bulk, rather than one at a time
The axial/radial ramps and the function-based lattice converted one color per call, i.e. one Wasm round-trip each for `IccColorSpace` (hence for `/DeviceCMYK`). Add `ColorSpace.getRgbItems`, overridden by `IccColorSpace` and `AlternateCS`, to convert a whole batch at once.
This commit is contained in:
parent
f38ea2d4cc
commit
526b69651f
@ -183,6 +183,21 @@ class ColorSpace {
|
||||
unreachable("Should not call ColorSpace.getRgbBuffer");
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts `count` unscaled colors to RGB, starting at `destOffset`.
|
||||
* Components use the native color-space ranges expected by `getRgbItem`,
|
||||
* and each output has a `3 + alpha01` byte stride.
|
||||
* Subclasses may override this to batch expensive conversions.
|
||||
*/
|
||||
getRgbItems(src, count, dest, destOffset, alpha01) {
|
||||
const { numComps } = this;
|
||||
|
||||
for (let i = 0, srcOffset = 0; i < count; i++, srcOffset += numComps) {
|
||||
this.getRgbItem(src, srcOffset, dest, destOffset);
|
||||
destOffset += 3 + alpha01;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the number of bytes required to store the result of the
|
||||
* conversion done by the getRgbBuffer method. As in getRgbBuffer,
|
||||
@ -379,6 +394,20 @@ class AlternateCS extends ColorSpace {
|
||||
this.base.getRgbItem(tmpBuf, 0, dest, destOffset);
|
||||
}
|
||||
|
||||
getRgbItems(src, count, dest, destOffset, alpha01) {
|
||||
const { base, numComps, tintFn } = this;
|
||||
const baseNumComps = base.numComps;
|
||||
// Tint first so the base color space can convert the batch at once.
|
||||
const tinted = new Float32Array(count * baseNumComps);
|
||||
|
||||
for (let i = 0, srcOffset = 0, tintedOffset = 0; i < count; i++) {
|
||||
tintFn(src, srcOffset, tinted, tintedOffset);
|
||||
srcOffset += numComps;
|
||||
tintedOffset += baseNumComps;
|
||||
}
|
||||
base.getRgbItems(tinted, count, dest, destOffset, alpha01);
|
||||
}
|
||||
|
||||
getRgbBuffer(src, srcOffset, count, dest, destOffset, bits, alpha01) {
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
|
||||
assert(
|
||||
|
||||
@ -117,6 +117,21 @@ class IccColorSpace extends ColorSpace {
|
||||
QCMS._destBuffer = null;
|
||||
}
|
||||
|
||||
getRgbItems(src, count, dest, destOffset, alpha01) {
|
||||
const { numComps } = this;
|
||||
const length = count * numComps;
|
||||
const scaled = new Uint8Array(length);
|
||||
// Uint8Array matches the truncation and wrapping of the scalar Wasm calls.
|
||||
for (let i = 0; i < length; i++) {
|
||||
scaled[i] = src[i] * 255;
|
||||
}
|
||||
QCMS._destBuffer = dest;
|
||||
QCMS._destOffset = destOffset;
|
||||
QCMS._destLength = count * (3 + alpha01);
|
||||
qcms_convert_array(this.#transformer, scaled);
|
||||
QCMS._destBuffer = null;
|
||||
}
|
||||
|
||||
getRgbBuffer(src, srcOffset, count, dest, destOffset, bits, alpha01) {
|
||||
src = src.subarray(srcOffset, srcOffset + count * this.numComps);
|
||||
if (bits !== 8) {
|
||||
|
||||
@ -46,6 +46,17 @@ const ShadingType = {
|
||||
TENSOR_PATCH_MESH: 7,
|
||||
};
|
||||
|
||||
// Bound temporary buffers; DeviceN component counts come from the PDF.
|
||||
const MAX_SAMPLED_COLOR_COMPONENTS = 1 << 16;
|
||||
|
||||
function getColorConversionBatchSize(count, numComps) {
|
||||
return MathClamp(
|
||||
Math.floor(MAX_SAMPLED_COLOR_COMPONENTS / numComps),
|
||||
1,
|
||||
count
|
||||
);
|
||||
}
|
||||
|
||||
class Pattern {
|
||||
// eslint-disable-next-line no-unused-private-class-members
|
||||
static #hasGPU = false;
|
||||
@ -202,22 +213,32 @@ class RadialAxialShading extends BaseShading {
|
||||
return;
|
||||
}
|
||||
|
||||
const color = new Float32Array(cs.numComps),
|
||||
ratio = new Float32Array(1);
|
||||
const { numComps } = cs;
|
||||
const ratio = new Float32Array(1);
|
||||
|
||||
// Batch colors to reduce ICC Wasm calls and bound temporary memory.
|
||||
const batchSize = getColorConversionBatchSize(NUMBER_OF_SAMPLES, numComps);
|
||||
const comps = new Float32Array(batchSize * numComps);
|
||||
const rgb = new Uint8ClampedArray(NUMBER_OF_SAMPLES * 3);
|
||||
for (let start = 0; start < NUMBER_OF_SAMPLES; start += batchSize) {
|
||||
const count = Math.min(batchSize, NUMBER_OF_SAMPLES - start);
|
||||
for (let i = 0, offset = 0; i < count; i++, offset += numComps) {
|
||||
ratio[0] = t0 + (start + i) * step;
|
||||
fn(ratio, 0, comps, offset);
|
||||
}
|
||||
cs.getRgbItems(comps, count, rgb, start * 3, /* alpha01 = */ 0);
|
||||
}
|
||||
|
||||
let iBase = 0;
|
||||
ratio[0] = t0;
|
||||
fn(ratio, 0, color, 0);
|
||||
const rgbBuffer = new Uint8ClampedArray(3);
|
||||
cs.getRgb(color, 0, rgbBuffer);
|
||||
let [rBase, gBase, bBase] = rgbBuffer;
|
||||
let rBase = rgb[0],
|
||||
gBase = rgb[1],
|
||||
bBase = rgb[2];
|
||||
colorStops.push([0, Util.makeHexColor(rBase, gBase, bBase)]);
|
||||
|
||||
let iPrev = 1;
|
||||
ratio[0] = t0 + step;
|
||||
fn(ratio, 0, color, 0);
|
||||
cs.getRgb(color, 0, rgbBuffer);
|
||||
let [rPrev, gPrev, bPrev] = rgbBuffer;
|
||||
let rPrev = rgb[3],
|
||||
gPrev = rgb[4],
|
||||
bPrev = rgb[5];
|
||||
|
||||
// Slopes are rise / run.
|
||||
// A max slope is from the least value the base component could have been
|
||||
@ -236,10 +257,10 @@ class RadialAxialShading extends BaseShading {
|
||||
let minSlopeB = bPrev - bBase - 1;
|
||||
|
||||
for (let i = 2; i < NUMBER_OF_SAMPLES; i++) {
|
||||
ratio[0] = t0 + i * step;
|
||||
fn(ratio, 0, color, 0);
|
||||
cs.getRgb(color, 0, rgbBuffer);
|
||||
const [r, g, b] = rgbBuffer;
|
||||
const rgbOffset = i * 3;
|
||||
const r = rgb[rgbOffset],
|
||||
g = rgb[rgbOffset + 1],
|
||||
b = rgb[rgbOffset + 2];
|
||||
|
||||
// Keep going if the maximum minimum slope <= the minimum maximum slope.
|
||||
// Otherwise add a rgbPrev color stop and make it the new base.
|
||||
@ -499,13 +520,18 @@ class FunctionBasedShading extends BaseShading {
|
||||
const coords = (this.coords = new Float32Array(totalVertices * 2));
|
||||
const colors = (this.colors = new Uint8ClampedArray(totalVertices * 4));
|
||||
|
||||
const { numComps } = cs;
|
||||
const xyBuf = new Float32Array(2);
|
||||
const colorBuf = new Float32Array(cs.numComps);
|
||||
// Batch colors to reduce ICC Wasm calls and bound temporary memory.
|
||||
const batchSize = getColorConversionBatchSize(totalVertices, numComps);
|
||||
const comps = new Float32Array(batchSize * numComps);
|
||||
const rangeX = (x1 - x0) / stepsX;
|
||||
const rangeY = (y1 - y0) / stepsY;
|
||||
const halfStepX = rangeX / 2;
|
||||
const halfStepY = rangeY / 2;
|
||||
let coordOffset = 0;
|
||||
let compOffset = 0;
|
||||
let batchCount = 0;
|
||||
let colorOffset = 0;
|
||||
for (let row = 0; row <= stepsY; row++) {
|
||||
const yDomain = y0 + rangeY * row;
|
||||
@ -515,16 +541,32 @@ class FunctionBasedShading extends BaseShading {
|
||||
for (let col = 0; col <= stepsX; col++) {
|
||||
const xDomain = x0 + rangeX * col;
|
||||
xyBuf[0] = col === stepsX ? xDomain - halfStepX : xDomain;
|
||||
fn(xyBuf, 0, colorBuf, 0);
|
||||
fn(xyBuf, 0, comps, compOffset);
|
||||
compOffset += numComps;
|
||||
batchCount++;
|
||||
|
||||
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
|
||||
if (batchCount === batchSize) {
|
||||
cs.getRgbItems(
|
||||
comps,
|
||||
batchCount,
|
||||
colors,
|
||||
colorOffset,
|
||||
/* alpha01 = */ 1
|
||||
);
|
||||
colorOffset += batchCount * 4;
|
||||
compOffset = batchCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (batchCount > 0) {
|
||||
cs.getRgbItems(comps, batchCount, colors, colorOffset, /* alpha01 = */ 1);
|
||||
}
|
||||
// Alpha stays zero.
|
||||
|
||||
const ps = new Uint32Array(totalVertices);
|
||||
for (let i = 0; i < totalVertices; i++) {
|
||||
|
||||
1
test/pdfs/.gitignore
vendored
1
test/pdfs/.gitignore
vendored
@ -949,3 +949,4 @@
|
||||
!jpx_smaskindata.pdf
|
||||
!nonisolated_blend_smask.pdf
|
||||
!signed_verified.pdf
|
||||
!function_based_shading_cmyk.pdf
|
||||
|
||||
BIN
test/pdfs/function_based_shading_cmyk.pdf
Normal file
BIN
test/pdfs/function_based_shading_cmyk.pdf
Normal file
Binary file not shown.
@ -14508,5 +14508,13 @@
|
||||
"md5": "4b87611416be34894eeb19c158fafbad",
|
||||
"rounds": 1,
|
||||
"type": "eq"
|
||||
},
|
||||
{
|
||||
"id": "function_based_shading_cmyk",
|
||||
"file": "pdfs/function_based_shading_cmyk.pdf",
|
||||
"md5": "6c2b4d131a007a6f40ac7a892ad5f2a1",
|
||||
"rounds": 1,
|
||||
"lastPage": 1,
|
||||
"type": "eq"
|
||||
}
|
||||
]
|
||||
|
||||
@ -53,6 +53,21 @@ describe("colorspace", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ColorSpace.getRgbItems", function () {
|
||||
it("should honor the destination offset and alpha stride", function () {
|
||||
const src = new Float32Array([0, 0.25, 0.5, 1, 0.75, 0.5, 0.1, 0.2, 0.3]);
|
||||
const dest = new Uint8ClampedArray(14).fill(9);
|
||||
|
||||
ColorSpaceUtils.rgb.getRgbItems(src, 3, dest, 1, /* alpha01 = */ 1);
|
||||
|
||||
expect(dest).toEqual(
|
||||
new Uint8ClampedArray([
|
||||
9, 0, 64, 128, 9, 255, 191, 128, 9, 26, 51, 77, 9, 9,
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ColorSpace caching", function () {
|
||||
let globalColorSpaceCache, localColorSpaceCache;
|
||||
|
||||
|
||||
@ -37,7 +37,12 @@ describe("pattern", function () {
|
||||
} = {}) {
|
||||
const dict = new Dict();
|
||||
dict.set("ShadingType", 1);
|
||||
dict.set("ColorSpace", Name.get(colorSpace));
|
||||
if (Array.isArray(colorSpace)) {
|
||||
// `setIfName` only accepts a name, and would silently drop an Array.
|
||||
dict.set("ColorSpace", colorSpace);
|
||||
} else {
|
||||
dict.setIfName("ColorSpace", colorSpace);
|
||||
}
|
||||
dict.set("Domain", domain);
|
||||
dict.set("Matrix", matrix);
|
||||
if (background) {
|
||||
@ -86,6 +91,44 @@ describe("pattern", function () {
|
||||
expect(ir[7]).toBeNull();
|
||||
});
|
||||
|
||||
it("must bound the color component buffer while batching", function () {
|
||||
const numComps = 32;
|
||||
let batchStarts = 0;
|
||||
let componentBufferLength = 0;
|
||||
const colorSpace = [
|
||||
Name.get("DeviceN"),
|
||||
Array.from({ length: numComps }, (_, i) => Name.get(`Colorant${i}`)),
|
||||
Name.get("DeviceRGB"),
|
||||
{
|
||||
fn(src, srcOffset, dest, destOffset) {
|
||||
dest[destOffset] = src[srcOffset];
|
||||
dest[destOffset + 1] = src[srcOffset + 1];
|
||||
dest[destOffset + 2] = src[srcOffset + 2];
|
||||
},
|
||||
},
|
||||
];
|
||||
const shading = createFunctionBasedShading({
|
||||
colorSpace,
|
||||
matrix: [64, 0, 0, 64, 0, 0],
|
||||
fn(src, srcOffset, dest, destOffset) {
|
||||
if (destOffset === 0) {
|
||||
batchStarts++;
|
||||
componentBufferLength = dest.length;
|
||||
}
|
||||
dest[destOffset] = src[srcOffset];
|
||||
dest[destOffset + 1] = src[srcOffset + 1];
|
||||
dest[destOffset + 2] = 0.5;
|
||||
dest.fill(0, destOffset + 3, destOffset + numComps);
|
||||
},
|
||||
});
|
||||
const [, , , colors] = shading.getIR();
|
||||
|
||||
const totalVertices = (64 + 1) ** 2;
|
||||
expect(batchStarts).toBeGreaterThan(1);
|
||||
expect(componentBufferLength).toBeLessThan(totalVertices * numComps);
|
||||
expect(Array.from(colors.subarray(0, 4))).toEqual([0, 0, 128, 0]);
|
||||
});
|
||||
|
||||
it("must keep mesh colors intact through binary serialization", function () {
|
||||
const shading = createFunctionBasedShading({
|
||||
background: [0.25, 0.5, 0.75],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user