fix(pdf): restore downloads on PDF conversion preset pages (#629)

Adds downloadUrl/originalSize/processedSize to the pdf-to-image route's synchronous response so PDF conversion presets (pdf-to-png, pdf-to-jpg, pdf-to-tiff) satisfy the standard tool-result contract and show their download action again.

Fixes #623

Co-authored-by: EuanTop <euan@mail.bnu.edu.cn>
This commit is contained in:
Euan
2026-07-24 18:53:19 +08:00
committed by GitHub
parent 301e6eb01a
commit e0a7aecde8
4 changed files with 77 additions and 1 deletions
+9
View File
@@ -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
+5 -1
View File
@@ -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) {
+17
View File
@@ -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$/);
});
}
});
@@ -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);
}
});
});