mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
@@ -2424,6 +2424,15 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
jobId:
|
jobId:
|
||||||
type: string
|
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:
|
pageCount:
|
||||||
type: integer
|
type: integer
|
||||||
description: Total pages in the PDF
|
description: Total pages in the PDF
|
||||||
|
|||||||
@@ -396,14 +396,18 @@ export function registerPdfToImageRoute(
|
|||||||
await zipDone;
|
await zipDone;
|
||||||
const zipBuffer = Buffer.concat(zipChunks);
|
const zipBuffer = Buffer.concat(zipChunks);
|
||||||
await putObject(`outputs/${jobId}/${zipFilename}`, zipBuffer);
|
await putObject(`outputs/${jobId}/${zipFilename}`, zipBuffer);
|
||||||
|
const zipUrl = `/api/v1/download/${jobId}/${encodeURIComponent(zipFilename)}`;
|
||||||
|
|
||||||
return reply.send({
|
return reply.send({
|
||||||
jobId,
|
jobId,
|
||||||
|
downloadUrl: zipUrl,
|
||||||
|
originalSize: fileBuffer.length,
|
||||||
|
processedSize: zipBuffer.length,
|
||||||
pageCount: totalPages,
|
pageCount: totalPages,
|
||||||
selectedPages,
|
selectedPages,
|
||||||
format: settings.format,
|
format: settings.format,
|
||||||
pages,
|
pages,
|
||||||
zipUrl: `/api/v1/download/${jobId}/${encodeURIComponent(zipFilename)}`,
|
zipUrl,
|
||||||
zipSize: zipBuffer.length,
|
zipSize: zipBuffer.length,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -72,3 +72,20 @@ test.describe("PDF to Image tool", () => {
|
|||||||
await expect(page.locator("text=3 of 3 pages selected")).toBeVisible();
|
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,
|
type Tool,
|
||||||
toolSection,
|
toolSection,
|
||||||
} from "@snapotter/shared";
|
} from "@snapotter/shared";
|
||||||
|
import AdmZip from "adm-zip";
|
||||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||||
import { fixtures, readFixture } from "../../fixtures/index.js";
|
import { fixtures, readFixture } from "../../fixtures/index.js";
|
||||||
import {
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user