From 0b5dfeb0cc3f8a0bc99a8958320c3d458c2bdca3 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Sat, 23 Nov 2024 20:35:30 +0100 Subject: [PATCH] Rescale the image data when they're really too large It fixes #17190. --- src/core/image_resizer.js | 96 ++++++++++++++++++++++++++++++++- src/shared/image_utils.js | 9 ++-- test/pdfs/issue17190.pdf.link | 1 + test/pdfs/issue17190_1.pdf.link | 1 + test/test_manifest.json | 18 +++++++ 5 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 test/pdfs/issue17190.pdf.link create mode 100644 test/pdfs/issue17190_1.pdf.link diff --git a/src/core/image_resizer.js b/src/core/image_resizer.js index 0f80c8f5981dac..886071a0917b67 100644 --- a/src/core/image_resizer.js +++ b/src/core/image_resizer.js @@ -14,6 +14,7 @@ */ import { FeatureTest, ImageKind, shadow, warn } from "../shared/util.js"; +import { convertToRGBA } from "../shared/image_utils.js"; const MIN_IMAGE_DIM = 2048; @@ -172,6 +173,18 @@ class ImageResizer { } async _createImage() { + const { _imgData: imgData } = this; + const { width, height } = imgData; + + if (width * height * 4 > 2 ** 31 - 1) { + // 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; @@ -206,8 +219,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, @@ -268,6 +279,87 @@ class ImageResizer { return imgData; } + _rescaleImageData() { + const { _imgData: imgData } = this; + const { data, width, height, kind } = imgData; + // K is such as width * height * 4 / 2 ** K <= 2 ** 31 - 1 + const K = Math.ceil(Math.log2((width * height * 4) / (2 ** 31 - 1))); + 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(width * height * 4); + } catch { + // n is such as 2 ** n - 1 > width * height * 4 + let n = Math.floor(Math.log2(width * height * 4 + 1)); + + while (true) { + try { + rgbaData = new Uint8Array(2 ** n - 1); + break; + } catch { + n -= 1; + } + } + + maxHeight = Math.floor((2 ** n - 1) / (width * 4)); + rgbaData = new Uint8Array(width * maxHeight * 4); + } + + 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; diff --git a/src/shared/image_utils.js b/src/shared/image_utils.js index e2e5c04e632031..b88342ccbfc318 100644 --- a/src/shared/image_utils.js +++ b/src/shared/image_utils.js @@ -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) { @@ -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; } @@ -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) { diff --git a/test/pdfs/issue17190.pdf.link b/test/pdfs/issue17190.pdf.link new file mode 100644 index 00000000000000..618accffb1023d --- /dev/null +++ b/test/pdfs/issue17190.pdf.link @@ -0,0 +1 @@ +https://github.com/mozilla/pdf.js/files/13180762/org_AVA89V01U0.Black.pdf diff --git a/test/pdfs/issue17190_1.pdf.link b/test/pdfs/issue17190_1.pdf.link new file mode 100644 index 00000000000000..506054eb5da31a --- /dev/null +++ b/test/pdfs/issue17190_1.pdf.link @@ -0,0 +1 @@ +https://github.com/mozilla/pdf.js/files/13670664/abc.pdf diff --git a/test/test_manifest.json b/test/test_manifest.json index 508e2af7d2fb19..0b6dcc6cdc004e 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -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 } ]