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/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. 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 ``; 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);