1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-19 14:48:08 +02:00

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;
* note the `ObjectLoader` class.
*/
async getImageData(length, ignoreColorSpace) {
return this.getBytes(length, ignoreColorSpace);
async getImageData(length, decoderOptions) {
return this.getBytes(length, decoderOptions);
}
async asyncGetBytes() {

View file

@ -370,6 +370,8 @@ class ColorSpace {
case "RGB":
case "DeviceRGB":
return this.singletons.rgb;
case "DeviceRGBA":
return this.singletons.rgba;
case "CMYK":
case "DeviceCMYK":
return this.singletons.cmyk;
@ -511,6 +513,9 @@ class ColorSpace {
get rgb() {
return shadow(this, "rgb", new DeviceRgbCS());
},
get rgba() {
return shadow(this, "rgba", new DeviceRgbaCS());
},
get cmyk() {
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])`.
*/

View file

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

View file

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

View file

@ -18,7 +18,6 @@ import {
FeatureTest,
FormatError,
ImageKind,
info,
warn,
} from "../shared/util.js";
import {
@ -104,7 +103,6 @@ class PDFImage {
localColorSpaceCache,
}) {
this.image = image;
let jpxDecode = false;
const dict = image.dict;
const filter = dict.get("F", "Filter");
@ -126,7 +124,11 @@ class PDFImage {
bitsPerComponent: image.bitsPerComponent,
} = JpxImage.parseImageProperties(image.stream));
image.stream.reset();
jpxDecode = true;
this.jpxDecoderOptions = {
numComponents: 0,
isIndexedColormap: false,
smaskInData: dict.has("SMaskInData"),
};
break;
case "JBIG2Decode":
image.bitsPerComponent = 1;
@ -180,23 +182,31 @@ class PDFImage {
if (!this.imageMask) {
let colorSpace = dict.getRaw("CS") || dict.getRaw("ColorSpace");
if (!colorSpace) {
info("JPX images (which do not require color spaces)");
switch (image.numComps) {
case 1:
colorSpace = Name.get("DeviceGray");
break;
case 3:
colorSpace = Name.get("DeviceRGB");
break;
case 4:
colorSpace = Name.get("DeviceCMYK");
break;
default:
throw new Error(
`JPX images with ${image.numComps} color components not supported.`
);
const hasColorSpace = !!colorSpace;
if (!hasColorSpace) {
if (this.jpxDecoderOptions) {
colorSpace = Name.get("DeviceRGBA");
} else {
switch (image.numComps) {
case 1:
colorSpace = Name.get("DeviceGray");
break;
case 3:
colorSpace = Name.get("DeviceRGB");
break;
case 4:
colorSpace = Name.get("DeviceCMYK");
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({
@ -208,9 +218,13 @@ class PDFImage {
});
this.numComps = this.colorSpace.numComps;
// 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.ignoreColorSpace = jpxDecode && this.colorSpace.name === "Indexed";
if (this.jpxDecoderOptions) {
this.jpxDecoderOptions.numComponents = hasColorSpace ? this.numComp : 0;
// 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");
@ -691,6 +705,28 @@ class PDFImage {
isOffscreenCanvasSupported &&
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 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
@ -994,7 +1030,7 @@ class PDFImage {
this.image.forceRGB = !!forceRGB;
const imageBytes = await this.image.getImageData(
length,
this.ignoreColorSpace
this.jpxDecoderOptions
);
// If imageBytes came from a DecodeStream, we're safe to transfer it

View file

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

View file

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