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

Use ImageDecoder in order to decode jpeg images (bug 1901223) #18910

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
4 changes: 4 additions & 0 deletions src/core/base_stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ class BaseStream {
return false;
}

async getTransferableImage() {
return null;
}

peekByte() {
const peekedByte = this.getByte();
if (peekedByte !== -1) {
Expand Down
22 changes: 22 additions & 0 deletions src/core/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,10 @@ class PDFImage {
drawWidth === originalWidth &&
drawHeight === originalHeight
) {
const image = await this.getImage(originalWidth, originalHeight);
if (image) {
return image;
}
const data = await this.getImageBytes(originalHeight * rowBytes, {});
if (isOffscreenCanvasSupported) {
if (mustBeResized) {
Expand Down Expand Up @@ -810,6 +814,10 @@ class PDFImage {
}

if (isHandled) {
const image = await this.getImage(drawWidth, drawHeight);
if (image) {
return image;
}
const rgba = await this.getImageBytes(imageLength, {
drawWidth,
drawHeight,
Expand Down Expand Up @@ -1013,6 +1021,20 @@ class PDFImage {
};
}

async getImage(width, height) {
const bitmap = await this.image.getTransferableImage();
if (!bitmap) {
return null;
}
return {
data: null,
width,
height,
bitmap,
interpolate: this.interpolate,
};
}

async getImageBytes(
length,
{
Expand Down
84 changes: 66 additions & 18 deletions src/core/jpeg_stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
* limitations under the License.
*/

import { shadow, warn } from "../shared/util.js";
import { DecodeStream } from "./decode_stream.js";
import { Dict } from "./primitives.js";
import { JpegImage } from "./jpg.js";
import { shadow } from "../shared/util.js";

/**
* For JPEG's we use a library to decode these images and the stream behaves
Expand Down Expand Up @@ -46,22 +46,7 @@ class JpegStream extends DecodeStream {
this.decodeImage();
}

decodeImage(bytes) {
if (this.eof) {
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;
}
}
get jpegOptions() {
const jpegOptions = {
decodeTransform: undefined,
colorTransform: undefined,
Expand Down Expand Up @@ -93,8 +78,34 @@ class JpegStream extends DecodeStream {
jpegOptions.colorTransform = colorTransform;
}
}
const jpegImage = new JpegImage(jpegOptions);
return shadow(this, "jpegOptions", jpegOptions);
}

skipUselessBytes(data) {
// 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 = data.length - 1; i < ii; i++) {
if (data[i] === 0xff && data[i + 1] === 0xd8) {
if (i > 0) {
data = data.subarray(i);
}
break;
}
}
return data;
}

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

// TODO: if an image has a mask we need to combine the data.
// So ideally get a VideoFrame from getTransferableImage and then use
// copyTo.

const jpegImage = new JpegImage(this.jpegOptions);
jpegImage.parse(bytes);
const data = jpegImage.getData({
width: this.drawWidth,
Expand All @@ -113,6 +124,43 @@ class JpegStream extends DecodeStream {
get canAsyncDecodeImageFromBuffer() {
return this.stream.isAsync;
}

async getTransferableImage() {
// eslint-disable-next-line no-undef
if (typeof ImageDecoder === "undefined") {
return null;
}
const jpegOptions = this.jpegOptions;
if (jpegOptions.decodeTransform) {
return null;
}
try {
const bytes =
(this.canAsyncDecodeImageFromBuffer &&
(await this.stream.asyncGetBytes())) ||
this.bytes;
if (!bytes) {
return null;
}
const data = this.skipUselessBytes(bytes);
if (!JpegImage.canUseImageDecoder(data, jpegOptions.colorTransform)) {
return null;
}
// eslint-disable-next-line no-undef
const decoder = new ImageDecoder({
data: bytes,
type: "image/jpeg",
preferAnimation: false,
});

const { image } = await decoder.decode();
decoder.close();
return image;
} catch (reason) {
warn(`getTransferableImage - failed: "${reason}".`);
return null;
}
}
}

export { JpegStream };
137 changes: 97 additions & 40 deletions src/core/jpg.js
Original file line number Diff line number Diff line change
Expand Up @@ -744,55 +744,111 @@ function findNextFileMarker(data, currentPos, startPos = currentPos) {
};
}

function prepareComponents(frame) {
const mcusPerLine = Math.ceil(frame.samplesPerLine / 8 / frame.maxH);
const mcusPerColumn = Math.ceil(frame.scanLines / 8 / frame.maxV);
for (const component of frame.components) {
const blocksPerLine = Math.ceil(
(Math.ceil(frame.samplesPerLine / 8) * component.h) / frame.maxH
);
const blocksPerColumn = Math.ceil(
(Math.ceil(frame.scanLines / 8) * component.v) / frame.maxV
);
const blocksPerLineForMcu = mcusPerLine * component.h;
const blocksPerColumnForMcu = mcusPerColumn * component.v;

const blocksBufferSize =
64 * blocksPerColumnForMcu * (blocksPerLineForMcu + 1);
component.blockData = new Int16Array(blocksBufferSize);
component.blocksPerLine = blocksPerLine;
component.blocksPerColumn = blocksPerColumn;
}
frame.mcusPerLine = mcusPerLine;
frame.mcusPerColumn = mcusPerColumn;
}

function readDataBlock(data, offset) {
const length = readUint16(data, offset);
offset += 2;
let endOffset = offset + length - 2;

const fileMarker = findNextFileMarker(data, endOffset, offset);
if (fileMarker?.invalid) {
warn(
"readDataBlock - incorrect length, current marker is: " +
fileMarker.invalid
);
endOffset = fileMarker.offset;
}

const array = data.subarray(offset, endOffset);
offset += array.length;
return { newOffset: offset, appData: array };
}

function skipData(data, offset) {
const length = readUint16(data, offset);
offset += 2;
const endOffset = offset + length - 2;

const fileMarker = findNextFileMarker(data, endOffset, offset);
if (fileMarker?.invalid) {
return fileMarker.offset;
}
return endOffset;
}

class JpegImage {
constructor({ decodeTransform = null, colorTransform = -1 } = {}) {
this._decodeTransform = decodeTransform;
this._colorTransform = colorTransform;
}

parse(data, { dnlScanLines = null } = {}) {
function readDataBlock() {
const length = readUint16(data, offset);
offset += 2;
let endOffset = offset + length - 2;

const fileMarker = findNextFileMarker(data, endOffset, offset);
if (fileMarker?.invalid) {
warn(
"readDataBlock - incorrect length, current marker is: " +
fileMarker.invalid
);
endOffset = fileMarker.offset;
}

const array = data.subarray(offset, endOffset);
offset += array.length;
return array;
static canUseImageDecoder(data, colorTransform = -1) {
let offset = 0;
let numComponents = null;
let fileMarker = readUint16(data, offset);
offset += 2;
if (fileMarker !== /* SOI (Start of Image) = */ 0xffd8) {
throw new JpegError("SOI not found");
}

function prepareComponents(frame) {
const mcusPerLine = Math.ceil(frame.samplesPerLine / 8 / frame.maxH);
const mcusPerColumn = Math.ceil(frame.scanLines / 8 / frame.maxV);
for (const component of frame.components) {
const blocksPerLine = Math.ceil(
(Math.ceil(frame.samplesPerLine / 8) * component.h) / frame.maxH
);
const blocksPerColumn = Math.ceil(
(Math.ceil(frame.scanLines / 8) * component.v) / frame.maxV
);
const blocksPerLineForMcu = mcusPerLine * component.h;
const blocksPerColumnForMcu = mcusPerColumn * component.v;

const blocksBufferSize =
64 * blocksPerColumnForMcu * (blocksPerLineForMcu + 1);
component.blockData = new Int16Array(blocksBufferSize);
component.blocksPerLine = blocksPerLine;
component.blocksPerColumn = blocksPerColumn;
fileMarker = readUint16(data, offset);
offset += 2;
markerLoop: while (fileMarker !== /* EOI (End of Image) = */ 0xffd9) {
switch (fileMarker) {
case 0xffc0: // SOF0 (Start of Frame, Baseline DCT)
case 0xffc1: // SOF1 (Start of Frame, Extended DCT)
case 0xffc2: // SOF2 (Start of Frame, Progressive DCT)
// Skip marker length.
// Skip precision.
// Skip scanLines.
// Skip samplesPerLine.
numComponents = data[offset + (2 + 1 + 2 + 2)];
break markerLoop;
case 0xffff: // Fill bytes
if (data[offset] !== 0xff) {
// Avoid skipping a valid marker.
offset--;
}
break;
}
frame.mcusPerLine = mcusPerLine;
frame.mcusPerColumn = mcusPerColumn;
offset = skipData(data, offset);
fileMarker = readUint16(data, offset);
offset += 2;
}
if (numComponents === 4) {
return false;
}
if (numComponents !== 3) {
return true;
}
if (colorTransform === 0) {
return false;
}
return true;
}

parse(data, { dnlScanLines = null } = {}) {
let offset = 0;
let jfif = null;
let adobe = null;
Expand Down Expand Up @@ -830,7 +886,8 @@ class JpegImage {
case 0xffee: // APP14
case 0xffef: // APP15
case 0xfffe: // COM (Comment)
const appData = readDataBlock();
const { appData, newOffset } = readDataBlock(data, offset);
offset = newOffset;

if (fileMarker === 0xffe0) {
// 'JFIF\x00'
Expand Down
6 changes: 4 additions & 2 deletions src/display/canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -1059,8 +1059,10 @@ class CanvasGraphics {
// Vertical or horizontal scaling shall not be more than 2 to not lose the
// pixels during drawImage operation, painting on the temporary canvas(es)
// that are twice smaller in size.
const width = img.width;
const height = img.height;

// displayWidth and displayHeight are used for VideoFrame.
const width = img.width ?? img.displayWidth;
const height = img.height ?? img.displayHeight;
let widthScale = Math.max(
Math.hypot(inverseTransform[0], inverseTransform[1]),
1
Expand Down