Skip to content
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
7 changes: 0 additions & 7 deletions l10n/en-US/viewer.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -758,13 +758,6 @@ pdfjs-views-manager-pages-status-undo-delete-label =
*[other] { $count } pages deleted
}

pdfjs-views-manager-pages-status-waiting-ready-label = Getting your file ready…
pdfjs-views-manager-pages-status-waiting-uploading-label = Uploading file…

pdfjs-views-manager-status-warning-cut-label = Couldn’t cut. Refresh page and try again.
pdfjs-views-manager-status-warning-copy-label = Couldn’t copy. Refresh page and try again.
pdfjs-views-manager-status-warning-delete-label = Couldn’t delete. Refresh page and try again.
pdfjs-views-manager-status-warning-save-label = Couldn’t save. Refresh page and try again.
pdfjs-views-manager-status-undo-button-label = Undo
pdfjs-views-manager-status-done-button-label = Done
pdfjs-views-manager-status-close-button =
Expand Down
6 changes: 5 additions & 1 deletion src/core/base_stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,11 @@ class BaseStream {
return false;
}

async getTransferableImage() {
/**
* @param {number} width - The width from the image dictionary.
* @param {number} height - The height from the image dictionary.
*/
async getTransferableImage(width, height) {
return null;
}

Expand Down
2 changes: 1 addition & 1 deletion src/core/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ class AESBaseCipher {
0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3,
]);

