From 892e50ac502fa6f852331a92943787d7253d8c59 Mon Sep 17 00:00:00 2001 From: Augustin Amari Date: Thu, 8 Oct 2026 09:21:05 +0200 Subject: [PATCH 1/3] feat(pdf-to-jpg): render pages at a chosen DPI pdfToImages() always rendered at 150 DPI. It now takes a dpi argument (150 by default, so callers are unchanged) and scales the page by dpi / 72, the number of PDF units per inch. Refs #6 --- src/engine/local.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/engine/local.ts b/src/engine/local.ts index 0cf9d07..4c569be 100644 --- a/src/engine/local.ts +++ b/src/engine/local.ts @@ -118,8 +118,8 @@ export async function imagesToPdf(files: File[]): Promise { }; } -/** Renders every page to a JPEG (or PNG) at 150 DPI with pdf.js; several pages come back as a ZIP. */ -export async function pdfToImages(file: File, format: 'jpg' | 'png' = 'jpg'): Promise { +/** Renders every page to a JPEG (or PNG) at `dpi` (150 by default) with pdf.js; several pages come back as a ZIP. */ +export async function pdfToImages(file: File, format: 'jpg' | 'png' = 'jpg', dpi = 150): Promise { const pdfjs = await import('pdfjs-dist/legacy/build/pdf.mjs'); // legacy = polyfilled for browsers from 2023 on pdfjs.GlobalWorkerOptions.workerSrc = (await import('pdfjs-dist/legacy/build/pdf.worker.min.mjs?url')).default; let doc; @@ -135,7 +135,7 @@ export async function pdfToImages(file: File, format: 'jpg' | 'png' = 'jpg'): Pr let last: Blob | undefined; for (let number = 1; number <= doc.numPages; number++) { const page = await doc.getPage(number); - const viewport = page.getViewport({ scale: 150 / 72 }); + const viewport = page.getViewport({ scale: dpi / 72 }); // PDF units are 1/72 inch const canvas = new OffscreenCanvas(Math.ceil(viewport.width), Math.ceil(viewport.height)); const context = canvas.getContext('2d')!; context.fillStyle = '#fff'; // JPEG has no transparency; PDFs assume white paper. From 618a49f680b107ee0b8a6243f9012580d36cb6f5 Mon Sep 17 00:00:00 2001 From: Augustin Amari Date: Thu, 8 Oct 2026 09:32:41 +0200 Subject: [PATCH 2/3] feat(pdf-to-jpg): let users pick 72, 150 or 300 DPI Adds a Quality select to PDF to JPG and PDF to PNG: low (72 DPI) for small web images, standard (150 DPI, still the default) and high (300 DPI) for print. It reuses the Redact PDF strings and adds one for 72 DPI; other languages fall back to English. Refs #6 --- src/app.ts | 2 +- src/i18n.ts | 1 + src/seo.ts | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index 075736b..dc98840 100644 --- a/src/app.ts +++ b/src/app.ts @@ -120,7 +120,7 @@ function setUp(op: ToolOp) { // Every tool that runs outside the qpdf worker. Engines load on first use. const LOCAL: Partial> = { 'jpg-to-pdf': async (f) => (await import('./engine/local')).imagesToPdf(f), - 'pdf-to-jpg': async (f, o) => (await import('./engine/local')).pdfToImages(f[0], o.format === 'png' ? 'png' : 'jpg'), + 'pdf-to-jpg': async (f, o) => (await import('./engine/local')).pdfToImages(f[0], o.format === 'png' ? 'png' : 'jpg', Number(o.dpi) || 150), 'page-numbers': async (f, o) => (await import('./engine/stamp')).addPageNumbers(f[0], { position: o.position as 'bottom-center' | 'bottom-right' | 'top-right', diff --git a/src/i18n.ts b/src/i18n.ts index abbae9d..9286850 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -165,6 +165,7 @@ export const UI = { 'ws.startAt': 'Start at', 'ws.targetKb': 'Max file size (KB)', 'ws.targetAny': 'No limit', + 'ws.dpiLow': 'Low (smallest file, for the web)', 'ws.dpiStandard': 'Standard (smaller file)', 'ws.dpiHigh': 'High (sharper small print)', 'ws.look': 'Look', diff --git a/src/seo.ts b/src/seo.ts index b9f09fd..3708c92 100644 --- a/src/seo.ts +++ b/src/seo.ts @@ -256,6 +256,8 @@ function optionsHtml(c: Copy, tool: Tool) {

${esc(c.t('ws.speechEnglish'))}

${esc(c.t('ws.speechModel', { mb: SPEECH_MODEL_MB }))}

`; + if (tool.op === 'pdf-to-jpg') + return ``; if (tool.op === 'excel-to-json') return ``; if (tool.op === 'compress-pdf' || tool.op === 'office-compress') return ``; From 87fadb54de613510592947fd68a6e140471d182f Mon Sep 17 00:00:00 2001 From: Augustin Amari Date: Thu, 8 Oct 2026 09:34:02 +0200 Subject: [PATCH 3/3] test(pdf-to-jpg): check each DPI gives the expected image size Extends the PDF to PNG browser test: a 400 pt wide page must come out 400, 834 and 1667 px wide at 72, 150 and 300 DPI, and the select must default to 150. Refs #6 --- tests/app.e2e.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/app.e2e.ts b/tests/app.e2e.ts index 21d61ba..49c2f68 100644 --- a/tests/app.e2e.ts +++ b/tests/app.e2e.ts @@ -830,6 +830,19 @@ test('numbers and watermarks PDF pages, and converts between image formats', asy await page.getByRole('button', { name: 'Convert to PNG' }).click(); await expect(page.locator('#status')).toContainText('Done'); expect((await downloadBytes(page)).name).toMatch(/\.(png|zip)$/); + + // The resolution select sets the image size: a 400 pt wide page is 400 px at 72 DPI, 834 at 150 and 1667 at 300. + for (const [dpi, width] of [['72', 400], ['150', 834], ['300', 1667]] as const) { + await page.goto('/pdf-to-png/'); + await expect(page.locator('select[name="dpi"]')).toHaveValue('150'); + await page.locator('#file-input').setInputFiles(fixture('blank')); + await page.locator('select[name="dpi"]').selectOption(dpi); + await page.getByRole('button', { name: 'Convert to PNG' }).click(); + await expect(page.locator('#status')).toContainText('Done — 1 image'); + const png = await downloadBytes(page); + expect(png.name).toBe('blank.png'); + expect(png.bytes.readUInt32BE(16)).toBe(width); + } await page.goto('/png-to-jpg/'); await expect(page.locator('select[name="format"]')).toHaveCount(0);