Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ function setUp(op: ToolOp) {
// Every tool that runs outside the qpdf worker. Engines load on first use.
const LOCAL: Partial<Record<ToolOp, LocalJob>> = {
'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',
Expand Down
6 changes: 3 additions & 3 deletions src/engine/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ export async function imagesToPdf(files: File[]): Promise<Output> {
};
}

/** 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<Output> {
/** 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<Output> {
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;
Expand All @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions src/seo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,8 @@ function optionsHtml(c: Copy, tool: Tool) {
<label>${esc(c.t('ws.saveAs'))}<select name="format">${option('txt', c.t('ws.fmtTxt'))}${option('srt', c.t('ws.fmtSrt'), tool.preset?.format === 'srt')}${option('vtt', c.t('ws.fmtVtt'))}</select></label>
<p class="note">${esc(c.t('ws.speechEnglish'))}</p>
<p class="note">${esc(c.t('ws.speechModel', { mb: SPEECH_MODEL_MB }))}</p>`;
if (tool.op === 'pdf-to-jpg')
return `<label>${esc(c.t('ws.quality'))}<select name="dpi">${option('72', c.t('ws.dpiLow'))}${option('150', c.t('ws.dpiStandard'), true)}${option('300', c.t('ws.dpiHigh'))}</select></label>`;
if (tool.op === 'excel-to-json') return `<label class="check"><input name="header" type="checkbox" checked> ${esc(c.t('ws.header'))}</label>`;
if (tool.op === 'compress-pdf' || tool.op === 'office-compress')
return `<label>${esc(c.t('ws.compression'))}<select name="level">${option('light', c.t('ws.balanced'))}${option('strong', c.t('ws.strong'))}</select></label>`;
Expand Down
13 changes: 13 additions & 0 deletions tests/app.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading