Merge pull request #18204 from calixteman/issue16782
Fix decoding of JPX images having an alpha channel
This commit is contained in:
commit
21e622769b
2
external/openjpeg/openjpeg.js
vendored
2
external/openjpeg/openjpeg.js
vendored
File diff suppressed because one or more lines are too long
@ -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() {
|
||||||
|
|||||||
@ -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])`.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
1
test/pdfs/isssue18194.pdf.link
Normal file
1
test/pdfs/isssue18194.pdf.link
Normal file
@ -0,0 +1 @@
|
|||||||
|
https://github.com/user-attachments/files/15511847/out2.pdf
|
||||||
1
test/pdfs/issue11306.pdf.link
Normal file
1
test/pdfs/issue11306.pdf.link
Normal file
@ -0,0 +1 @@
|
|||||||
|
https://github.com/mozilla/pdf.js/files/3806127/2018.Wrapped.pdf
|
||||||
1
test/pdfs/issue16782.pdf.link
Normal file
1
test/pdfs/issue16782.pdf.link
Normal file
@ -0,0 +1 @@
|
|||||||
|
https://github.com/mozilla/pdf.js/files/12244643/tt1.pdf
|
||||||
@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user