Compare commits

...

8 Commits

Author SHA1 Message Date
Jonas Jenwald
4152eae3fb
Merge pull request #19635 from Snuffleupagus/thumbnails-maxCanvasPixels
Support the `maxCanvasPixels` option in the thumbnails code
2025-03-10 16:30:25 +01:00
Jonas Jenwald
26b5c8f821
Merge pull request #19625 from Snuffleupagus/issue-19624
Improve text-selection for Type3 fonts, using `d0` operators, with empty /FontBBox-entries (issue 19624)
2025-03-10 16:26:04 +01:00
Jonas Jenwald
0edfd29a3e Improve text-selection for Type3 fonts, using d0 operators, with empty /FontBBox-entries (issue 19624)
For Type3 glyphs with `d1` operators it's easy to compute a fallback bounding box, however for `d0` the situation is more difficult.
Given that we nowadays compute the min/max of basic path-rendering operators on the worker-thread, we can utilize that by parsing these Type3 operatorLists to guess a more suitable fallback bounding box.
2025-03-10 16:21:54 +01:00
Jonas Jenwald
b9e8844541
Merge pull request #19613 from Snuffleupagus/issue-19611
Let SMask/Mask images fallback to the parent image dimensions (issue 19611)
2025-03-10 16:12:48 +01:00
Jonas Jenwald
fc22d3afc7 Support the maxCanvasPixels option in the thumbnails code
This addresses an inconsistency in the viewer, since the thumbnails don't respect the `maxCanvasPixels` option.
Note that, as far as I know, this has not lead to any bugs since the thumbnails render with a fixed (and small) width, however it really cannot hurt to address this (especially after the introduction of the `maxCanvasDim` option).

