in src/core/image.js [698:951]
async createImageData(forceRGBA = false, isOffscreenCanvasSupported = false) {
const drawWidth = this.drawWidth;
const drawHeight = this.drawHeight;
const imgData = {
width: drawWidth,
height: drawHeight,
interpolate: this.interpolate,
kind: 0,
data: null,
// Other fields are filled in below.
};
const numComps = this.numComps;
const originalWidth = this.width;
const originalHeight = this.height;
const bpc = this.bpc;
// Rows start at byte boundary.
const rowBytes = (originalWidth * numComps * bpc + 7) >> 3;
const mustBeResized =
isOffscreenCanvasSupported &&
ImageResizer.needsToBeResized(drawWidth, drawHeight);
if (!this.smask && !this.mask && this.colorSpace.name === "DeviceRGBA") {
imgData.kind = ImageKind.RGBA_32BPP;
const imgArray = (imgData.data = await this.getImageBytes(
originalHeight * originalWidth * 4,
{}
));
if (isOffscreenCanvasSupported) {
if (!mustBeResized) {
return this.createBitmap(
ImageKind.RGBA_32BPP,
drawWidth,
drawHeight,
imgArray
);
}
return ImageResizer.createImage(imgData, false);
}
return imgData;
}
if (!forceRGBA) {
// If it is a 1-bit-per-pixel grayscale (i.e. black-and-white) image
// without any complications, we pass a same-sized copy to the main
// thread rather than expanding by 32x to RGBA form. This saves *lots*
// of memory for many scanned documents. It's also much faster.
//
// Similarly, if it is a 24-bit-per pixel RGB image without any
// complications, we avoid expanding by 1.333x to RGBA form.
let kind;
if (this.colorSpace.name === "DeviceGray" && bpc === 1) {
kind = ImageKind.GRAYSCALE_1BPP;
} else if (
this.colorSpace.name === "DeviceRGB" &&
bpc === 8 &&
!this.needsDecode
) {
kind = ImageKind.RGB_24BPP;
}
if (
kind &&
!this.smask &&
!this.mask &&
drawWidth === originalWidth &&
drawHeight === originalHeight
) {
const image = await this.#getImage(originalWidth, originalHeight);
if (image) {
return image;
}
const data = await this.getImageBytes(originalHeight * rowBytes, {});
if (isOffscreenCanvasSupported) {
if (mustBeResized) {
return ImageResizer.createImage(
{
data,
kind,
width: drawWidth,
height: drawHeight,
interpolate: this.interpolate,
},
this.needsDecode
);
}
return this.createBitmap(kind, originalWidth, originalHeight, data);
}
imgData.kind = kind;
imgData.data = data;
if (this.needsDecode) {
// Invert the buffer (which must be grayscale if we reached here).
assert(
kind === ImageKind.GRAYSCALE_1BPP,
"PDFImage.createImageData: The image must be grayscale."
);
const buffer = imgData.data;
for (let i = 0, ii = buffer.length; i < ii; i++) {
buffer[i] ^= 0xff;
}
}
return imgData;
}
if (
this.image instanceof JpegStream &&
!this.smask &&
!this.mask &&
!this.needsDecode
) {
let imageLength = originalHeight * rowBytes;
if (isOffscreenCanvasSupported && !mustBeResized) {
let isHandled = false;
switch (this.colorSpace.name) {
case "DeviceGray":
// Avoid truncating the image, since `JpegImage.getData`
// will expand the image data when `forceRGB === true`.
imageLength *= 4;
isHandled = true;
break;
case "DeviceRGB":
imageLength = (imageLength / 3) * 4;
isHandled = true;
break;
case "DeviceCMYK":
isHandled = true;
break;
}
if (isHandled) {
const image = await this.#getImage(drawWidth, drawHeight);
if (image) {
return image;
}
const rgba = await this.getImageBytes(imageLength, {
drawWidth,
drawHeight,
forceRGBA: true,
});
return this.createBitmap(
ImageKind.RGBA_32BPP,
drawWidth,
drawHeight,
rgba
);
}
} else {
switch (this.colorSpace.name) {
case "DeviceGray":
imageLength *= 3;
/* falls through */
case "DeviceRGB":
case "DeviceCMYK":
imgData.kind = ImageKind.RGB_24BPP;
imgData.data = await this.getImageBytes(imageLength, {
drawWidth,
drawHeight,
forceRGB: true,
});
if (mustBeResized) {
// The image is too big so we resize it.
return ImageResizer.createImage(imgData);
}
return imgData;
}
}
}
}
const imgArray = await this.getImageBytes(originalHeight * rowBytes, {
internal: true,
});
// imgArray can be incomplete (e.g. after CCITT fax encoding).
const actualHeight =
0 | (((imgArray.length / rowBytes) * drawHeight) / originalHeight);
const comps = this.getComponents(imgArray);
// If opacity data is present, use RGBA_32BPP form. Otherwise, use the
// more compact RGB_24BPP form if allowable.
let alpha01, maybeUndoPreblend;
let canvas, ctx, canvasImgData, data;
if (isOffscreenCanvasSupported && !mustBeResized) {
canvas = new OffscreenCanvas(drawWidth, drawHeight);
ctx = canvas.getContext("2d");
canvasImgData = ctx.createImageData(drawWidth, drawHeight);
data = canvasImgData.data;
}
imgData.kind = ImageKind.RGBA_32BPP;
if (!forceRGBA && !this.smask && !this.mask) {
if (!isOffscreenCanvasSupported || mustBeResized) {
imgData.kind = ImageKind.RGB_24BPP;
data = new Uint8ClampedArray(drawWidth * drawHeight * 3);
alpha01 = 0;
} else {
const arr = new Uint32Array(data.buffer);
arr.fill(FeatureTest.isLittleEndian ? 0xff000000 : 0x000000ff);
alpha01 = 1;
}
maybeUndoPreblend = false;
} else {
if (!isOffscreenCanvasSupported || mustBeResized) {
data = new Uint8ClampedArray(drawWidth * drawHeight * 4);
}
alpha01 = 1;
maybeUndoPreblend = true;
// Color key masking (opacity) must be performed before decoding.
await this.fillOpacity(data, drawWidth, drawHeight, actualHeight, comps);
}
if (this.needsDecode) {
this.decodeBuffer(comps);
}
this.colorSpace.fillRgb(
data,
originalWidth,
originalHeight,
drawWidth,
drawHeight,
actualHeight,
bpc,
comps,
alpha01
);
if (maybeUndoPreblend) {
this.undoPreblend(data, drawWidth, actualHeight);
}
if (isOffscreenCanvasSupported && !mustBeResized) {
ctx.putImageData(canvasImgData, 0, 0);
const bitmap = canvas.transferToImageBitmap();
return {
data: null,
width: drawWidth,
height: drawHeight,
bitmap,
interpolate: this.interpolate,
};
}
imgData.data = data;
if (mustBeResized) {
return ImageResizer.createImage(imgData);
}
return imgData;
}