Merge pull request #19773 from Snuffleupagus/inline-PDFImage-createRawMask

Inline `PDFImage.createRawMask` in the `PDFImage.createMask` method
This commit is contained in:
Jonas Jenwald 2025-04-08 17:19:09 +02:00 committed by GitHub
commit 12c7c7b0af
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 85 additions and 114 deletions

View File

@ -72,7 +72,6 @@ import { BaseStream } from "./base_stream.js";
import { bidi } from "./bidi.js"; import { bidi } from "./bidi.js";
import { ColorSpace } from "./colorspace.js"; import { ColorSpace } from "./colorspace.js";
import { ColorSpaceUtils } from "./colorspace_utils.js"; import { ColorSpaceUtils } from "./colorspace_utils.js";
import { DecodeStream } from "./decode_stream.js";
import { getFontSubstitution } from "./font_substitutions.js"; import { getFontSubstitution } from "./font_substitutions.js";
import { getGlyphsUnicode } from "./glyphlist.js"; import { getGlyphsUnicode } from "./glyphlist.js";
import { getMetrics } from "./metrics.js"; import { getMetrics } from "./metrics.js";
@ -571,7 +570,10 @@ class PartialEvaluator {
localImageCache, localImageCache,
localColorSpaceCache, localColorSpaceCache,
}) { }) {
const dict = image.dict; const { maxImageSize, ignoreErrors, isOffscreenCanvasSupported } =
this.options;
const { dict } = image;
const imageRef = dict.objId; const imageRef = dict.objId;
const w = dict.get("W", "Width"); const w = dict.get("W", "Width");
const h = dict.get("H", "Height"); const h = dict.get("H", "Height");
@ -580,15 +582,14 @@ class PartialEvaluator {
warn("Image dimensions are missing, or not numbers."); warn("Image dimensions are missing, or not numbers.");
return; return;
} }
const maxImageSize = this.options.maxImageSize;
if (maxImageSize !== -1 && w * h > maxImageSize) { if (maxImageSize !== -1 && w * h > maxImageSize) {
const msg = "Image exceeded maximum allowed size and was removed."; const msg = "Image exceeded maximum allowed size and was removed.";
if (this.options.ignoreErrors) { if (!ignoreErrors) {
warn(msg); throw new Error(msg);
return;
} }
throw new Error(msg); warn(msg);
return;
} }
let optionalContent; let optionalContent;
@ -607,52 +608,10 @@ class PartialEvaluator {
// data can't be done here. Instead of creating a // data can't be done here. Instead of creating a
// complete PDFImage, only read the information needed // complete PDFImage, only read the information needed
// for later. // for later.
const interpolate = dict.get("I", "Interpolate");
const bitStrideLength = (w + 7) >> 3;
const imgArray = image.getBytes(bitStrideLength * h);
const decode = dict.getArray("D", "Decode");
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.
imgData = PDFImage.createRawMask({
imgArray,
width: w,
height: h,
imageIsFromDecodeStream: image instanceof DecodeStream,
inverseDecode: decode?.[0] > 0,
interpolate,
});
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;
}
imgData = await PDFImage.createMask({ imgData = await PDFImage.createMask({
imgArray, image,
width: w, isOffscreenCanvasSupported:
height: h, isOffscreenCanvasSupported && !this.parsingType3Font,
imageIsFromDecodeStream: image instanceof DecodeStream,
inverseDecode: decode?.[0] > 0,
interpolate,
isOffscreenCanvasSupported: this.options.isOffscreenCanvasSupported,
}); });
if (imgData.isSingleOpaquePixel) { if (imgData.isSingleOpaquePixel) {
@ -677,6 +636,36 @@ class PartialEvaluator {
return; 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()}`; const objId = `mask_${this.idFactory.createObjId()}`;
operatorList.addDependency(objId); operatorList.addDependency(objId);
@ -736,7 +725,7 @@ class PartialEvaluator {
} catch (reason) { } catch (reason) {
const msg = `Unable to decode inline image: "${reason}".`; const msg = `Unable to decode inline image: "${reason}".`;
if (!this.options.ignoreErrors) { if (!ignoreErrors) {
throw new Error(msg); throw new Error(msg);
} }
warn(msg); warn(msg);
@ -819,8 +808,7 @@ class PartialEvaluator {
.then(async imageObj => { .then(async imageObj => {
imgData = await imageObj.createImageData( imgData = await imageObj.createImageData(
/* forceRGBA = */ false, /* forceRGBA = */ false,
/* isOffscreenCanvasSupported = */ this.options isOffscreenCanvasSupported
.isOffscreenCanvasSupported
); );
imgData.dataLen = imgData.bitmap imgData.dataLen = imgData.bitmap
? imgData.width * imgData.height * 4 ? imgData.width * imgData.height * 4

View File

@ -348,58 +348,18 @@ class PDFImage {
}); });
} }
static createRawMask({ static async createMask({ image, isOffscreenCanvasSupported = false }) {
imgArray, const { dict } = image;
width, const width = dict.get("W", "Width");
height, const height = dict.get("H", "Height");
imageIsFromDecodeStream,
inverseDecode, const interpolate = dict.get("I", "Interpolate");
interpolate, const decode = dict.getArray("D", "Decode");
}) { const inverseDecode = decode?.[0] > 0;
// |imgArray| might not contain full data for every pixel of the mask, so
// we need to distinguish between |computedLength| and |actualLength|.
// In particular, if inverseDecode is true, then the array we return must
// have a length of |computedLength|.
const computedLength = ((width + 7) >> 3) * height; const computedLength = ((width + 7) >> 3) * height;
const actualLength = imgArray.byteLength; const imgArray = image.getBytes(computedLength);
const haveFullData = computedLength === actualLength;
let data, i;
if (imageIsFromDecodeStream && (!inverseDecode || haveFullData)) {
// imgArray came from a DecodeStream and its data is in an appropriate
// form, so we can just transfer it.
data = imgArray;
} else if (!inverseDecode) {
data = new Uint8Array(imgArray);
} else {
data = new Uint8Array(computedLength);
data.set(imgArray);
data.fill(0xff, actualLength);
}
// If necessary, invert the original mask data (but not any extra we might
// have added above). It's safe to modify the array -- whether it's the
// original or a copy, we're about to transfer it anyway, so nothing else
// in this thread can be relying on its contents.
if (inverseDecode) {
for (i = 0; i < actualLength; i++) {
data[i] ^= 0xff;
}
}
return { data, width, height, interpolate };
}
static async createMask({
imgArray,
width,
height,
imageIsFromDecodeStream,
inverseDecode,
interpolate,
isOffscreenCanvasSupported = false,
}) {
const isSingleOpaquePixel = const isSingleOpaquePixel =
width === 1 && width === 1 &&
height === 1 && height === 1 &&
@ -452,17 +412,40 @@ class PDFImage {
bitmap, bitmap,
}; };
} }
// Fallback to get the data almost as they're and they'll be decoded
// Get the data almost as they're and they'll be decoded
// just before being drawn. // just before being drawn.
return this.createRawMask({
imgArray, // |imgArray| might not contain full data for every pixel of the mask, so
width, // we need to distinguish between |computedLength| and |actualLength|.
height, // In particular, if inverseDecode is true, then the array we return must
inverseDecode, // have a length of |computedLength|.
imageIsFromDecodeStream, const actualLength = imgArray.byteLength;
interpolate, const haveFullData = computedLength === actualLength;
}); let data;
if (image instanceof DecodeStream && (!inverseDecode || haveFullData)) {
// imgArray came from a DecodeStream and its data is in an appropriate
// form, so we can just transfer it.
data = imgArray;
} else if (!inverseDecode) {
data = new Uint8Array(imgArray);
} else {
data = new Uint8Array(computedLength);
data.set(imgArray);
data.fill(0xff, actualLength);
}
// If necessary, invert the original mask data (but not any extra we might
// have added above). It's safe to modify the array -- whether it's the
// original or a copy, we're about to transfer it anyway, so nothing else
// in this thread can be relying on its contents.
if (inverseDecode) {
for (let i = 0; i < actualLength; i++) {
data[i] ^= 0xff;
}
}
return { data, width, height, interpolate };
} }
get drawWidth() { get drawWidth() {