Use ImageDecoder in order to decode jpeg images (bug 1901223)
This commit is contained in:
parent
d37e4b08e4
commit
b6c4f0b69e
@ -68,6 +68,10 @@ class BaseStream {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getTransferableImage() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
peekByte() {
|
peekByte() {
|
||||||
const peekedByte = this.getByte();
|
const peekedByte = this.getByte();
|
||||||
if (peekedByte !== -1) {
|
if (peekedByte !== -1) {
|
||||||
|
|||||||
@ -752,6 +752,10 @@ class PDFImage {
|
|||||||
drawWidth === originalWidth &&
|
drawWidth === originalWidth &&
|
||||||
drawHeight === originalHeight
|
drawHeight === originalHeight
|
||||||
) {
|
) {
|
||||||
|
const image = await this.#getImage(originalWidth, originalHeight);
|
||||||
|
if (image) {
|
||||||
|
return image;
|
||||||
|
}
|
||||||
const data = await this.getImageBytes(originalHeight * rowBytes, {});
|
const data = await this.getImageBytes(originalHeight * rowBytes, {});
|
||||||
if (isOffscreenCanvasSupported) {
|
if (isOffscreenCanvasSupported) {
|
||||||
if (mustBeResized) {
|
if (mustBeResized) {
|
||||||
@ -810,6 +814,10 @@ class PDFImage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isHandled) {
|
if (isHandled) {
|
||||||
|
const image = await this.#getImage(drawWidth, drawHeight);
|
||||||
|
if (image) {
|
||||||
|
return image;
|
||||||
|
}
|
||||||
const rgba = await this.getImageBytes(imageLength, {
|
const rgba = await this.getImageBytes(imageLength, {
|
||||||
drawWidth,
|
drawWidth,
|
||||||
drawHeight,
|
drawHeight,
|
||||||
@ -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(
|
async getImageBytes(
|
||||||
length,
|
length,
|
||||||
{
|
{
|
||||||
|
|||||||
@ -13,10 +13,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { shadow, warn } from "../shared/util.js";
|
||||||
import { DecodeStream } from "./decode_stream.js";
|
import { DecodeStream } from "./decode_stream.js";
|
||||||
import { Dict } from "./primitives.js";
|
import { Dict } from "./primitives.js";
|
||||||
import { JpegImage } from "./jpg.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
|
* For JPEG's we use a library to decode these images and the stream behaves
|
||||||
@ -32,6 +32,18 @@ class JpegStream extends DecodeStream {
|
|||||||
this.params = params;
|
this.params = params;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get canUseImageDecoder() {
|
||||||
|
return shadow(
|
||||||
|
this,
|
||||||
|
"canUseImageDecoder",
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
typeof ImageDecoder === "undefined"
|
||||||
|
? Promise.resolve(false)
|
||||||
|
: // eslint-disable-next-line no-undef
|
||||||
|
ImageDecoder.isTypeSupported("image/jpeg")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
get bytes() {
|
get bytes() {
|
||||||
// If `this.maybeLength` is null, we'll get the entire stream.
|
// If `this.maybeLength` is null, we'll get the entire stream.
|
||||||
return shadow(this, "bytes", this.stream.getBytes(this.maybeLength));
|
return shadow(this, "bytes", this.stream.getBytes(this.maybeLength));
|
||||||
@ -46,22 +58,7 @@ class JpegStream extends DecodeStream {
|
|||||||
this.decodeImage();
|
this.decodeImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
decodeImage(bytes) {
|
get jpegOptions() {
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const jpegOptions = {
|
const jpegOptions = {
|
||||||
decodeTransform: undefined,
|
decodeTransform: undefined,
|
||||||
colorTransform: undefined,
|
colorTransform: undefined,
|
||||||
@ -93,8 +90,34 @@ class JpegStream extends DecodeStream {
|
|||||||
jpegOptions.colorTransform = colorTransform;
|
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);
|
jpegImage.parse(bytes);
|
||||||
const data = jpegImage.getData({
|
const data = jpegImage.getData({
|
||||||
width: this.drawWidth,
|
width: this.drawWidth,
|
||||||
@ -113,6 +136,48 @@ class JpegStream extends DecodeStream {
|
|||||||
get canAsyncDecodeImageFromBuffer() {
|
get canAsyncDecodeImageFromBuffer() {
|
||||||
return this.stream.isAsync;
|
return this.stream.isAsync;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getTransferableImage() {
|
||||||
|
if (!(await JpegStream.canUseImageDecoder)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const jpegOptions = this.jpegOptions;
|
||||||
|
if (jpegOptions.decodeTransform) {
|
||||||
|
// TODO: We could decode the image thanks to ImageDecoder and then
|
||||||
|
// get the pixels with copyTo and apply the decodeTransform.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let decoder;
|
||||||
|
try {
|
||||||
|
// TODO: If the stream is Flate & DCT we could try to just pipe the
|
||||||
|
// the DecompressionStream into the ImageDecoder: it'll avoid the
|
||||||
|
// intermediate ArrayBuffer.
|
||||||
|
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
|
||||||
|
decoder = new ImageDecoder({
|
||||||
|
data,
|
||||||
|
type: "image/jpeg",
|
||||||
|
preferAnimation: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (await decoder.decode()).image;
|
||||||
|
} catch (reason) {
|
||||||
|
warn(`getTransferableImage - failed: "${reason}".`);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
decoder?.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { JpegStream };
|
export { JpegStream };
|
||||||
|
|||||||
111
src/core/jpg.js
111
src/core/jpg.js
@ -744,33 +744,7 @@ function findNextFileMarker(data, currentPos, startPos = currentPos) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class JpegImage {
|
function prepareComponents(frame) {
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepareComponents(frame) {
|
|
||||||
const mcusPerLine = Math.ceil(frame.samplesPerLine / 8 / frame.maxH);
|
const mcusPerLine = Math.ceil(frame.samplesPerLine / 8 / frame.maxH);
|
||||||
const mcusPerColumn = Math.ceil(frame.scanLines / 8 / frame.maxV);
|
const mcusPerColumn = Math.ceil(frame.scanLines / 8 / frame.maxV);
|
||||||
for (const component of frame.components) {
|
for (const component of frame.components) {
|
||||||
@ -791,8 +765,88 @@ class JpegImage {
|
|||||||
}
|
}
|
||||||
frame.mcusPerLine = mcusPerLine;
|
frame.mcusPerLine = mcusPerLine;
|
||||||
frame.mcusPerColumn = mcusPerColumn;
|
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 { appData: array, newOffset: offset };
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
offset = skipData(data, offset);
|
||||||
|
fileMarker = readUint16(data, offset);
|
||||||
|
offset += 2;
|
||||||
|
}
|
||||||
|
if (numComponents === 4) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (numComponents === 3 && colorTransform === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(data, { dnlScanLines = null } = {}) {
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
let jfif = null;
|
let jfif = null;
|
||||||
let adobe = null;
|
let adobe = null;
|
||||||
@ -830,7 +884,8 @@ class JpegImage {
|
|||||||
case 0xffee: // APP14
|
case 0xffee: // APP14
|
||||||
case 0xffef: // APP15
|
case 0xffef: // APP15
|
||||||
case 0xfffe: // COM (Comment)
|
case 0xfffe: // COM (Comment)
|
||||||
const appData = readDataBlock();
|
const { appData, newOffset } = readDataBlock(data, offset);
|
||||||
|
offset = newOffset;
|
||||||
|
|
||||||
if (fileMarker === 0xffe0) {
|
if (fileMarker === 0xffe0) {
|
||||||
// 'JFIF\x00'
|
// 'JFIF\x00'
|
||||||
|
|||||||
@ -1059,8 +1059,10 @@ class CanvasGraphics {
|
|||||||
// Vertical or horizontal scaling shall not be more than 2 to not lose the
|
// Vertical or horizontal scaling shall not be more than 2 to not lose the
|
||||||
// pixels during drawImage operation, painting on the temporary canvas(es)
|
// pixels during drawImage operation, painting on the temporary canvas(es)
|
||||||
// that are twice smaller in size.
|
// 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(
|
let widthScale = Math.max(
|
||||||
Math.hypot(inverseTransform[0], inverseTransform[1]),
|
Math.hypot(inverseTransform[0], inverseTransform[1]),
|
||||||
1
|
1
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user