Skip to content

Commit

Permalink
Merge pull request #18204 from calixteman/issue16782
Browse files Browse the repository at this point in the history
Fix decoding of JPX images having an alpha channel
  • Loading branch information
calixteman authored Jun 3, 2024
2 parents 5c51d56 + 196affd commit 21e6227
Show file tree
Hide file tree
Showing 12 changed files with 126 additions and 39 deletions.
2 changes: 1 addition & 1 deletion external/openjpeg/openjpeg.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/core/base_stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
22 changes: 22 additions & 0 deletions src/core/colorspace.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
},
Expand Down Expand Up @@ -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])`.
*/
Expand Down
12 changes: 6 additions & 6 deletions src/core/decode_stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -82,15 +82,15 @@ 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) {
end = bufEnd;
}
} else {
while (!this.eof) {
this.readBlock(ignoreColorSpace);
this.readBlock(decoderOptions);
}
end = this.bufferLength;
}
Expand All @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion src/core/flate_stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
82 changes: 59 additions & 23 deletions src/core/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
FeatureTest,
FormatError,
ImageKind,
info,
warn,
} from "../shared/util.js";
import {
Expand Down Expand Up @@ -104,7 +103,6 @@ class PDFImage {
localColorSpaceCache,
}) {
this.image = image;
let jpxDecode = false;
const dict = image.dict;

const filter = dict.get("F", "Filter");
Expand All @@ -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;
Expand Down Expand Up @@ -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({
Expand All @@ -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");
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions src/core/jpx.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
8 changes: 4 additions & 4 deletions src/core/jpx_stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
1 change: 1 addition & 0 deletions test/pdfs/isssue18194.pdf.link
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://github.com/user-attachments/files/15511847/out2.pdf
1 change: 1 addition & 0 deletions test/pdfs/issue11306.pdf.link
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://github.com/mozilla/pdf.js/files/3806127/2018.Wrapped.pdf
1 change: 1 addition & 0 deletions test/pdfs/issue16782.pdf.link
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://github.com/mozilla/pdf.js/files/12244643/tt1.pdf
25 changes: 25 additions & 0 deletions test/test_manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]

0 comments on commit 21e6227

Please sign in to comment.