in src/core/evaluator.js [568:849]
async buildPaintImageXObject({
resources,
image,
isInline = false,
operatorList,
cacheKey,
localImageCache,
localColorSpaceCache,
}) {
const { maxImageSize, ignoreErrors, isOffscreenCanvasSupported } =
this.options;
const { dict } = image;
const imageRef = dict.objId;
const w = dict.get("W", "Width");
const h = dict.get("H", "Height");
if (!(w && typeof w === "number") || !(h && typeof h === "number")) {
warn("Image dimensions are missing, or not numbers.");
return;
}
if (maxImageSize !== -1 && w * h > maxImageSize) {
const msg = "Image exceeded maximum allowed size and was removed.";
if (!ignoreErrors) {
throw new Error(msg);
}
warn(msg);
return;
}
let optionalContent;
if (dict.has("OC")) {
optionalContent = await this.parseMarkedContentProps(
dict.get("OC"),
resources
);
}
const imageMask = dict.get("IM", "ImageMask") || false;
let imgData, fn, args;
if (imageMask) {
// This depends on a tmpCanvas being filled with the
// current fillStyle, such that processing the pixel
// data can't be done here. Instead of creating a
// complete PDFImage, only read the information needed
// for later.
imgData = await PDFImage.createMask({
image,
isOffscreenCanvasSupported:
isOffscreenCanvasSupported && !this.parsingType3Font,
});
if (imgData.isSingleOpaquePixel) {
// Handles special case of mainly LaTeX documents which use image
// masks to draw lines with the current fill style.
fn = OPS.paintSolidColorImageMask;
args = [];
operatorList.addImageOps(fn, args, optionalContent);
if (cacheKey) {
const cacheData = { fn, args, optionalContent };
localImageCache.set(cacheKey, imageRef, cacheData);
if (imageRef) {
this._regionalImageCache.set(
/* name = */ null,
imageRef,
cacheData
);
}
}
return;
}
if (this.parsingType3Font) {
// NOTE: Compared to other image resources we don't bother caching
// Type3-glyph image masks, since we've not come across any cases
// where that actually helps.
// In Type3-glyphs image masks are "always" inline resources,
// they're usually fairly small and aren't being re-used either.
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
assert(
imgData.data instanceof Uint8Array,
"Type3 glyph image mask must be a TypedArray."
);
}
args = compileType3Glyph(imgData);
if (args) {
operatorList.addImageOps(OPS.constructPath, args, optionalContent);
return;
}
warn("Cannot compile Type3 glyph.");
// If compilation failed, or was disabled, fallback to using an inline
// image mask; this case should be extremely rare.
operatorList.addImageOps(
OPS.paintImageMaskXObject,
[imgData],
optionalContent
);
return;
}
const objId = `mask_${this.idFactory.createObjId()}`;
operatorList.addDependency(objId);
imgData.dataLen = imgData.bitmap
? imgData.width * imgData.height * 4
: imgData.data.length;
this._sendImgData(objId, imgData);
fn = OPS.paintImageMaskXObject;
args = [
{
data: objId,
width: imgData.width,
height: imgData.height,
interpolate: imgData.interpolate,
count: 1,
},
];
operatorList.addImageOps(fn, args, optionalContent);
if (cacheKey) {
const cacheData = { objId, fn, args, optionalContent };
localImageCache.set(cacheKey, imageRef, cacheData);
if (imageRef) {
this._regionalImageCache.set(/* name = */ null, imageRef, cacheData);
}
}
return;
}
const SMALL_IMAGE_DIMENSIONS = 200;
const hasMask = dict.has("SMask") || dict.has("Mask");
// Inlining small images into the queue as RGB data
if (isInline && w + h < SMALL_IMAGE_DIMENSIONS && !hasMask) {
try {
const imageObj = new PDFImage({
xref: this.xref,
res: resources,
image,
isInline,
pdfFunctionFactory: this._pdfFunctionFactory,
globalColorSpaceCache: this.globalColorSpaceCache,
localColorSpaceCache,
});
// We force the use of RGBA_32BPP images here, because we can't handle
// any other kind.
imgData = await imageObj.createImageData(
/* forceRGBA = */ true,
/* isOffscreenCanvasSupported = */ false
);
operatorList.addImageOps(
OPS.paintInlineImageXObject,
[imgData],
optionalContent
);
} catch (reason) {
const msg = `Unable to decode inline image: "${reason}".`;
if (!ignoreErrors) {
throw new Error(msg);
}
warn(msg);
}
return;
}
// If there is no imageMask, create the PDFImage and a lot
// of image processing can be done here.
let objId = `img_${this.idFactory.createObjId()}`,
cacheGlobally = false,
globalCacheData = null;
if (this.parsingType3Font) {
objId = `${this.idFactory.getDocId()}_type3_${objId}`;
} else if (cacheKey && imageRef) {
cacheGlobally = this.globalImageCache.shouldCache(
imageRef,
this.pageIndex
);
if (cacheGlobally) {
assert(!isInline, "Cannot cache an inline image globally.");
objId = `${this.idFactory.getDocId()}_${objId}`;
}
}
// Ensure that the dependency is added before the image is decoded.
operatorList.addDependency(objId);
fn = OPS.paintImageXObject;
args = [objId, w, h];
operatorList.addImageOps(fn, args, optionalContent, hasMask);
if (cacheGlobally) {
globalCacheData = {
objId,
fn,
args,
optionalContent,
hasMask,
byteSize: 0, // Temporary entry, to avoid `setData` returning early.
};
if (this.globalImageCache.hasDecodeFailed(imageRef)) {
this.globalImageCache.setData(imageRef, globalCacheData);
this._sendImgData(objId, /* imgData = */ null, cacheGlobally);
return;
}
// For large (at least 500x500) or more complex images that we'll cache
// globally, check if the image is still cached locally on the main-thread
// to avoid having to re-parse the image (since that can be slow).
if (w * h > 250000 || hasMask) {
const localLength = await this.handler.sendWithPromise("commonobj", [
objId,
"CopyLocalImage",
{ imageRef },
]);
if (localLength) {
this.globalImageCache.setData(imageRef, globalCacheData);
this.globalImageCache.addByteSize(imageRef, localLength);
return;
}
}
}
PDFImage.buildImage({
xref: this.xref,
res: resources,
image,
isInline,
pdfFunctionFactory: this._pdfFunctionFactory,
globalColorSpaceCache: this.globalColorSpaceCache,
localColorSpaceCache,
})
.then(async imageObj => {
imgData = await imageObj.createImageData(
/* forceRGBA = */ false,
isOffscreenCanvasSupported
);
imgData.dataLen = imgData.bitmap
? imgData.width * imgData.height * 4
: imgData.data.length;
imgData.ref = imageRef;
if (cacheGlobally) {
this.globalImageCache.addByteSize(imageRef, imgData.dataLen);
}
return this._sendImgData(objId, imgData, cacheGlobally);
})
.catch(reason => {
warn(`Unable to decode image "${objId}": "${reason}".`);
if (imageRef) {
this.globalImageCache.addDecodeFailed(imageRef);
}
return this._sendImgData(objId, /* imgData = */ null, cacheGlobally);
});
if (cacheKey) {
const cacheData = { objId, fn, args, optionalContent, hasMask };
localImageCache.set(cacheKey, imageRef, cacheData);
if (imageRef) {
this._regionalImageCache.set(/* name = */ null, imageRef, cacheData);
if (cacheGlobally) {
assert(globalCacheData, "The global cache-data must be available.");
this.globalImageCache.setData(imageRef, globalCacheData);
}
}
}
}