Merge pull request #18204 from calixteman/issue16782

Fix decoding of JPX images having an alpha channel
This commit is contained in:
calixteman 2024-06-03 21:21:52 +02:00 committed by GitHub
commit 21e622769b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 126 additions and 39 deletions

File diff suppressed because one or more lines are too long

View File

@ -49,8 +49,8 @@ class BaseStream {
* to be fully loaded, since otherwise intermittent errors may occur; * to be fully loaded, since otherwise intermittent errors may occur;
* note the `ObjectLoader` class. * note the `ObjectLoader` class.
*/ */
async getImageData(length, ignoreColorSpace) { async getImageData(length, decoderOptions) {
return this.getBytes(length, ignoreColorSpace); return this.getBytes(length, decoderOptions);
} }
async asyncGetBytes() { async asyncGetBytes() {

View File

@ -370,6 +370,8 @@ class ColorSpace {
case "RGB": case "RGB":
case "DeviceRGB": case "DeviceRGB":
return this.singletons.rgb; return this.singletons.rgb;
case "DeviceRGBA":
return this.singletons.rgba;
case "CMYK": case "CMYK":
case "DeviceCMYK": case "DeviceCMYK":
return this.singletons.cmyk; return this.singletons.cmyk;
@ -511,6 +513,9 @@ class ColorSpace {
get rgb() { get rgb() {
return shadow(this, "rgb", new DeviceRgbCS()); return shadow(this, "rgb", new DeviceRgbCS());
}, },
get rgba() {
return shadow(this, "rgba", new DeviceRgbaCS());
},
get cmyk() { get cmyk() {
return shadow(this, "cmyk", new DeviceCmykCS()); return shadow(this, "cmyk", new DeviceCmykCS());
}, },
@ -778,6 +783,23 @@ class DeviceRgbCS extends ColorSpace {
} }
} }
/**
* The default color is `new Float32Array([0, 0, 0, 1])`.
*/
class DeviceRgbaCS extends ColorSpace {
constructor() {
super("DeviceRGBA", 4);
}
getOutputLength(inputLength, _alpha01) {
return inputLength * 4;
}
isPassthrough(bits) {
return bits === 8;
}
}
/** /**
* The default color is `new Float32Array([0, 0, 0, 1])`. * The default color is `new Float32Array([0, 0, 0, 1])`.
*/ */

View File

@ -73,7 +73,7 @@ class DecodeStream extends BaseStream {
return this.buffer[this.pos++]; return this.buffer[this.pos++];
} }
getBytes(length, ignoreColorSpace = false) { getBytes(length, decoderOptions = null) {
const pos = this.pos; const pos = this.pos;
let end; let end;
@ -82,7 +82,7 @@ class DecodeStream extends BaseStream {
end = pos + length; end = pos + length;
while (!this.eof && this.bufferLength < end) { while (!this.eof && this.bufferLength < end) {
this.readBlock(ignoreColorSpace); this.readBlock(decoderOptions);
} }
const bufEnd = this.bufferLength; const bufEnd = this.bufferLength;
if (end > bufEnd) { if (end > bufEnd) {
@ -90,7 +90,7 @@ class DecodeStream extends BaseStream {
} }
} else { } else {
while (!this.eof) { while (!this.eof) {
this.readBlock(ignoreColorSpace); this.readBlock(decoderOptions);
} }
end = this.bufferLength; end = this.bufferLength;
} }
@ -99,12 +99,12 @@ class DecodeStream extends BaseStream {
return this.buffer.subarray(pos, end); return this.buffer.subarray(pos, end);
} }
async getImageData(length, ignoreColorSpace = false) { async getImageData(length, decoderOptions = null) {
if (!this.canAsyncDecodeImageFromBuffer) { if (!this.canAsyncDecodeImageFromBuffer) {
return this.getBytes(length, ignoreColorSpace); return this.getBytes(length, decoderOptions);
} }
const data = await this.stream.asyncGetBytes(); const data = await this.stream.asyncGetBytes();
return this.decodeImage(data, ignoreColorSpace); return this.decodeImage(data, decoderOptions);
} }
reset() { reset() {

View File

@ -149,7 +149,7 @@ class FlateStream extends DecodeStream {
this.codeBuf = 0; this.codeBuf = 0;
} }
async getImageData(length, _ignoreColorSpace) { async getImageData(length, _decoderOptions) {
const data = await this.asyncGetBytes(); const data = await this.asyncGetBytes();
return data?.subarray(0, length) || this.getBytes(length); return data?.subarray(0, length) || this.getBytes(length);
} }

View File

@ -18,7 +18,6 @@ import {
FeatureTest, FeatureTest,
FormatError, FormatError,
ImageKind, ImageKind,
info,
warn, warn,
} from "../shared/util.js"; } from "../shared/util.js";
import { import {
@ -104,7 +103,6 @@ class PDFImage {
localColorSpaceCache, localColorSpaceCache,
}) { }) {
this.image = image; this.image = image;
let jpxDecode = false;
const dict = image.dict; const dict = image.dict;
const filter = dict.get("F", "Filter"); const filter = dict.get("F", "Filter");
@ -126,7 +124,11 @@ class PDFImage {
bitsPerComponent: image.bitsPerComponent, bitsPerComponent: image.bitsPerComponent,
} = JpxImage.parseImageProperties(image.stream)); } = JpxImage.parseImageProperties(image.stream));
image.stream.reset(); image.stream.reset();
jpxDecode = true; this.jpxDecoderOptions = {
numComponents: 0,
isIndexedColormap: false,
smaskInData: dict.has("SMaskInData"),
};
break; break;
case "JBIG2Decode": case "JBIG2Decode":
image.bitsPerComponent = 1; image.bitsPerComponent = 1;
@ -180,23 +182,31 @@ class PDFImage {
if (!this.imageMask) { if (!this.imageMask) {
let colorSpace = dict.getRaw("CS") || dict.getRaw("ColorSpace"); let colorSpace = dict.getRaw("CS") || dict.getRaw("ColorSpace");
if (!colorSpace) { const hasColorSpace = !!colorSpace;
info("JPX images (which do not require color spaces)"); if (!hasColorSpace) {
switch (image.numComps) { if (this.jpxDecoderOptions) {
case 1: colorSpace = Name.get("DeviceRGBA");
colorSpace = Name.get("DeviceGray"); } else {
break; switch (image.numComps) {
case 3: case 1:
colorSpace = Name.get("DeviceRGB"); colorSpace = Name.get("DeviceGray");
break; break;
case 4: case 3:
colorSpace = Name.get("DeviceCMYK"); colorSpace = Name.get("DeviceRGB");
break; break;
default: case 4:
throw new Error( colorSpace = Name.get("DeviceCMYK");
`JPX images with ${image.numComps} color components not supported.` break;
); default:
throw new Error(
`Images with ${image.numComps} color components not supported.`
);
}
} }
} else if (this.jpxDecoderOptions?.smaskInData) {
// If the jpx image has a color space then it mustn't be used in order
// to be able to use the color space that comes from the pdf.
colorSpace = Name.get("DeviceRGBA");
} }
this.colorSpace = ColorSpace.parse({ this.colorSpace = ColorSpace.parse({
@ -208,9 +218,13 @@ class PDFImage {
}); });
this.numComps = this.colorSpace.numComps; this.numComps = this.colorSpace.numComps;
// If the jpx image has a color space then it musn't be used in order to if (this.jpxDecoderOptions) {
// be able to use the color space that comes from the pdf. this.jpxDecoderOptions.numComponents = hasColorSpace ? this.numComp : 0;
this.ignoreColorSpace = jpxDecode && this.colorSpace.name === "Indexed"; // If the jpx image has a color space then it musn't be used in order to
// be able to use the color space that comes from the pdf.
this.jpxDecoderOptions.isIndexedColormap =
this.colorSpace.name === "Indexed";
}
} }
this.decode = dict.getArray("D", "Decode"); this.decode = dict.getArray("D", "Decode");
@ -691,6 +705,28 @@ class PDFImage {
isOffscreenCanvasSupported && isOffscreenCanvasSupported &&
ImageResizer.needsToBeResized(drawWidth, drawHeight); ImageResizer.needsToBeResized(drawWidth, drawHeight);
if (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 (!forceRGBA) {
// If it is a 1-bit-per-pixel grayscale (i.e. black-and-white) image // 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 // without any complications, we pass a same-sized copy to the main
@ -994,7 +1030,7 @@ class PDFImage {
this.image.forceRGB = !!forceRGB; this.image.forceRGB = !!forceRGB;
const imageBytes = await this.image.getImageData( const imageBytes = await this.image.getImageData(
length, length,
this.ignoreColorSpace this.jpxDecoderOptions
); );
// If imageBytes came from a DecodeStream, we're safe to transfer it // If imageBytes came from a DecodeStream, we're safe to transfer it

View File

@ -26,9 +26,10 @@ class JpxError extends BaseException {
class JpxImage { class JpxImage {
static #module = null; static #module = null;
static decode(data, ignoreColorSpace = false) { static decode(data, decoderOptions) {
decoderOptions ||= {};
this.#module ||= OpenJPEG({ warn }); this.#module ||= OpenJPEG({ warn });
const imageData = this.#module.decode(data, ignoreColorSpace); const imageData = this.#module.decode(data, decoderOptions);
if (typeof imageData === "string") { if (typeof imageData === "string") {
throw new JpxError(imageData); throw new JpxError(imageData);
} }

View File

@ -41,16 +41,16 @@ class JpxStream extends DecodeStream {
// directly insert all of its data into `this.buffer`. // directly insert all of its data into `this.buffer`.
} }
readBlock(ignoreColorSpace) { readBlock(decoderOptions) {
this.decodeImage(null, ignoreColorSpace); this.decodeImage(null, decoderOptions);
} }
decodeImage(bytes, ignoreColorSpace) { decodeImage(bytes, decoderOptions) {
if (this.eof) { if (this.eof) {
return this.buffer; return this.buffer;
} }
bytes ||= this.bytes; bytes ||= this.bytes;
this.buffer = JpxImage.decode(bytes, ignoreColorSpace); this.buffer = JpxImage.decode(bytes, decoderOptions);
this.bufferLength = this.buffer.length; this.bufferLength = this.buffer.length;
this.eof = true; this.eof = true;

View File

@ -0,0 +1 @@
https://github.com/user-attachments/files/15511847/out2.pdf

View File

@ -0,0 +1 @@
https://github.com/mozilla/pdf.js/files/3806127/2018.Wrapped.pdf

View File

@ -0,0 +1 @@
https://github.com/mozilla/pdf.js/files/12244643/tt1.pdf

View File

@ -10031,5 +10031,30 @@
"rounds": 1, "rounds": 1,
"link": true, "link": true,
"type": "eq" "type": "eq"
},
{
"id": "issue16782",
"file": "pdfs/issue16782.pdf",
"md5": "b203313ae62c0dac2585fae1c5fa6f8b",
"rounds": 1,
"link": true,
"type": "eq"
},
{
"id": "issue11306",
"file": "pdfs/issue11306.pdf",
"md5": "dfc626ee307f9488d74341d21641db9e",
"rounds": 1,
"link": true,
"lastPage": 1,
"type": "eq"
},
{
"id": "isssue18194",
"file": "pdfs/isssue18194.pdf",
"md5": "dbc12624353401deac99d94a2962df4d",
"rounds": 1,
"link": true,
"type": "eq"
} }
] ]