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:
commit
21e622769b
12 changed files with 126 additions and 39 deletions
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;
|
||||
* note the `ObjectLoader` class.
|
||||
*/
|
||||
async getImageData(length, ignoreColorSpace) {
|
||||
return this.getBytes(length, ignoreColorSpace);
|
||||
async getImageData(length, decoderOptions) {
|
||||
return this.getBytes(length, decoderOptions);
|
||||
}
|
||||
|
||||
async asyncGetBytes() {
|
||||
|
|
|
@ -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])`.
|
||||
*/
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
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,
|
||||
"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"
|
||||
}
|
||||
]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue