Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decompress when it's possible images in using DecompressionStream #18167

Merged
merged 1 commit into from
Jun 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/core/base_stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,27 @@ class BaseStream {
unreachable("Abstract method `getBytes` called");
}

/**
* NOTE: This method can only be used to get image-data that is guaranteed
* to be fully loaded, since otherwise intermittent errors may occur;
* note the `ObjectLoader` class.
*/
async getImageData(length, ignoreColorSpace) {
calixteman marked this conversation as resolved.
Show resolved Hide resolved
return this.getBytes(length, ignoreColorSpace);
}

async asyncGetBytes() {
unreachable("Abstract method `asyncGetBytes` called");
}

get isAsync() {
return false;
}

get canAsyncDecodeImageFromBuffer() {
return false;
}

peekByte() {
const peekedByte = this.getByte();
if (peekedByte !== -1) {
Expand Down
8 changes: 8 additions & 0 deletions src/core/decode_stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ class DecodeStream extends BaseStream {
return this.buffer.subarray(pos, end);
}

async getImageData(length, ignoreColorSpace = false) {
if (!this.canAsyncDecodeImageFromBuffer) {
return this.getBytes(length, ignoreColorSpace);
}
const data = await this.stream.asyncGetBytes();
return this.decodeImage(data, ignoreColorSpace);
}

reset() {
this.pos = 0;
}
Expand Down
52 changes: 52 additions & 0 deletions src/core/flate_stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import { FormatError, info } from "../shared/util.js";
import { DecodeStream } from "./decode_stream.js";
import { Stream } from "./stream.js";

const codeLenCodeMap = new Int32Array([
16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15,
Expand Down Expand Up @@ -148,6 +149,57 @@ class FlateStream extends DecodeStream {
this.codeBuf = 0;
}

async getImageData(length, _ignoreColorSpace) {
const data = await this.asyncGetBytes();
return data?.subarray(0, length) || this.getBytes(length);
}

async asyncGetBytes() {
this.str.reset();
const bytes = this.str.getBytes();

try {
const { readable, writable } = new DecompressionStream("deflate");
const writer = writable.getWriter();
writer.write(bytes);
writer.close();

const chunks = [];
let totalLength = 0;

for await (const chunk of readable) {
chunks.push(chunk);
totalLength += chunk.byteLength;
}
const data = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
data.set(chunk, offset);
offset += chunk.byteLength;
}

return data;
} catch {
// DecompressionStream failed (for example because there are some extra
// bytes after the end of the compressed data), so we fallback to our
// decoder.
// We already get the bytes from the underlying stream, so we just reuse
// them to avoid get them again.
this.str = new Stream(
bytes,
2 /* = header size (see ctor) */,
bytes.length,
this.str.dict
);
this.reset();
return null;
}
}

get isAsync() {
return true;
}

getBits(bits) {
const str = this.str;
let codeSize = this.codeSize;
Expand Down
29 changes: 17 additions & 12 deletions src/core/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,7 @@ class PDFImage {
return output;
}

fillOpacity(rgbaBuf, width, height, actualHeight, image) {
async fillOpacity(rgbaBuf, width, height, actualHeight, image) {
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
assert(
rgbaBuf instanceof Uint8ClampedArray,
Expand All @@ -580,7 +580,7 @@ class PDFImage {
sw = smask.width;
sh = smask.height;
alphaBuf = new Uint8ClampedArray(sw * sh);
smask.fillGrayBuffer(alphaBuf);
await smask.fillGrayBuffer(alphaBuf);
if (sw !== width || sh !== height) {
alphaBuf = resizeImageMask(alphaBuf, smask.bpc, sw, sh, width, height);
}
Expand All @@ -590,7 +590,7 @@ class PDFImage {
sh = mask.height;
alphaBuf = new Uint8ClampedArray(sw * sh);
mask.numComps = 1;
mask.fillGrayBuffer(alphaBuf);
await mask.fillGrayBuffer(alphaBuf);

// Need to invert values in rgbaBuf
for (i = 0, ii = sw * sh; i < ii; ++i) {
Expand Down Expand Up @@ -716,7 +716,7 @@ class PDFImage {
drawWidth === originalWidth &&
drawHeight === originalHeight
) {
const data = this.getImageBytes(originalHeight * rowBytes, {});
const data = await this.getImageBytes(originalHeight * rowBytes, {});
if (isOffscreenCanvasSupported) {
if (mustBeResized) {
return ImageResizer.createImage(
Expand Down Expand Up @@ -774,7 +774,7 @@ class PDFImage {
}

if (isHandled) {
const rgba = this.getImageBytes(imageLength, {
const rgba = await this.getImageBytes(imageLength, {
drawWidth,
drawHeight,
forceRGBA: true,
Expand All @@ -794,7 +794,7 @@ class PDFImage {
case "DeviceRGB":
case "DeviceCMYK":
imgData.kind = ImageKind.RGB_24BPP;
imgData.data = this.getImageBytes(imageLength, {
imgData.data = await this.getImageBytes(imageLength, {
drawWidth,
drawHeight,
forceRGB: true,
Expand All @@ -809,7 +809,7 @@ class PDFImage {
}
}

const imgArray = this.getImageBytes(originalHeight * rowBytes, {
const imgArray = await this.getImageBytes(originalHeight * rowBytes, {
internal: true,
});
// imgArray can be incomplete (e.g. after CCITT fax encoding).
Expand Down Expand Up @@ -852,7 +852,7 @@ class PDFImage {
maybeUndoPreblend = true;

// Color key masking (opacity) must be performed before decoding.
this.fillOpacity(data, drawWidth, drawHeight, actualHeight, comps);
await this.fillOpacity(data, drawWidth, drawHeight, actualHeight, comps);
}

if (this.needsDecode) {
Expand Down Expand Up @@ -893,7 +893,7 @@ class PDFImage {
return imgData;
}

fillGrayBuffer(buffer) {
async fillGrayBuffer(buffer) {
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
assert(
buffer instanceof Uint8ClampedArray,
Expand All @@ -913,7 +913,9 @@ class PDFImage {

// rows start at byte boundary
const rowBytes = (width * numComps * bpc + 7) >> 3;
const imgArray = this.getImageBytes(height * rowBytes, { internal: true });
const imgArray = await this.getImageBytes(height * rowBytes, {
internal: true,
});

const comps = this.getComponents(imgArray);
let i, length;
Expand Down Expand Up @@ -975,7 +977,7 @@ class PDFImage {
};
}

getImageBytes(
async getImageBytes(
length,
{
drawWidth,
Expand All @@ -990,7 +992,10 @@ class PDFImage {
this.image.drawHeight = drawHeight || this.height;
this.image.forceRGBA = !!forceRGBA;
this.image.forceRGB = !!forceRGB;
const imageBytes = this.image.getBytes(length, this.ignoreColorSpace);
const imageBytes = await this.image.getImageData(
length,
this.ignoreColorSpace
);

// If imageBytes came from a DecodeStream, we're safe to transfer it
// (and thus detach its underlying buffer) because it will constitute
Expand Down
15 changes: 13 additions & 2 deletions src/core/jbig2_stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,14 @@ class Jbig2Stream extends DecodeStream {
}

readBlock() {
this.decodeImage();
}

decodeImage(bytes) {
if (this.eof) {
return;
return this.buffer;
}
bytes ||= this.bytes;
const jbig2Image = new Jbig2Image();

const chunks = [];
Expand All @@ -57,7 +62,7 @@ class Jbig2Stream extends DecodeStream {
chunks.push({ data: globals, start: 0, end: globals.length });
}
}
chunks.push({ data: this.bytes, start: 0, end: this.bytes.length });
chunks.push({ data: bytes, start: 0, end: bytes.length });
const data = jbig2Image.parseChunks(chunks);
const dataLength = data.length;

Expand All @@ -68,6 +73,12 @@ class Jbig2Stream extends DecodeStream {
this.buffer = data;
this.bufferLength = dataLength;
this.eof = true;

return this.buffer;
}

get canAsyncDecodeImageFromBuffer() {
return this.stream.isAsync;
}
}

Expand Down
36 changes: 24 additions & 12 deletions src/core/jpeg_stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,6 @@ import { shadow } from "../shared/util.js";
*/
class JpegStream extends DecodeStream {
constructor(stream, maybeLength, params) {
// Some images may contain 'junk' before the SOI (start-of-image) marker.
// Note: this seems to mainly affect inline images.
let ch;
while ((ch = stream.getByte()) !== -1) {
// Find the first byte of the SOI marker (0xFFD8).
if (ch === 0xff) {
stream.skip(-1); // Reset the stream position to the SOI.
break;
}
}
super(maybeLength);

this.stream = stream;
Expand All @@ -53,8 +43,24 @@ class JpegStream extends DecodeStream {
}

readBlock() {
this.decodeImage();
}

decodeImage(bytes) {
if (this.eof) {
return;
return this.buffer;
}
bytes ||= this.bytes;

// Some images may contain 'junk' before the SOI (start-of-image) marker.
// Note: this seems to mainly affect inline images.
for (let i = 0, ii = bytes.length - 1; i < ii; i++) {
if (bytes[i] === 0xff && bytes[i + 1] === 0xd8) {
if (i > 0) {
bytes = bytes.subarray(i);
}
break;
}
}
const jpegOptions = {
decodeTransform: undefined,
Expand Down Expand Up @@ -89,7 +95,7 @@ class JpegStream extends DecodeStream {
}
const jpegImage = new JpegImage(jpegOptions);

jpegImage.parse(this.bytes);
jpegImage.parse(bytes);
const data = jpegImage.getData({
width: this.drawWidth,
height: this.drawHeight,
Expand All @@ -100,6 +106,12 @@ class JpegStream extends DecodeStream {
this.buffer = data;
this.bufferLength = data.length;
this.eof = true;

return this.buffer;
}

get canAsyncDecodeImageFromBuffer() {
return this.stream.isAsync;
}
}

Expand Down
16 changes: 13 additions & 3 deletions src/core/jpx_stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,23 @@ class JpxStream extends DecodeStream {
}

readBlock(ignoreColorSpace) {
this.decodeImage(null, ignoreColorSpace);
}

decodeImage(bytes, ignoreColorSpace) {
if (this.eof) {
return;
return this.buffer;
}

this.buffer = JpxImage.decode(this.bytes, ignoreColorSpace);
bytes ||= this.bytes;
this.buffer = JpxImage.decode(bytes, ignoreColorSpace);
this.bufferLength = this.buffer.length;
this.eof = true;

return this.buffer;
}

get canAsyncDecodeImageFromBuffer() {
return this.stream.isAsync;
}
}

Expand Down