mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-04-09 14:54:04 +02:00
This patch adds right-click support for images in the PDF, allowing users to download them. To minimize memory consumption, we: - Do not store the images separately, and instead crop them out of the PDF page canvas - Only extract the images when needed (i.e. when the user right-clicks on them), rather than eagery having all of them available. To do so, we layer one empty 0x0 canvas per image, stretched to cover the whole image, and only populate its contents on right click. These images need to be inside the text layer: they cannot be _behind_ it, otherwise they would be covered by the text layer's container and not be clickable, and they cannot be in front of it, otherwise they would make the text spans unselectable. This feature is managed by a new preference, `imagesRightClickMinSize`: - when it's set to `-1`, right-click support is disabled - when set to `0`, all images are available for right click - when set to a positive integer, only images whose width and height are greater than or equal to that value (in the PDF page frame of reference) are available for right click. This features is disabled by default outside of MOZCENTRAL, as it significantly degrades the text selection experience in non-Firefox browsers.
290 lines
9.8 KiB
JavaScript
290 lines
9.8 KiB
JavaScript
/* Copyright 2026 Mozilla Foundation
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import { closePages, loadAndWait } from "./test_utils.mjs";
|
|
|
|
describe("Text layer images", () => {
|
|
describe("basic", () => {
|
|
let pages;
|
|
|
|
beforeEach(async () => {
|
|
pages = await loadAndWait(
|
|
"images.pdf",
|
|
`.page[data-page-number = "1"] .endOfContent`,
|
|
undefined,
|
|
{
|
|
// When running Firefox with Puppeteer, setting the
|
|
// devicePixelRatio Puppeteer option does not properly set
|
|
// the `window.devicePixelRatio` value. Set it manually.
|
|
earlySetup: `() => { window.devicePixelRatio = 1 }`,
|
|
},
|
|
{ imagesRightClickMinSize: 16 },
|
|
{ width: 800, height: 600, devicePixelRatio: 1 }
|
|
);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await closePages(pages);
|
|
});
|
|
|
|
it("should render images in the text layer", async () => {
|
|
await Promise.all(
|
|
pages.map(async ([browserName, page]) => {
|
|
const images = await page.$$eval(
|
|
`.page[data-page-number="1"] > .textLayer > .textLayerImages > canvas`,
|
|
els => els.map(el => JSON.stringify(el.getBoundingClientRect()))
|
|
);
|
|
|
|
expect(images.length).withContext(`In ${browserName}`).toEqual(5);
|
|
})
|
|
);
|
|
});
|
|
|
|
it("when right-clicking an image it should get the contents", async () => {
|
|
await Promise.all(
|
|
pages.map(async ([browserName, page]) => {
|
|
const imageCanvas = await page.$(
|
|
`.page[data-page-number="1"] > .textLayer > .textLayerImages > canvas`
|
|
);
|
|
|
|
expect(await page.evaluate(el => el.width, imageCanvas))
|
|
.withContext(`Initial width, in ${browserName}`)
|
|
.toBe(0);
|
|
expect(await page.evaluate(el => el.height, imageCanvas))
|
|
.withContext(`Initial height, in ${browserName}`)
|
|
.toBe(0);
|
|
|
|
await imageCanvas.click({ button: "right" });
|
|
|
|
expect(await page.evaluate(el => el.width, imageCanvas))
|
|
.withContext(`Final width, in ${browserName}`)
|
|
.toBeGreaterThan(0);
|
|
expect(await page.evaluate(el => el.height, imageCanvas))
|
|
.withContext(`Final height, in ${browserName}`)
|
|
.toBeGreaterThan(0);
|
|
|
|
expect(
|
|
await page.evaluate(el => {
|
|
const ctx = el.getContext("2d");
|
|
const imageData = ctx.getImageData(0, 0, el.width, el.height);
|
|
const pixels = new Uint32Array(imageData.data.buffer);
|
|
const firstPixel = pixels[0];
|
|
return pixels.some(pixel => pixel !== firstPixel);
|
|
}, imageCanvas)
|
|
)
|
|
.withContext(`Image is not all the same pixel, in ${browserName}`)
|
|
.toBe(true);
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("transforms", () => {
|
|
let pages;
|
|
|
|
beforeEach(async () => {
|
|
pages = await loadAndWait(
|
|
"images.pdf",
|
|
`.page[data-page-number = "1"] .endOfContent`,
|
|
undefined,
|
|
{
|
|
// When running Firefox with Puppeteer, setting the
|
|
// devicePixelRatio Puppeteer option does not properly set
|
|
// the `window.devicePixelRatio` value. Set it manually.
|
|
earlySetup: `() => { window.devicePixelRatio = 1 }`,
|
|
},
|
|
{ imagesRightClickMinSize: 16 },
|
|
{ width: 800, height: 600, devicePixelRatio: 1 }
|
|
);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await closePages(pages);
|
|
});
|
|
|
|
it("the three copies of the PDF.js logo have different rotations", async () => {
|
|
await Promise.all(
|
|
pages.map(async ([browserName, page]) => {
|
|
const getRotation = async nth =>
|
|
page.evaluate(n => {
|
|
const canvas = document.querySelectorAll(
|
|
`.page[data-page-number="1"] > .textLayer > .textLayerImages > canvas`
|
|
)[n];
|
|
const cssTransform = getComputedStyle(canvas).transform;
|
|
if (cssTransform && cssTransform !== "none") {
|
|
const matrixValues = cssTransform
|
|
.slice(7, -1)
|
|
.split(", ")
|
|
.map(parseFloat);
|
|
return (
|
|
Math.atan2(matrixValues[1], matrixValues[0]) * (180 / Math.PI)
|
|
);
|
|
}
|
|
return 0;
|
|
}, nth);
|
|
|
|
const rotation1 = await getRotation(1);
|
|
const rotation2 = await getRotation(2);
|
|
const rotation4 = await getRotation(4);
|
|
|
|
expect(Math.abs(rotation1 - rotation2))
|
|
.withContext(`Rotation between 1 and 2, in ${browserName}`)
|
|
.toBeGreaterThan(10);
|
|
expect(Math.abs(rotation1 - rotation4))
|
|
.withContext(`Rotation between 1 and 4, in ${browserName}`)
|
|
.toBeGreaterThan(10);
|
|
expect(Math.abs(rotation2 - rotation4))
|
|
.withContext(`Rotation between 2 and 4, in ${browserName}`)
|
|
.toBeGreaterThan(10);
|
|
})
|
|
);
|
|
});
|
|
|
|
it("the three copies of the PDF.js logo have the same size", async () => {
|
|
await Promise.all(
|
|
pages.map(async ([browserName, page]) => {
|
|
const getSize = async nth =>
|
|
page.evaluate(n => {
|
|
const canvas = document.querySelectorAll(
|
|
`.page[data-page-number="1"] > .textLayer > .textLayerImages > canvas`
|
|
)[n];
|
|
return { width: canvas.width, height: canvas.height };
|
|
}, nth);
|
|
|
|
const size1 = await getSize(1);
|
|
const size2 = await getSize(2);
|
|
const size4 = await getSize(4);
|
|
|
|
const EPSILON = 3;
|
|
|
|
expect(size1.width)
|
|
.withContext(`1-2 width, in ${browserName}`)
|
|
.toBeCloseTo(size2.width, EPSILON);
|
|
expect(size1.height)
|
|
.withContext(`1-2 height, in ${browserName}`)
|
|
.toBeCloseTo(size2.height, EPSILON);
|
|
|
|
expect(size1.width)
|
|
.withContext(`1-4 width, in ${browserName}`)
|
|
.toBeCloseTo(size4.width, EPSILON);
|
|
expect(size1.height)
|
|
.withContext(`1-4 height, in ${browserName}`)
|
|
.toBeCloseTo(size4.height, EPSILON);
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("trimming", () => {
|
|
let pages;
|
|
|
|
beforeEach(async () => {
|
|
pages = await loadAndWait(
|
|
"bug_jpx.pdf",
|
|
`.page[data-page-number = "1"] .endOfContent`,
|
|
undefined,
|
|
{
|
|
// When running Firefox with Puppeteer, setting the
|
|
// devicePixelRatio Puppeteer option does not properly set
|
|
// the `window.devicePixelRatio` value. Set it manually.
|
|
earlySetup: `() => { window.devicePixelRatio = 1 }`,
|
|
},
|
|
{ imagesRightClickMinSize: 16 },
|
|
{ width: 800, height: 600, devicePixelRatio: 1 }
|
|
);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await closePages(pages);
|
|
});
|
|
|
|
it("no white border around black image", async () => {
|
|
await Promise.all(
|
|
pages.map(async ([browserName, page]) => {
|
|
const canvasHandle = await page.$(
|
|
`.page[data-page-number="1"] > .textLayer > .textLayerImages > canvas`
|
|
);
|
|
|
|
await canvasHandle.click({ button: "right" });
|
|
|
|
expect(
|
|
await page.evaluate(el => {
|
|
const ctx = el.getContext("2d");
|
|
const imageData = ctx.getImageData(0, 0, el.width, el.height);
|
|
const pixels = new Uint32Array(imageData.data.buffer);
|
|
return Array.from(pixels.filter(pixel => pixel !== 0xff000000));
|
|
}, canvasHandle)
|
|
)
|
|
.withContext(`Image is all black, in ${browserName}`)
|
|
.toEqual([]);
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("trimming after rotation", () => {
|
|
let pages;
|
|
|
|
beforeEach(async () => {
|
|
pages = await loadAndWait(
|
|
"image-rotated-black-white-ratio.pdf",
|
|
`.page[data-page-number = "1"] .endOfContent`,
|
|
undefined,
|
|
{
|
|
// When running Firefox with Puppeteer, setting the
|
|
// devicePixelRatio Puppeteer option does not properly set
|
|
// the `window.devicePixelRatio` value. Set it manually.
|
|
earlySetup: `() => { window.devicePixelRatio = 1 }`,
|
|
},
|
|
{ imagesRightClickMinSize: 16 },
|
|
{ width: 800, height: 600, devicePixelRatio: 1 }
|
|
);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await closePages(pages);
|
|
});
|
|
|
|
it("no white extra white around rotated image", async () => {
|
|
await Promise.all(
|
|
pages.map(async ([browserName, page]) => {
|
|
const canvasHandle = await page.$(
|
|
`.page[data-page-number="1"] > .textLayer > .textLayerImages > canvas`
|
|
);
|
|
|
|
await canvasHandle.click({ button: "right" });
|
|
|
|
expect(
|
|
await page.evaluate(el => {
|
|
const ctx = el.getContext("2d");
|
|
const imageData = ctx.getImageData(0, 0, el.width, el.height);
|
|
const pixels = new Uint32Array(imageData.data.buffer);
|
|
const blackPixels = pixels.filter(
|
|
pixel => pixel === 0xff000000
|
|
).length;
|
|
const whitePixels = pixels.filter(
|
|
pixel => pixel === 0xffffffff
|
|
).length;
|
|
return blackPixels / (blackPixels + whitePixels);
|
|
}, canvasHandle)
|
|
)
|
|
.withContext(`Image is 75% black, in ${browserName}`)
|
|
.toBeCloseTo(0.75);
|
|
})
|
|
);
|
|
});
|
|
});
|
|
});
|