300 DPI 代表像素數增至四倍:PDF 渲染前先設定畫布像素預算
300 DPI means four times the pixels: budget your canvas before rendering a PDF
文章指出,US Letter 頁面由 150 DPI 提升至 300 DPI 時,像素數增至四倍,應在配置畫布前按單邊尺寸和總像素數限制光柵。示例預設單邊上限為 4,096 像素、總量上限為 8,000,000 像素;要求 300 DPI 時,輸出為 2486 × 3217 像素,所用縮放約相當於 292.51 DPI。
A JPEG size limit is checked after encoding. A canvas has to exist before that encoding can happen.
That ordering matters in a browser PDF-to-image pipeline. Lowering JPEG quality can reduce the downloaded file while leaving the rendering dimensions unchanged. It is useful to give the raster its own budget before asking the browser to allocate it.
AI disclosure: this article and its examples were prepared with AI assistance. The pixel calculations and budget invariants were checked with executable Node.js tests. The figures below are a storage model, not measurements of a browser's memory usage.
Doubling DPI squares the pixel count
Take a US Letter page represented as 612 × 792 PDF points. With 72 points per inch, the scale for a requested DPI is dpi / 72. Doubling that scale doubles both dimensions.
Here is the result for an uncapped page. The last column assumes an 8-bit RGBA buffer: four bytes per pixel, divided by 1,048,576 to express MiB.
| DPI | Dimensions | Pixels | One RGBA8 buffer (MiB) |
|---|---|---|---|
| 150 | 1275 × 1650 | 2,103,750 | 8.03 |
| 300 | 2550 × 3300 | 8,415,000 | 32.10 |
The raw pixel count is four times larger at 300 DPI. The eventual JPEG size does not appear in that calculation.
MDN's ImageData reference describes RGBA ordering and the rgba-unorm8 pixel format. That gives a useful estimate for one RGBA8 buffer, not a guarantee about a canvas implementation. PDF decoding, fonts, image caches, temporary encoder buffers, retained downloads, and browser overhead are outside this table. A browser may use other pixel formats or storage strategies.
Put limits on both area and edge length
A pixel-area limit by itself leaves a gap: a very long, thin page may fit the area budget but exceed a browser's dimension limit. An edge limit by itself lets a square consume more pixels than intended.
Use the smallest scale allowed by the requested DPI, either edge, and the total area. Then round the dimensions down and validate the final integer grid.
Save this standalone example as raster-budget.mjs:
export function fitRaster(widthPt, heightPt, requestedDpi, {
maxEdge = 4096,
maxPixels = 8_000_000,
} = {}) {
if (![widthPt, heightPt, requestedDpi].every(n => Number.isFinite(n) && n > 0)) {
throw new RangeError('Dimensions and DPI must be positive finite numbers');
}
if (![maxEdge, maxPixels].every(n => Number.isSafeInteger(n) && n > 0)) {
throw new RangeError('Budgets must be positive safe integers');
}
const scale = Math.min(
requestedDpi / 72,
maxEdge / widthPt,
maxEdge / heightPt,
Math.sqrt(maxPixels / widthPt / heightPt),
);
if (!(scale > 0) || !Number.isFinite(scale)) {
throw new RangeError('Dimensions cannot be represented at this budget');
}
const width = Math.max(1, Math.floor(widthPt * scale));
const height = Math.max(1, Math.floor(heightPt * scale));
if (width > maxEdge || height > maxEdge || width * height > maxPixels) {
throw new RangeError('Raster cannot fit the budget on an integer pixel grid');
}
return { width, height, scale, effectiveDpi: scale * 72 };
}
The defaults are application policy: 4,096 pixels on either edge and eight million pixels in total. They are not a claim that every device can allocate that canvas. Canvas limits vary by browser and environment, and allocation or rendering can still fail below a chosen budget. Keep that error path visible.
For the same Letter page requested at 300 DPI, those defaults choose a scale corresponding to about 292.51 DPI before integer rounding, producing 2486 × 3217 pixels. If the UI still says “300 DPI output,” it is describing the request rather than the result. Show the resulting dimensions, and distinguish requested resolution from the scale actually used.
The final guard is deliberate. Extremely thin dimensions can require a minimum one-pixel edge that breaks a tiny area budget. This example rejects a grid that cannot satisfy the limits instead of silently returning an oversized result.
Keep the calculation reproducible
Put this next to the module as check.mjs, then run node check.mjs:
import assert from 'node:assert/strict';
import { fitRaster } from './raster-budget.mjs';
const roomy = { maxEdge: 10_000, maxPixels: 16_000_000 };
const low = fitRaster(612, 792, 150, roomy);
const high = fitRaster(612, 792, 300, roomy);
assert.deepEqual([low.width, low.height], [1275, 1650]);
assert.deepEqual([high.width, high.height], [2550, 3300]);
assert.equal(high.width * high.height / (low.width * low.height), 4);
const capped = fitRaster(612, 792, 300);
assert.ok(capped.width <= 4096 && capped.height <= 4096);
assert.ok(capped.width * capped.height <= 8_000_000);
assert.throws(() => fitRaster(612, 792, NaN), RangeError);
assert.throws(() => fitRaster(612, 792, 300, { maxPixels: 0 }), RangeError);
console.log('Pixel math and budget checks passed');
These checks verify dimensions, scaling, and rejection behavior. They do not allocate a canvas, render a PDF, measure peak memory, or establish a safe device-specific limit. Those need separate browser tests with representative files.
Budget the rest of the pipeline too
The raster cap controls one page's dimensions. A multipage converter still needs a concurrency policy and a budget for retained output.
An unbounded Promise.all over pages can start several renders before any encoder finishes. Processing pages sequentially, or with a small bounded pool, limits the number of active page jobs. With PDF.js, the same canvas also cannot render two pages simultaneously. Await the render and encoding work before reusing it.
After a page is finished, clear references that are no longer needed and reset the temporary canvas dimensions in a finally block. Assigning canvas width resets its rendering context. Resetting dimensions is resource cleanup, not proof that the process's reported memory immediately drops.
Meanwhile, the completed JPEG blobs may remain in memory for individual downloads or a ZIP. Count their blob.size values separately and stop retaining results when the application's output budget would be exceeded. That accounting does not include every byte an encoder or ZIP builder may temporarily allocate.
A partial conversion should report its completed-page count and stop reason. An output limit reached after several pages is useful recoverable information; displaying the same success message as a complete conversion would hide the missing pages.
The practical checks belong at different stages: calculate the raster dimensions before allocation, bound active jobs while rendering, and account for retained encoded files as they become ready. A small downloaded JPEG is evidence about the last stage only.
來源:DEV Community · dev.to