mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-06-08 11:11:02 +02:00
Merge pull request #20900 from wooorm/wooorm/image-mask-perf
Refactor to improve performance around image masks (bug 1941562)
This commit is contained in:
commit
601d961fc3
@ -35,47 +35,46 @@ import { JpxImage } from "./jpx.js";
|
||||
import { Name } from "./primitives.js";
|
||||
|
||||
/**
|
||||
* Resizes an image mask with 1 component.
|
||||
* @param {TypedArray} src - The source buffer.
|
||||
* @param {number} bpc - Number of bits per component.
|
||||
* @param {number} w1 - Original width.
|
||||
* @param {number} h1 - Original height.
|
||||
* @param {number} w2 - New width.
|
||||
* @param {number} h2 - New height.
|
||||
* @returns {TypedArray} The resized image mask buffer.
|
||||
* Configuration for {@linkcode PDFImage.fillGrayBuffer}.
|
||||
*
|
||||
* @typedef FillGrayBufferOptions
|
||||
* @property {number} [destWidth]
|
||||
* Destination width; defaults to the source image width (no resampling).
|
||||
* @property {number} [destHeight]
|
||||
* Destination height; defaults to the source image height (no resampling).
|
||||
* @property {boolean} [invertOutput=false]
|
||||
* Whether to invert the output values (as in `x = 255 - x`).
|
||||
* @property {number} [maxRows]
|
||||
* Maximum number of destination rows to write.
|
||||
* @property {number} [offset=0]
|
||||
* Where to start.
|
||||
* @property {number} [stride=1]
|
||||
* Step size between consecutive elements.
|
||||
*/
|
||||
function resizeImageMask(src, bpc, w1, h1, w2, h2) {
|
||||
const length = w2 * h2;
|
||||
let dest;
|
||||
if (bpc <= 8) {
|
||||
dest = new Uint8Array(length);
|
||||
} else if (bpc <= 16) {
|
||||
dest = new Uint16Array(length);
|
||||
} else {
|
||||
dest = new Uint32Array(length);
|
||||
}
|
||||
const xRatio = w1 / w2;
|
||||
const yRatio = h1 / h2;
|
||||
let i,
|
||||
j,
|
||||
py,
|
||||
newIndex = 0,
|
||||
oldIndex;
|
||||
const xScaled = new Uint16Array(w2);
|
||||
const w1Scanline = w1;
|
||||
|
||||
for (i = 0; i < w2; i++) {
|
||||
xScaled[i] = Math.floor(i * xRatio);
|
||||
}
|
||||
for (i = 0; i < h2; i++) {
|
||||
py = Math.floor(i * yRatio) * w1Scanline;
|
||||
for (j = 0; j < w2; j++) {
|
||||
oldIndex = py + xScaled[j];
|
||||
dest[newIndex++] = src[oldIndex];
|
||||
}
|
||||
}
|
||||
return dest;
|
||||
}
|
||||
/**
|
||||
* Configuration for {@linkcode FillMaskAlphaCallback} functions.
|
||||
*
|
||||
* @typedef FillMaskAlphaOptions
|
||||
* @property {number} maxRows
|
||||
* Maximum number of image rows to write; defaults to the full image height.
|
||||
* @property {number} offset
|
||||
* Where to start.
|
||||
* @property {number} stride
|
||||
* Step size between consecutive elements.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fills the alpha values for the mask.
|
||||
*
|
||||
* @callback FillMaskAlphaCallback
|
||||
* @param {Uint8ClampedArray} buffer
|
||||
* Buffer to write the alpha values to.
|
||||
* @param {FillMaskAlphaOptions} options
|
||||
* Configuration for filling the alpha values.
|
||||
* @return {Promise<undefined> | undefined | void}
|
||||
* Optional promise that resolves when the alpha values have been filled.
|
||||
*/
|
||||
|
||||
class PDFImage {
|
||||
constructor({
|
||||
@ -242,6 +241,8 @@ class PDFImage {
|
||||
this.jpxDecoderOptions.isIndexedColormap =
|
||||
this.colorSpace.name === "Indexed";
|
||||
}
|
||||
} else {
|
||||
this.numComps = 1;
|
||||
}
|
||||
|
||||
this.decode = dict.getArray("D", "Decode");
|
||||
@ -599,67 +600,64 @@ class PDFImage {
|
||||
'PDFImage.fillOpacity: Unsupported "rgbaBuf" type.'
|
||||
);
|
||||
}
|
||||
const smask = this.smask;
|
||||
const mask = this.mask;
|
||||
let alphaBuf, sw, sh, i, ii, j;
|
||||
/** @type {FillMaskAlphaCallback} */
|
||||
let apply;
|
||||
|
||||
if (smask) {
|
||||
sw = smask.width;
|
||||
sh = smask.height;
|
||||
alphaBuf = new Uint8ClampedArray(sw * sh);
|
||||
await smask.fillGrayBuffer(alphaBuf);
|
||||
if (sw !== width || sh !== height) {
|
||||
alphaBuf = resizeImageMask(alphaBuf, smask.bpc, sw, sh, width, height);
|
||||
}
|
||||
} else if (mask) {
|
||||
if (mask instanceof PDFImage) {
|
||||
sw = mask.width;
|
||||
sh = mask.height;
|
||||
alphaBuf = new Uint8ClampedArray(sw * sh);
|
||||
mask.numComps = 1;
|
||||
await mask.fillGrayBuffer(alphaBuf);
|
||||
|
||||
// Need to invert values in rgbaBuf
|
||||
for (i = 0, ii = sw * sh; i < ii; ++i) {
|
||||
alphaBuf[i] = 255 - alphaBuf[i];
|
||||
}
|
||||
|
||||
if (sw !== width || sh !== height) {
|
||||
alphaBuf = resizeImageMask(alphaBuf, mask.bpc, sw, sh, width, height);
|
||||
}
|
||||
} else if (Array.isArray(mask)) {
|
||||
if (this.smask) {
|
||||
apply = (buffer, options) =>
|
||||
this.smask.fillGrayBuffer(buffer, {
|
||||
...options,
|
||||
destWidth: width,
|
||||
destHeight: height,
|
||||
});
|
||||
} else if (this.mask) {
|
||||
if (this.mask instanceof PDFImage) {
|
||||
// Single mask.
|
||||
apply = (buffer, options) =>
|
||||
this.mask.fillGrayBuffer(buffer, {
|
||||
...options,
|
||||
invertOutput: true,
|
||||
destWidth: width,
|
||||
destHeight: height,
|
||||
});
|
||||
} else if (Array.isArray(this.mask)) {
|
||||
// Color key mask: if any of the components are outside the range
|
||||
// then they should be painted.
|
||||
alphaBuf = new Uint8ClampedArray(width * height);
|
||||
const numComps = this.numComps;
|
||||
for (i = 0, ii = width * height; i < ii; ++i) {
|
||||
let opacity = 0;
|
||||
const imageOffset = i * numComps;
|
||||
for (j = 0; j < numComps; ++j) {
|
||||
const color = image[imageOffset + j];
|
||||
const maskOffset = j * 2;
|
||||
if (color < mask[maskOffset] || color > mask[maskOffset + 1]) {
|
||||
opacity = 255;
|
||||
break;
|
||||
apply = (buffer, { maxRows, offset, stride }) => {
|
||||
for (let i = 0, ii = width * maxRows; i < ii; ++i) {
|
||||
let opacity = 0;
|
||||
const imageOffset = i * this.numComps;
|
||||
for (let j = 0; j < this.numComps; ++j) {
|
||||
const color = image[imageOffset + j];
|
||||
const maskOffset = j * 2;
|
||||
if (
|
||||
color < this.mask[maskOffset] ||
|
||||
color > this.mask[maskOffset + 1]
|
||||
) {
|
||||
opacity = 255;
|
||||
break;
|
||||
}
|
||||
}
|
||||
buffer[i * stride + offset] = opacity;
|
||||
}
|
||||
alphaBuf[i] = opacity;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
throw new FormatError("Unknown mask format.");
|
||||
}
|
||||
}
|
||||
|
||||
if (alphaBuf) {
|
||||
for (i = 0, j = 3, ii = width * actualHeight; i < ii; ++i, j += 4) {
|
||||
rgbaBuf[j] = alphaBuf[i];
|
||||
}
|
||||
} else {
|
||||
// No mask.
|
||||
for (i = 0, j = 3, ii = width * actualHeight; i < ii; ++i, j += 4) {
|
||||
rgbaBuf[j] = 255;
|
||||
}
|
||||
apply = (buffer, { maxRows, offset, stride }) => {
|
||||
for (let i = 0, ii = width * maxRows; i < ii; ++i) {
|
||||
buffer[i * stride + offset] = 255;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
await apply(rgbaBuf, {
|
||||
maxRows: actualHeight,
|
||||
offset: 3,
|
||||
stride: 4,
|
||||
});
|
||||
}
|
||||
|
||||
undoPreblend(buffer, width, height) {
|
||||
@ -722,7 +720,7 @@ class PDFImage {
|
||||
imgData.kind = ImageKind.RGBA_32BPP;
|
||||
const imgArray = (imgData.data = await this.getImageBytes(
|
||||
originalHeight * originalWidth * 4,
|
||||
{}
|
||||
{ internal: isOffscreenCanvasSupported && mustBeResized }
|
||||
));
|
||||
|
||||
if (isOffscreenCanvasSupported) {
|
||||
@ -769,7 +767,9 @@ class PDFImage {
|
||||
if (image) {
|
||||
return image;
|
||||
}
|
||||
const data = await this.getImageBytes(originalHeight * rowBytes, {});
|
||||
const data = await this.getImageBytes(originalHeight * rowBytes, {
|
||||
internal: isOffscreenCanvasSupported && mustBeResized,
|
||||
});
|
||||
if (isOffscreenCanvasSupported) {
|
||||
if (mustBeResized) {
|
||||
return ImageResizer.createImage(
|
||||
@ -835,6 +835,7 @@ class PDFImage {
|
||||
drawWidth,
|
||||
drawHeight,
|
||||
forceRGBA: true,
|
||||
internal: true,
|
||||
});
|
||||
return this.createBitmap(
|
||||
ImageKind.RGBA_32BPP,
|
||||
@ -855,6 +856,7 @@ class PDFImage {
|
||||
drawWidth,
|
||||
drawHeight,
|
||||
forceRGB: true,
|
||||
internal: mustBeResized,
|
||||
});
|
||||
if (mustBeResized) {
|
||||
// The image is too big so we resize it.
|
||||
@ -950,7 +952,32 @@ class PDFImage {
|
||||
return imgData;
|
||||
}
|
||||
|
||||
async fillGrayBuffer(buffer) {
|
||||
/**
|
||||
* Fills `buffer` with decoded grayscale values from the image.
|
||||
*
|
||||
* When `destWidth`/`destHeight` match the source image dimensions (or are
|
||||
* omitted), pixels are sampled linearly with no extra allocation.
|
||||
* When they differ, nearest-neighbour resampling is used, sampling decoded
|
||||
* pixels directly from the `comps` array with no intermediate buffer.
|
||||
*
|
||||
* @param {Uint8ClampedArray} buffer
|
||||
* Buffer to fill with grayscale values.
|
||||
* @param {FillGrayBufferOptions} [options]
|
||||
* Configuration (optional).
|
||||
* @returns {Promise<undefined>}
|
||||
* Promise that resolves to `undefined`.
|
||||
*/
|
||||
async fillGrayBuffer(
|
||||
buffer,
|
||||
{
|
||||
destWidth,
|
||||
destHeight,
|
||||
invertOutput,
|
||||
maxRows,
|
||||
offset = 0,
|
||||
stride = 1,
|
||||
} = {}
|
||||
) {
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
|
||||
assert(
|
||||
buffer instanceof Uint8ClampedArray,
|
||||
@ -964,31 +991,75 @@ class PDFImage {
|
||||
);
|
||||
}
|
||||
|
||||
const width = this.width;
|
||||
const height = this.height;
|
||||
const srcWidth = this.width;
|
||||
const srcHeight = this.height;
|
||||
const bpc = this.bpc;
|
||||
|
||||
// rows start at byte boundary
|
||||
const rowBytes = (width * numComps * bpc + 7) >> 3;
|
||||
const imgArray = await this.getImageBytes(height * rowBytes, {
|
||||
const rowBytes = (srcWidth * numComps * bpc + 7) >> 3;
|
||||
const imgArray = await this.getImageBytes(srcHeight * rowBytes, {
|
||||
internal: true,
|
||||
});
|
||||
|
||||
const comps = this.getComponents(imgArray);
|
||||
let i, length;
|
||||
|
||||
const resolvedDestWidth = destWidth ?? srcWidth;
|
||||
const resolvedDestHeight = destHeight ?? srcHeight;
|
||||
const needsResampling =
|
||||
resolvedDestWidth !== srcWidth || resolvedDestHeight !== srcHeight;
|
||||
const rows =
|
||||
maxRows === undefined
|
||||
? resolvedDestHeight
|
||||
: Math.min(resolvedDestHeight, maxRows);
|
||||
|
||||
let outputWidth = srcWidth;
|
||||
let yRatio = 0;
|
||||
let xScaled = null;
|
||||
if (needsResampling) {
|
||||
outputWidth = resolvedDestWidth;
|
||||
yRatio = srcHeight / resolvedDestHeight;
|
||||
const xRatio = srcWidth / resolvedDestWidth;
|
||||
xScaled = new Uint32Array(resolvedDestWidth);
|
||||
for (let i = 0; i < resolvedDestWidth; i++) {
|
||||
xScaled[i] = Math.floor(i * xRatio);
|
||||
}
|
||||
}
|
||||
|
||||
const mask = invertOutput ? 0xff : 0;
|
||||
|
||||
if (bpc === 1) {
|
||||
// inline decoding (= inversion) for 1 bpc images
|
||||
length = width * height;
|
||||
if (this.needsDecode) {
|
||||
// invert and scale to {0, 255}
|
||||
for (i = 0; i < length; ++i) {
|
||||
buffer[i] = (comps[i] - 1) & 255;
|
||||
if (xScaled) {
|
||||
const xMap = xScaled;
|
||||
let destIndex = offset;
|
||||
if (this.needsDecode) {
|
||||
for (let row = 0; row < rows; row++) {
|
||||
const py = Math.floor(row * yRatio) * srcWidth;
|
||||
for (let col = 0; col < outputWidth; col++) {
|
||||
buffer[destIndex] = ((comps[py + xMap[col]] - 1) & 255) ^ mask;
|
||||
destIndex += stride;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let row = 0; row < rows; row++) {
|
||||
const py = Math.floor(row * yRatio) * srcWidth;
|
||||
for (let col = 0; col < outputWidth; col++) {
|
||||
buffer[destIndex] = (-comps[py + xMap[col]] & 255) ^ mask;
|
||||
destIndex += stride;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// scale to {0, 255}
|
||||
for (i = 0; i < length; ++i) {
|
||||
buffer[i] = -comps[i] & 255;
|
||||
const length = outputWidth * rows;
|
||||
if (this.needsDecode) {
|
||||
// invert and scale to {0, 255}
|
||||
for (let i = 0; i < length; ++i) {
|
||||
buffer[i * stride + offset] = ((comps[i] - 1) & 255) ^ mask;
|
||||
}
|
||||
} else {
|
||||
// scale to {0, 255}
|
||||
for (let i = 0; i < length; ++i) {
|
||||
buffer[i * stride + offset] = (-comps[i] & 255) ^ mask;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
@ -997,11 +1068,23 @@ class PDFImage {
|
||||
if (this.needsDecode) {
|
||||
this.decodeBuffer(comps);
|
||||
}
|
||||
length = width * height;
|
||||
// we aren't using a colorspace so we need to scale the value
|
||||
const scale = 255 / ((1 << bpc) - 1);
|
||||
for (i = 0; i < length; ++i) {
|
||||
buffer[i] = scale * comps[i];
|
||||
if (xScaled) {
|
||||
const xMap = xScaled;
|
||||
let destIndex = offset;
|
||||
for (let row = 0; row < rows; row++) {
|
||||
const py = Math.floor(row * yRatio) * srcWidth;
|
||||
for (let col = 0; col < outputWidth; col++) {
|
||||
buffer[destIndex] = (scale * comps[py + xMap[col]]) ^ mask;
|
||||
destIndex += stride;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const length = outputWidth * rows;
|
||||
for (let i = 0; i < length; ++i) {
|
||||
buffer[i * stride + offset] = (scale * comps[i]) ^ mask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1068,14 +1151,16 @@ class PDFImage {
|
||||
this.jpxDecoderOptions
|
||||
);
|
||||
|
||||
// If imageBytes came from a DecodeStream, we're safe to transfer it
|
||||
// (and thus detach its underlying buffer) because it will constitute
|
||||
// the entire DecodeStream's data. But if it came from a Stream, we
|
||||
// need to copy it because it'll only be a portion of the Stream's
|
||||
// data, and the rest will be read later on.
|
||||
if (internal || this.image instanceof DecodeStream) {
|
||||
// Internal callers never transfer/return raw bytes out of the worker,
|
||||
// and DecodeStream-backed bytes are self-contained for the decode.
|
||||
return imageBytes;
|
||||
}
|
||||
|
||||
// Stream-backed image data can be a subarray into shared stream storage,
|
||||
// so returning it directly would risk detaching/mutating bytes that
|
||||
// subsequent stream reads still need.
|
||||
// Always return a fresh copy.
|
||||
assert(
|
||||
imageBytes instanceof Uint8Array,
|
||||
'PDFImage.getImageBytes: Unsupported "imageBytes" type.'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user