_mixCol = new Uint8Array(256).map((_, i) =>
_mixCol = Uint8Array.from({ length: 256 }, (_, i) =>
i < 128 ? i << 1 : (i << 1) ^ 0x1b
);

Expand Down
83 changes: 41 additions & 42 deletions src/core/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -837,30 +837,34 @@ class PDFImage {
!this.mask &&
!this.needsDecode
) {
let imageLength = originalHeight * rowBytes;
if (isOffscreenCanvasSupported && !mustBeResized) {
let isHandled = false;
switch (this.colorSpace.name) {
case "DeviceGray":
// Avoid truncating the image, since `JpegImage.getData`
// will expand the image data when `forceRGB === true`.
imageLength *= 4;
isHandled = true;
break;
case "DeviceRGB":
imageLength = (imageLength / 3) * 4;
isHandled = true;
break;
case "DeviceCMYK":
isHandled = true;
break;
}

if (isHandled) {
let isHandled = false;
switch (this.colorSpace.name) {
case "DeviceGray":
case "DeviceRGB":
case "DeviceCMYK":
isHandled = true;
break;
}
if (isHandled) {
if (isOffscreenCanvasSupported) {
// Try ImageDecoder before the pixel-buffer fallback.
const image = await this.#getImage(drawWidth, drawHeight);
if (image) {
return image;
}
}
let imageLength = originalHeight * rowBytes;

if (isOffscreenCanvasSupported && !mustBeResized) {
switch (this.colorSpace.name) {
case "DeviceGray":
// Account for the DeviceGray-to-RGBA expansion.
imageLength *= 4;
break;
case "DeviceRGB":
imageLength = (imageLength / 3) * 4;
break;
}
const rgba = await this.getImageBytes(imageLength, {
drawWidth,
drawHeight,
Expand All @@ -874,26 +878,20 @@ class PDFImage {
rgba
);
}
} else {
switch (this.colorSpace.name) {
case "DeviceGray":
imageLength *= 3;
/* falls through */
case "DeviceRGB":
case "DeviceCMYK":
imgData.kind = ImageKind.RGB_24BPP;
imgData.data = await this.getImageBytes(imageLength, {
drawWidth,
drawHeight,
forceRGB: true,
internal: mustBeResized,
});
if (mustBeResized) {
// The image is too big so we resize it.
return ImageResizer.createImage(imgData);
}
return imgData;
if (this.colorSpace.name === "DeviceGray") {
imageLength *= 3;
}
imgData.kind = ImageKind.RGB_24BPP;
imgData.data = await this.getImageBytes(imageLength, {
drawWidth,
drawHeight,
forceRGB: true,
internal: mustBeResized,
});
if (mustBeResized) {
return ImageResizer.createImage(imgData);
}
return imgData;
}
}
}
Expand Down Expand Up @@ -1148,14 +1146,15 @@ class PDFImage {
}

async #getImage(width, height) {
const bitmap = await this.image.getTransferableImage();
const bitmap = await this.image.getTransferableImage(width, height);
if (!bitmap) {
return null;
}
// ImageDecoder may ignore the requested dimensions.
return {
data: null,
width,
height,
width: bitmap.displayWidth ?? width,
height: bitmap.displayHeight ?? height,
bitmap,
interpolate: this.interpolate,
};
Expand Down
32 changes: 20 additions & 12 deletions src/core/image_resizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,28 +96,36 @@ class ImageResizer {
return area > maxArea;
}

static getReducePowerForJPX(width, height, componentsCount) {
// Return the power-of-two reduction exponent for canvas and area limits.
static getReducePower(width, height, maxArea = Infinity) {
if (
!Number.isInteger(width) ||
width <= 0 ||
!Number.isInteger(height) ||
height <= 0
) {
return 0;
}
const area = width * height;
// The maximum memory we've in the wasm runtime is 2GB.
// Each component is 4 bytes and we can't allocate all the memory just for
// the buffers so we limit the size to 1GB / (componentsCount * 4).
// We could use more than 2GB by setting MAXIMUM_MEMORY but it would take
// too much time to decode a big image.
const maxJPXArea = 2 ** 30 / (componentsCount * 4);
if (!this.needsToBeResized(width, height)) {
if (area > maxJPXArea) {
// The image is too large, we need to rescale it.
return Math.ceil(Math.log2(area / maxJPXArea));
if (area > maxArea) {
return Math.ceil(Math.log2(area / maxArea));
}
return 0;
}
const { MAX_DIM, MAX_AREA } = this;
const minFactor = Math.max(
width / MAX_DIM,
height / MAX_DIM,
Math.sqrt(area / Math.min(maxJPXArea, MAX_AREA))
Math.sqrt(area / Math.min(maxArea, MAX_AREA))
);
return Math.ceil(Math.log2(minFactor));
return Math.max(0, Math.ceil(Math.log2(minFactor)));
}

static getReducePowerForJPX(width, height, componentsCount) {
// Budget 1 GiB of OpenJPEG's 2 GiB Wasm heap for four-byte component
// samples.
return this.getReducePower(width, height, 2 ** 30 / (componentsCount * 4));
}

static get MAX_DIM() {
Expand Down
26 changes: 23 additions & 3 deletions src/core/jpeg_stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import { FeatureTest, shadow, warn } from "../shared/util.js";
import { DecodeStream } from "./decode_stream.js";
import { Dict } from "./primitives.js";
import { ImageResizer } from "./image_resizer.js";
import { JpegImage } from "./jpg.js";

/**
Expand Down Expand Up @@ -140,7 +141,7 @@ class JpegStream extends DecodeStream {
return this.stream.isAsync;
}

async getTransferableImage() {
async getTransferableImage(width, height) {
if (!(await JpegStream.canUseImageDecoder)) {
return null;
}
Expand Down Expand Up @@ -170,6 +171,17 @@ class JpegStream extends DecodeStream {
if (!useImageDecoder) {
return null;
}
if (
useImageDecoder.width !== width ||
useImageDecoder.height !== height
) {
// The SOF dimensions disagree with the image dictionary, e.g. because
// the height is only known from a DNL marker or because the scan simply
// ends early (issue15492.pdf). `ImageDecoder` reports and scales the
// frame according to the SOF, so let our own decoder, which honours the
// dictionary, handle the image instead.
return null;
}
if (useImageDecoder.exifStart) {
// Replace the entire EXIF-block with dummy data, to ensure that a
// non-default EXIF orientation won't cause the image to be rotated
Expand All @@ -179,11 +191,19 @@ class JpegStream extends DecodeStream {
data = data.slice();
data.fill(0x00, useImageDecoder.exifStart, useImageDecoder.exifEnd);
}
decoder = new ImageDecoder({
const init = {
data,
type: "image/jpeg",
preferAnimation: false,
});
};
// Request reduced dimensions; ImageDecoder treats them as best-effort.
const reducePower = ImageResizer.getReducePower(width, height);
if (reducePower) {
const factor = 2 ** reducePower;
init.desiredWidth = Math.ceil(width / factor);
init.desiredHeight = Math.ceil(height / factor);
}
decoder = new ImageDecoder(init);

return (await decoder.decode()).image;
} catch (reason) {
Expand Down
9 changes: 6 additions & 3 deletions src/core/jpg.js
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,8 @@ class JpegImage {
let exifOffsets = null;
let offset = 0;
let numComponents = null;
let scanLines = 0,
samplesPerLine = 0;
let fileMarker = view.getUint16(offset);
offset += 2;
if (fileMarker !== /* SOI (Start of Image) = */ 0xffd8) {
Expand Down Expand Up @@ -858,8 +860,8 @@ class JpegImage {
case 0xffc2: // SOF2 (Start of Frame, Progressive DCT)
// Skip marker length.
// Skip precision.
// Skip scanLines.
// Skip samplesPerLine.
scanLines = view.getUint16(offset + (2 + 1));
samplesPerLine = view.getUint16(offset + (2 + 1 + 2));
numComponents = data[offset + (2 + 1 + 2 + 2)];
break markerLoop;
case 0xffff: // Fill bytes
Expand All @@ -879,7 +881,8 @@ class JpegImage {
if (numComponents === 3 && colorTransform === 0) {
return null;
}
return exifOffsets || {};
// A zero SOF height means that a later DNL marker defines it.
return { width: samplesPerLine, height: scanLines, ...exifOffsets };
}

parse(data, { dnlScanLines = null } = {}) {
Expand Down
8 changes: 4 additions & 4 deletions src/core/xfa/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,10 @@ function getColor(data, def = [0, 0, 0]) {
if (!data) {
return { r, g, b };
}
const color = data
.split(",", 3)
.map(c => MathClamp(parseInt(c.trim(), 10), 0, 255))
.map(c => (isNaN(c) ? 0 : c));
const color = data.split(",", 3).map(c => {
c = parseInt(c.trim(), 10);
return isNaN(c) ? 0 : MathClamp(c, 0, 255);
});

if (color.length < 3) {
return { r, g, b };
Expand Down
46 changes: 35 additions & 11 deletions test/integration/text_layer_spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*/

/**
* @import { Page } from "puppeteer"
* @import { Browser, Page } from "puppeteer"
*/

import {
Expand Down Expand Up @@ -50,6 +50,32 @@ import { startBrowser } from "../test.mjs";
* @property {string} text
*/

// These suites require Firefox profile preferences and a dedicated browser, so
// exclude them when Firefox is disabled.
const describeFirefoxOnly = global.integrationSessions.some(
session => session.browserType === "firefox"
)
? describe
: xdescribe;

// Dedicated browser setup can exceed Jasmine's 30-second default. Keep this
// above `startBrowser`'s protocol timeout so protocol errors surface first.
const BROWSER_HOOK_TIMEOUT = 60000;

/**
* @param {Browser} [browser]
* @param {Page} [page]
*/
async function closeDedicatedBrowser(browser, page) {
try {
if (page) {
await closeSinglePage(page);
}
} finally {
await browser?.close();
}
}

describe("Text layer", () => {
describe("Text layout", () => {
let pages;
Expand Down Expand Up @@ -1240,7 +1266,7 @@ describe("Text layer", () => {
});
});

describe("using selection carets", () => {
describeFirefoxOnly("using selection carets", () => {
let browser;
let page;

Expand All @@ -1264,12 +1290,11 @@ describe("Text layer", () => {
`.page[data-page-number = "1"] .endOfContent`,
{ timeout: 0 }
);
});
}, BROWSER_HOOK_TIMEOUT);

afterEach(async () => {
await closeSinglePage(page);
await browser.close();
});
await closeDedicatedBrowser(browser, page);
}, BROWSER_HOOK_TIMEOUT);

it("doesn't jump when moving selection", async () => {
const [initialStart, initialEnd, finalEnd] = await Promise.all([
Expand Down Expand Up @@ -1468,7 +1493,7 @@ describe("Text layer", () => {
});
});

describe("when the browser enforces a minimum font size", () => {
describeFirefoxOnly("when the browser enforces a minimum font size", () => {
let browser;
let page;

Expand All @@ -1489,12 +1514,11 @@ describe("Text layer", () => {
`.page[data-page-number = "1"] .endOfContent`,
{ timeout: 0 }
);
});
}, BROWSER_HOOK_TIMEOUT);

afterEach(async () => {
await closeSinglePage(page);
await browser.close();
});
await closeDedicatedBrowser(browser, page);
}, BROWSER_HOOK_TIMEOUT);

it("renders spans with the right size", async () => {
const rect = await getSpanRectFromText(
Expand Down
1 change: 1 addition & 0 deletions test/pdfs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -950,3 +950,4 @@
!nonisolated_blend_smask.pdf
!signed_verified.pdf
!function_based_shading_cmyk.pdf
!large_jpeg_downscale.pdf
Binary file added test/pdfs/large_jpeg_downscale.pdf
Binary file not shown.
Loading
Loading