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

Rescale the image data when they're really too large #19095

Merged
merged 1 commit into from
Nov 24, 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
4 changes: 4 additions & 0 deletions src/core/core_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import { Dict, isName, Ref, RefSet } from "./primitives.js";
import { BaseStream } from "./base_stream.js";

const PDF_VERSION_REGEXP = /^[1-9]\.\d$/;
const MAX_INT_32 = 2 ** 31 - 1;
const MIN_INT_32 = -(2 ** 31);

function getLookupTableFactory(initializer) {
let lookup;
Expand Down Expand Up @@ -713,6 +715,8 @@ export {
lookupMatrix,
lookupNormalRect,
lookupRect,
MAX_INT_32,
MIN_INT_32,
MissingDataException,
numberToString,
ParserEOFException,
Expand Down
101 changes: 99 additions & 2 deletions src/core/image_resizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
*/

import { FeatureTest, ImageKind, shadow, warn } from "../shared/util.js";
import { convertToRGBA } from "../shared/image_utils.js";
import { MAX_INT_32 } from "./core_utils.js";

const MIN_IMAGE_DIM = 2048;

Expand Down Expand Up @@ -172,6 +174,18 @@ class ImageResizer {
}

async _createImage() {
const { _imgData: imgData } = this;
const { width, height } = imgData;

if (width * height * 4 > MAX_INT_32) {
// The resulting RGBA image is too large.
// We just rescale the data.
const result = this.#rescaleImageData();
if (result) {
return result;
}
}

const data = this._encodeBMP();
let decoder, imagePromise;

Expand Down Expand Up @@ -206,8 +220,6 @@ class ImageResizer {
}

const { MAX_AREA, MAX_DIM } = ImageResizer;
const { _imgData: imgData } = this;
const { width, height } = imgData;
const minFactor = Math.max(
width / MAX_DIM,
height / MAX_DIM,
Expand Down Expand Up @@ -268,6 +280,91 @@ class ImageResizer {
return imgData;
}

#rescaleImageData() {
const { _imgData: imgData } = this;
const { data, width, height, kind } = imgData;
const rgbaSize = width * height * 4;
// K is such as width * height * 4 / 2 ** K <= 2 ** 31 - 1
const K = Math.ceil(Math.log2(rgbaSize / MAX_INT_32));
const newWidth = width >> K;
const newHeight = height >> K;
let rgbaData;
let maxHeight = height;

// We try to allocate the buffer with the maximum size but it can fail.
try {
rgbaData = new Uint8Array(rgbaSize);
} catch {
// n is such as 2 ** n - 1 > width * height * 4
let n = Math.floor(Math.log2(rgbaSize + 1));

while (true) {
try {
rgbaData = new Uint8Array(2 ** n - 1);
Fixed Show fixed Hide fixed
break;
} catch {
n -= 1;
}
}

maxHeight = Math.floor((2 ** n - 1) / (width * 4));
const newSize = width * maxHeight * 4;
if (newSize < rgbaData.length) {
rgbaData = new Uint8Array(newSize);
}
}

const src32 = new Uint32Array(rgbaData.buffer);
const dest32 = new Uint32Array(newWidth * newHeight);

let srcPos = 0;
let newIndex = 0;
const step = Math.ceil(height / maxHeight);
const remainder = height % maxHeight === 0 ? height : height % maxHeight;
for (let k = 0; k < step; k++) {
const h = k < step - 1 ? maxHeight : remainder;
({ srcPos } = convertToRGBA({
kind,
src: data,
dest: src32,
width,
height: h,
inverseDecode: this._isMask,
srcPos,
}));

for (let i = 0, ii = h >> K; i < ii; i++) {
const buf = src32.subarray((i << K) * width);
for (let j = 0; j < newWidth; j++) {
dest32[newIndex++] = buf[j << K];
}
}
}

if (ImageResizer.needsToBeResized(newWidth, newHeight)) {
imgData.data = dest32;
imgData.width = newWidth;
imgData.height = newHeight;
imgData.kind = ImageKind.RGBA_32BPP;

return null;
}

const canvas = new OffscreenCanvas(newWidth, newHeight);
const ctx = canvas.getContext("2d", { willReadFrequently: true });
ctx.putImageData(
new ImageData(new Uint8ClampedArray(dest32.buffer), newWidth, newHeight),
0,
0
);
imgData.data = null;
imgData.bitmap = canvas.transferToImageBitmap();
imgData.width = newWidth;
imgData.height = newHeight;

return imgData;
}

_encodeBMP() {
const { width, height, kind } = this._imgData;
let data = this._imgData.data;
Expand Down
12 changes: 8 additions & 4 deletions src/core/jbig2.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@
*/

import { BaseException, shadow } from "../shared/util.js";
import { log2, readInt8, readUint16, readUint32 } from "./core_utils.js";
import {
log2,
MAX_INT_32,
MIN_INT_32,
readInt8,
readUint16,
readUint32,
} from "./core_utils.js";
import { ArithmeticDecoder } from "./arithmetic_decoder.js";
import { CCITTFaxDecoder } from "./ccitt.js";

Expand Down Expand Up @@ -52,9 +59,6 @@ class DecodingContext {
}
}

const MAX_INT_32 = 2 ** 31 - 1;
const MIN_INT_32 = -(2 ** 31);

// Annex A. Arithmetic Integer Decoding Procedure
// A.2 Procedure for decoding values
function decodeInteger(contextCache, procedure, decoder) {
Expand Down
9 changes: 5 additions & 4 deletions src/shared/image_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ function convertRGBToRGBA({
height,
}) {
let i = 0;
const len32 = src.length >> 2;
const len = width * height * 3;
const len32 = len >> 2;
const src32 = new Uint32Array(src.buffer, srcPos, len32);

if (FeatureTest.isLittleEndian) {
Expand All @@ -94,7 +95,7 @@ function convertRGBToRGBA({
dest[destPos + 3] = (s3 >>> 8) | 0xff000000;
}

for (let j = i * 4, jj = src.length; j < jj; j += 3) {
for (let j = i * 4, jj = srcPos + len; j < jj; j += 3) {
dest[destPos++] =
src[j] | (src[j + 1] << 8) | (src[j + 2] << 16) | 0xff000000;
}
Expand All @@ -110,13 +111,13 @@ function convertRGBToRGBA({
dest[destPos + 3] = (s3 << 8) | 0xff;
}

for (let j = i * 4, jj = src.length; j < jj; j += 3) {
for (let j = i * 4, jj = srcPos + len; j < jj; j += 3) {
dest[destPos++] =
(src[j] << 24) | (src[j + 1] << 16) | (src[j + 2] << 8) | 0xff;
}
}

return { srcPos, destPos };
return { srcPos: srcPos + len, destPos };
}

function grayToRGBA(src, dest) {
Expand Down
1 change: 1 addition & 0 deletions test/pdfs/issue17190.pdf.link
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://github.com/mozilla/pdf.js/files/13180762/org_AVA89V01U0.Black.pdf
1 change: 1 addition & 0 deletions test/pdfs/issue17190_1.pdf.link
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://github.com/mozilla/pdf.js/files/13670664/abc.pdf
18 changes: 18 additions & 0 deletions test/test_manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10775,5 +10775,23 @@
"forms": true,
"talos": false,
"type": "eq"
},
{
"id": "issue17190",
"file": "pdfs/issue17190.pdf",
"md5": "06e3ce6492dc0b5815a63965b5d7144c",
"rounds": 1,
"talos": false,
"type": "eq",
"link": true
},
{
"id": "issue17190_1",
"file": "pdfs/issue17190_1.pdf",
"md5": "63bbc71a6c2cdec11bb20c5744232c47",
"rounds": 1,
"talos": false,
"type": "eq",
"link": true
}
]