To support this a new `OutputScale`-method was added, to avoid having to duplicate code in multiple files.
2025-03-10 14:12:07 +01:00
Jonas Jenwald
10a99ea0a7 Let SMask/Mask images fallback to the parent image dimensions (issue 19611)
One of the images have a corrupt SMask, where the /Height-entry is bogus; see the excerpt below (via https://brendandahl.github.io/pdf.js.utils/browser/).
```
SMask (stream) [id: 17, gen: 0]

    ColorSpace = /DeviceGray
    Height = /Length
    Subtype = /Image
    Filter = /FlateDecode
    Type = /XObject
    Width = 157
    Matte (array)
    BitsPerComponent = 8
    Length = 3893
    <view contents> download
```

Hence we enable SMask/Mask images to fallback to the parent image dimensions, and also add more validation of the width/height to get a better error message when that data is wrong.
2025-03-10 12:37:44 +01:00
Jonas Jenwald
1bc98dfbd9
Merge pull request #19621 from Snuffleupagus/bug-1943094-thumbs
Limit the maximum thumbnail canvas width/height, similar to pages (PR 19604 follow-up)
2025-03-10 11:32:08 +01:00
Jonas Jenwald
eef15a30cd Limit the maximum thumbnail canvas width/height, similar to pages (PR 19604 follow-up)
The changes in PR 19604 weren't enough for thumbnail canvases in some cases, see e.g. https://web.archive.org/web/20231204152348if_/https://user.informatik.uni-bremen.de/cabo/rfc9000.pdf#pagemode=thumbs
2025-03-06 23:00:20 +01:00
10 changed files with 171 additions and 56 deletions

View File

@ -4679,8 +4679,15 @@ class TranslatedFont {
// not execute any operators that set the colour (or other
// colour-related parameters) in the graphics state;
// any use of such operators shall be ignored."
if (operatorList.fnArray[0] === OPS.setCharWidthAndBounds) {
this.#removeType3ColorOperators(operatorList, fontBBoxSize);
switch (operatorList.fnArray[0]) {
case OPS.setCharWidthAndBounds:
this.#removeType3ColorOperators(operatorList, fontBBoxSize);
break;
case OPS.setCharWidth:
if (!fontBBoxSize) {
this.#guessType3FontBBox(operatorList);
}
break;
}
charProcOperatorList[key] = operatorList.getIR();
@ -4728,13 +4735,7 @@ class TranslatedFont {
// Override the fontBBox when it's undefined/empty, or when it's at least
// (approximately) one order of magnitude smaller than the charBBox
// (fixes issue14999_reduced.pdf).
if (!this._bbox) {
this._bbox = [Infinity, Infinity, -Infinity, -Infinity];
}
this._bbox[0] = Math.min(this._bbox[0], charBBox[0]);
this._bbox[1] = Math.min(this._bbox[1], charBBox[1]);
this._bbox[2] = Math.max(this._bbox[2], charBBox[2]);
this._bbox[3] = Math.max(this._bbox[3], charBBox[3]);
this.#computeCharBBox(charBBox);
}
let i = 0,
@ -4787,6 +4788,37 @@ class TranslatedFont {
i++;
}
}
#guessType3FontBBox(operatorList) {
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
assert(
operatorList.fnArray[0] === OPS.setCharWidth,
"Type3 glyph shall start with the d0 operator."
);
}
let i = 1;
const ii = operatorList.length;
while (i < ii) {
switch (operatorList.fnArray[i]) {
case OPS.constructPath:
const minMax = operatorList.argsArray[i][2];
// Override the fontBBox when it's undefined/empty (fixes 19624.pdf).
this.#computeCharBBox(minMax);
break;
}
i++;
}
}
#computeCharBBox(bbox) {
this._bbox ||= [Infinity, Infinity, -Infinity, -Infinity];
this._bbox[0] = Math.min(this._bbox[0], bbox[0]);
this._bbox[1] = Math.min(this._bbox[1], bbox[1]);
this._bbox[2] = Math.max(this._bbox[2], bbox[2]);
this._bbox[3] = Math.max(this._bbox[3], bbox[3]);
}
}
class StateManager {

View File

@ -140,11 +140,26 @@ class PDFImage {
);
width = image.width;
height = image.height;
}
if (width < 1 || height < 1) {
throw new FormatError(
`Invalid image width: ${width} or height: ${height}`
);
} else {
const validWidth = typeof width === "number" && width > 0,
validHeight = typeof height === "number" && height > 0;
if (!validWidth || !validHeight) {
if (!image.fallbackDims) {
throw new FormatError(
`Invalid image width: ${width} or height: ${height}`
);
}
warn(
"PDFImage - using the Width/Height of the parent image, for SMask/Mask data."
);
if (!validWidth) {
width = image.fallbackDims.width;
}
if (!validHeight) {
height = image.fallbackDims.height;
}
}
}
this.width = width;
this.height = height;
@ -244,6 +259,10 @@ class PDFImage {
}
if (smask) {
// Provide fallback width/height values for corrupt SMask images
// (see issue19611.pdf).
smask.fallbackDims ??= { width, height };
this.smask = new PDFImage({
xref,
res,
@ -260,6 +279,10 @@ class PDFImage {
if (!imageMask) {
warn("Ignoring /Mask in image without /ImageMask.");
} else {
// Provide fallback width/height values for corrupt Mask images
// (see issue19611.pdf).
mask.fallbackDims ??= { width, height };
this.mask = new PDFImage({
xref,
res,

View File

@ -640,9 +640,39 @@ class OutputScale {
return this.sx !== 1 || this.sy !== 1;
}
/**
* @type {boolean} Returns `true` when scaling is symmetric,
* `false` otherwise.
*/
get symmetric() {
return this.sx === this.sy;
}
/**
* @returns {boolean} Returns `true` if scaling was limited,
* `false` otherwise.
*/
limitCanvas(width, height, maxPixels, maxDim) {
let maxAreaScale = Infinity,
maxWidthScale = Infinity,
maxHeightScale = Infinity;
if (maxPixels > 0) {
maxAreaScale = Math.sqrt(maxPixels / (width * height));
}
if (maxDim !== -1) {
maxWidthScale = maxDim / width;
maxHeightScale = maxDim / height;
}
const maxScale = Math.min(maxAreaScale, maxWidthScale, maxHeightScale);
if (this.sx > maxScale || this.sy > maxScale) {
this.sx = maxScale;
this.sy = maxScale;
return true;
}
return false;
}
}
// See https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types

View File

@ -0,0 +1 @@
https://github.com/user-attachments/files/19102190/test.pdf

View File

@ -0,0 +1 @@
https://github.com/user-attachments/files/19126771/okm2500682934750615600.pdf

View File

@ -3913,6 +3913,23 @@
"rounds": 1,
"type": "eq"
},
{
"id": "issue19611",
"file": "pdfs/issue19611.pdf",
"md5": "169dc6df1c43dcb4659b2ddb6a4b39e4",
"rounds": 1,
"link": true,
"type": "eq"
},
{
"id": "issue19624-text",
"file": "pdfs/issue19624.pdf",
"md5": "087ac2141dbfa218be833efcc143925a",
"rounds": 1,
"link": true,
"firstPage": 2,
"type": "text"
},
{
"id": "issue1127-text",
"file": "pdfs/issue1127.pdf",

View File

@ -478,6 +478,7 @@ const PDFViewerApplication = {
: null;
const enableHWA = AppOptions.get("enableHWA"),
maxCanvasPixels = AppOptions.get("maxCanvasPixels"),
maxCanvasDim = AppOptions.get("maxCanvasDim");
const pdfViewer = new PDFViewer({
container,
@ -506,7 +507,7 @@ const PDFViewerApplication = {
),
imageResourcesPath: AppOptions.get("imageResourcesPath"),
enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
maxCanvasPixels,
maxCanvasDim,
enableDetailCanvas: AppOptions.get("enableDetailCanvas"),
enablePermissions: AppOptions.get("enablePermissions"),
@ -529,6 +530,7 @@ const PDFViewerApplication = {
eventBus,
renderingQueue: pdfRenderingQueue,
linkService: pdfLinkService,
maxCanvasPixels,
maxCanvasDim,
pageColors,
abortSignal,

View File

@ -775,28 +775,13 @@ class PDFPageView extends BasePDFPageView {
outputScale.sx *= invScale;
outputScale.sy *= invScale;
this.#needsRestrictedScaling = true;
} else if (this.maxCanvasPixels > 0 || this.maxCanvasDim !== -1) {
let maxAreaScale = Infinity,
maxWidthScale = Infinity,
maxHeightScale = Infinity;
if (this.maxCanvasPixels > 0) {
const pixelsInViewport = width * height;
maxAreaScale = Math.sqrt(this.maxCanvasPixels / pixelsInViewport);
}
if (this.maxCanvasDim !== -1) {
maxWidthScale = this.maxCanvasDim / width;
maxHeightScale = this.maxCanvasDim / height;
}
const maxScale = Math.min(maxAreaScale, maxWidthScale, maxHeightScale);
if (outputScale.sx > maxScale || outputScale.sy > maxScale) {
outputScale.sx = maxScale;
outputScale.sy = maxScale;
this.#needsRestrictedScaling = true;
} else {
this.#needsRestrictedScaling = false;
}
} else {
this.#needsRestrictedScaling = outputScale.limitCanvas(
width,
height,
this.maxCanvasPixels,
this.maxCanvasDim
);
}
}

View File

@ -42,6 +42,9 @@ const THUMBNAIL_WIDTH = 98; // px
* The default value is `null`.
* @property {IPDFLinkService} linkService - The navigation/linking service.
* @property {PDFRenderingQueue} renderingQueue - The rendering queue object.
* @property {number} [maxCanvasPixels] - The maximum supported canvas size in
* total pixels, i.e. width * height. Use `-1` for no limit, or `0` for
* CSS-only zooming. The default value is 4096 * 8192 (32 mega-pixels).
* @property {number} [maxCanvasDim] - The maximum supported canvas dimension,
* in either width or height. Use `-1` for no limit.
* The default value is 32767.
@ -97,6 +100,7 @@ class PDFThumbnailView {
optionalContentConfigPromise,
linkService,
renderingQueue,
maxCanvasPixels,
maxCanvasDim,
pageColors,
enableHWA,
@ -110,6 +114,7 @@ class PDFThumbnailView {
this.viewport = defaultViewport;
this.pdfPageRotate = defaultViewport.rotation;
this._optionalContentConfigPromise = optionalContentConfigPromise || null;
this.maxCanvasPixels = maxCanvasPixels ?? AppOptions.get("maxCanvasPixels");
this.maxCanvasDim = maxCanvasDim || AppOptions.get("maxCanvasDim");
this.pageColors = pageColors || null;
this.enableHWA = enableHWA || false;
@ -215,9 +220,17 @@ class PDFThumbnailView {
willReadFrequently: !enableHWA,
});
const outputScale = new OutputScale();
const width = upscaleFactor * this.canvasWidth,
height = upscaleFactor * this.canvasHeight;
canvas.width = (upscaleFactor * this.canvasWidth * outputScale.sx) | 0;
canvas.height = (upscaleFactor * this.canvasHeight * outputScale.sy) | 0;
outputScale.limitCanvas(
width,
height,
this.maxCanvasPixels,
this.maxCanvasDim
);
canvas.width = (width * outputScale.sx) | 0;
canvas.height = (height * outputScale.sy) | 0;
const transform = outputScale.scaled
? [outputScale.sx, 0, 0, outputScale.sy, 0, 0]
@ -352,6 +365,27 @@ class PDFThumbnailView {
this.#convertCanvasToImage(canvas);
}
#getReducedImageDims(canvas) {
let reducedWidth = canvas.width << MAX_NUM_SCALING_STEPS,
reducedHeight = canvas.height << MAX_NUM_SCALING_STEPS;
const outputScale = new OutputScale();
// Here we're not actually "rendering" to the canvas and the `OutputScale`
// is thus only used to limit the canvas size, hence the identity scale.
outputScale.sx = outputScale.sy = 1;
outputScale.limitCanvas(
reducedWidth,
reducedHeight,
this.maxCanvasPixels,
this.maxCanvasDim
);
reducedWidth = (reducedWidth * outputScale.sx) | 0;
reducedHeight = (reducedHeight * outputScale.sy) | 0;
return [reducedWidth, reducedHeight];
}
#reduceImage(img) {
const { ctx, canvas } = this.#getPageDrawContext(1, true);
@ -369,24 +403,8 @@ class PDFThumbnailView {
);
return canvas;
}
const { maxCanvasDim } = this;
// drawImage does an awful job of rescaling the image, doing it gradually.
let reducedWidth = canvas.width << MAX_NUM_SCALING_STEPS;
let reducedHeight = canvas.height << MAX_NUM_SCALING_STEPS;
if (maxCanvasDim !== -1) {
const maxWidthScale = maxCanvasDim / reducedWidth,
maxHeightScale = maxCanvasDim / reducedHeight;
if (maxWidthScale < 1) {
reducedWidth = maxCanvasDim;
reducedHeight = (reducedHeight * maxWidthScale) | 0;
} else if (maxHeightScale < 1) {
reducedWidth = (reducedWidth * maxHeightScale) | 0;
reducedHeight = maxCanvasDim;
}
}
let [reducedWidth, reducedHeight] = this.#getReducedImageDims(canvas);
const [reducedImage, reducedImageCtx] = TempImageFactory.getCanvas(
reducedWidth,
reducedHeight

View File

@ -39,6 +39,9 @@ const THUMBNAIL_SELECTED_CLASS = "selected";
* @property {EventBus} eventBus - The application event bus.
* @property {IPDFLinkService} linkService - The navigation/linking service.
* @property {PDFRenderingQueue} renderingQueue - The rendering queue object.
* @property {number} [maxCanvasPixels] - The maximum supported canvas size in
* total pixels, i.e. width * height. Use `-1` for no limit, or `0` for
* CSS-only zooming. The default value is 4096 * 8192 (32 mega-pixels).
* @property {number} [maxCanvasDim] - The maximum supported canvas dimension,
* in either width or height. Use `-1` for no limit.
* The default value is 32767.
@ -63,6 +66,7 @@ class PDFThumbnailViewer {
eventBus,
linkService,
renderingQueue,
maxCanvasPixels,
maxCanvasDim,
pageColors,
abortSignal,
@ -72,6 +76,7 @@ class PDFThumbnailViewer {
this.eventBus = eventBus;
this.linkService = linkService;
this.renderingQueue = renderingQueue;
this.maxCanvasPixels = maxCanvasPixels;
this.maxCanvasDim = maxCanvasDim;
this.pageColors = pageColors || null;
this.enableHWA = enableHWA || false;
@ -214,6 +219,7 @@ class PDFThumbnailViewer {
optionalContentConfigPromise,
linkService: this.linkService,
renderingQueue: this.renderingQueue,
maxCanvasPixels: this.maxCanvasPixels,
maxCanvasDim: this.maxCanvasDim,
pageColors: this.pageColors,
enableHWA: this.enableHWA,