diff --git a/apps/api/src/openapi.yaml b/apps/api/src/openapi.yaml index b5f23bfd..3c4178c3 100644 --- a/apps/api/src/openapi.yaml +++ b/apps/api/src/openapi.yaml @@ -2424,6 +2424,15 @@ paths: properties: jobId: type: string + downloadUrl: + type: string + description: URL to download all pages as ZIP + originalSize: + type: integer + description: Input PDF size in bytes + processedSize: + type: integer + description: Output ZIP size in bytes pageCount: type: integer description: Total pages in the PDF diff --git a/apps/api/src/routes/tools/pdf-to-image.ts b/apps/api/src/routes/tools/pdf-to-image.ts index 301b4d79..ae25a870 100644 --- a/apps/api/src/routes/tools/pdf-to-image.ts +++ b/apps/api/src/routes/tools/pdf-to-image.ts @@ -396,14 +396,18 @@ export function registerPdfToImageRoute( await zipDone; const zipBuffer = Buffer.concat(zipChunks); await putObject(`outputs/${jobId}/${zipFilename}`, zipBuffer); + const zipUrl = `/api/v1/download/${jobId}/${encodeURIComponent(zipFilename)}`; return reply.send({ jobId, + downloadUrl: zipUrl, + originalSize: fileBuffer.length, + processedSize: zipBuffer.length, pageCount: totalPages, selectedPages, format: settings.format, pages, - zipUrl: `/api/v1/download/${jobId}/${encodeURIComponent(zipFilename)}`, + zipUrl, zipSize: zipBuffer.length, }); } catch (err) { diff --git a/tests/e2e/pdf-to-image.spec.ts b/tests/e2e/pdf-to-image.spec.ts index c7d795bb..3a35c065 100644 --- a/tests/e2e/pdf-to-image.spec.ts +++ b/tests/e2e/pdf-to-image.spec.ts @@ -72,3 +72,20 @@ test.describe("PDF to Image tool", () => { await expect(page.locator("text=3 of 3 pages selected")).toBeVisible(); }); }); + +test.describe("PDF conversion preset pages", () => { + for (const toolId of ["pdf-to-png", "pdf-to-jpg"]) { + test(`${toolId} shows the ZIP download after synchronous conversion`, async ({ + loggedInPage: page, + }) => { + await page.goto(`/pdf/${toolId}`); + await uploadPdf(page); + + await page.getByTestId("preset-submit").click(); + + const download = page.getByTestId("preset-download"); + await expect(download).toBeVisible({ timeout: 15_000 }); + await expect(download).toHaveAttribute("href", /\/api\/v1\/download\/[^/]+\/pdf-pages\.zip$/); + }); + } +}); diff --git a/tests/integration/tools/conversion-presets.test.ts b/tests/integration/tools/conversion-presets.test.ts index 6eb07cb3..f09e1d9b 100644 --- a/tests/integration/tools/conversion-presets.test.ts +++ b/tests/integration/tools/conversion-presets.test.ts @@ -8,6 +8,7 @@ import { type Tool, toolSection, } from "@snapotter/shared"; +import AdmZip from "adm-zip"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { fixtures, readFixture } from "../../fixtures/index.js"; import { @@ -214,3 +215,48 @@ describe("conversion presets (all)", () => { ); } }); + +describe("PDF to image conversion presets", () => { + it.each([ + ["pdf-to-png", "png"], + ["pdf-to-jpg", "jpg"], + ["pdf-to-tiff", "tiff"], + ])("%s returns the standard synchronous result and a downloadable ZIP", async (toolId, ext) => { + const input = readFixture(fixtures.document.tiny("pdf")); + const { body, contentType } = createMultipartPayload([ + { + name: "file", + filename: "input.pdf", + contentType: "application/pdf", + content: input, + }, + { name: "settings", content: JSON.stringify({ dpi: 72 }) }, + ]); + const res = await testApp.app.inject({ + method: "POST", + url: `/api/v1/tools/pdf/${toolId}`, + headers: { authorization: `Bearer ${token}`, "content-type": contentType }, + body, + }); + + expect(res.statusCode, res.body).toBe(200); + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBe(result.zipUrl); + expect(result.originalSize).toBe(input.length); + expect(result.processedSize).toBe(result.zipSize); + + const download = await testApp.app.inject({ + method: "GET", + url: result.downloadUrl, + }); + expect(download.statusCode).toBe(200); + expect(download.headers["content-type"]).toContain("application/zip"); + + const entries = new AdmZip(download.rawPayload).getEntries(); + expect(entries).toHaveLength(result.selectedPages.length); + for (const entry of entries) { + expect(entry.entryName).toMatch(new RegExp(`\\.${ext}$`)); + expect(entry.header.size).toBeGreaterThan(0); + } + }); +});