mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(tools): 2.0 phase 5 wave 2 - pdf depth (21 tools) (#220)
This commit is contained in:
@@ -71,6 +71,13 @@ interface ParsedStep {
|
||||
pool: Pool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tools whose settings carry passwords. They are blocked from ALL pipeline
|
||||
* paths so secrets never persist in step rows (the single-tool route redacts
|
||||
* via dbSettings).
|
||||
*/
|
||||
const PASSWORD_TOOLS = new Set(["protect-pdf", "unlock-pdf"]);
|
||||
|
||||
/**
|
||||
* Build a FlowJob tree for a single-file pipeline.
|
||||
*
|
||||
@@ -313,6 +320,12 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
|
||||
});
|
||||
}
|
||||
|
||||
if (PASSWORD_TOOLS.has(step.toolId)) {
|
||||
return reply.status(400).send({
|
||||
error: `Step ${i + 1}: This tool cannot be used in pipelines because it requires a password`,
|
||||
});
|
||||
}
|
||||
|
||||
const settingsResult = toolConfig.settingsSchema.safeParse(step.settings);
|
||||
if (!settingsResult.success) {
|
||||
return reply.status(400).send({
|
||||
@@ -483,6 +496,11 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
|
||||
|
||||
// Validate all tool IDs exist
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
if (PASSWORD_TOOLS.has(steps[i].toolId)) {
|
||||
return reply.status(400).send({
|
||||
error: `This tool cannot be used in saved pipelines because it requires a password`,
|
||||
});
|
||||
}
|
||||
const toolConfig = getToolConfig(steps[i].toolId);
|
||||
if (!toolConfig) {
|
||||
return reply.status(400).send({
|
||||
@@ -696,6 +714,12 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
|
||||
});
|
||||
}
|
||||
|
||||
if (PASSWORD_TOOLS.has(step.toolId)) {
|
||||
return reply.status(400).send({
|
||||
error: `Step ${i + 1}: This tool cannot be used in pipelines because it requires a password`,
|
||||
});
|
||||
}
|
||||
|
||||
const settingsResult = toolConfig.settingsSchema.safeParse(step.settings);
|
||||
if (!settingsResult.success) {
|
||||
return reply.status(400).send({
|
||||
|
||||
@@ -80,6 +80,20 @@ export interface ToolRouteConfig<T> {
|
||||
) => Promise<{ buffer: Buffer; filename: string; contentType: string }>;
|
||||
/** Optional v2 process function. When set, the worker calls this instead of the legacy process. */
|
||||
processV2?: ToolProcessV2;
|
||||
/**
|
||||
* When set, the factory passes `{ scratchDir, lenient: true }` to the
|
||||
* input handler's prepare(). DocumentInputHandler skips qpdfCheck and
|
||||
* page-cap when lenient, keeping only the %PDF- header check. Set on
|
||||
* tools that intentionally accept damaged inputs (e.g. repair-pdf).
|
||||
*/
|
||||
skipStructuralValidation?: boolean;
|
||||
/**
|
||||
* When set, produces a redacted copy of settings for the durable DB
|
||||
* row. Passwords and other secrets are replaced so they do not persist
|
||||
* in the jobs table (retention keeps rows for days). The BullMQ job
|
||||
* data keeps the real settings; the worker reads from job data.
|
||||
*/
|
||||
redactSettingsForAudit?: (settings: unknown) => Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** Type-erased config stored in the registry (settings type is widened to avoid variance issues). */
|
||||
@@ -94,6 +108,8 @@ export interface AnyToolRouteConfig {
|
||||
ctx?: ToolProcessCtx,
|
||||
) => Promise<{ buffer: Buffer; filename: string; contentType: string }>;
|
||||
processV2?: ToolProcessV2;
|
||||
skipStructuralValidation?: boolean;
|
||||
redactSettingsForAudit?: (settings: unknown) => Record<string, unknown>;
|
||||
}
|
||||
|
||||
// ── Legacy adapter ────────────────────────────────────────────
|
||||
@@ -289,6 +305,7 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
try {
|
||||
const prepared = await inputHandlerFor(modality).prepare(fileBuffer, fname, {
|
||||
scratchDir,
|
||||
lenient: config.skipStructuralValidation,
|
||||
});
|
||||
fileBuffer = prepared.buffer;
|
||||
fname = prepared.filename;
|
||||
@@ -366,6 +383,9 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
const pool = resolveToolPool(config.toolId);
|
||||
|
||||
// Enqueue for the BullMQ worker
|
||||
const dbSettings = config.redactSettingsForAudit
|
||||
? config.redactSettingsForAudit(settings)
|
||||
: undefined;
|
||||
await enqueueToolJob({
|
||||
jobId,
|
||||
toolId: config.toolId,
|
||||
@@ -374,6 +394,7 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
inputRefs,
|
||||
filename,
|
||||
settings,
|
||||
dbSettings,
|
||||
fileId: fileId ?? undefined,
|
||||
clientJobId: clientJobId ?? undefined,
|
||||
kind: "tool",
|
||||
@@ -404,6 +425,7 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
originalSize: result.originalSize,
|
||||
processedSize: result.processedSize,
|
||||
savedFileId: result.savedFileId,
|
||||
...result.resultPayload,
|
||||
});
|
||||
}
|
||||
return reply.status(202).send({ jobId: clientJobId || jobId, async: true });
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import type { BookletValue } from "@snapotter/doc-engine";
|
||||
import { pdfcpuBooklet } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
perSheet: z.union([z.literal(2), z.literal(4), z.literal(6), z.literal(8)]).default(2),
|
||||
});
|
||||
|
||||
export function registerBookletPdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "booklet-pdf",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("booklet-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_booklet.pdf`);
|
||||
ctx.report(10, "Creating booklet");
|
||||
await pdfcpuBooklet(inPath, settings.perSheet as BookletValue, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_booklet.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { pdfcpuCropMargin } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
margin: z.number().min(0).max(2000).default(20),
|
||||
});
|
||||
|
||||
export function registerCropPdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "crop-pdf",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("crop-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_cropped.pdf`);
|
||||
ctx.report(10, "Cropping");
|
||||
await pdfcpuCropMargin(inPath, settings.margin, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_cropped.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { qpdfPagesSpec } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const rangeField = z
|
||||
.string()
|
||||
.max(200)
|
||||
.regex(/^[0-9rz][0-9rz,-]*$/i, "Invalid page range");
|
||||
|
||||
const settingsSchema = z.object({
|
||||
range: rangeField,
|
||||
});
|
||||
|
||||
export function registerExtractPages(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "extract-pages",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("extract-pages is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_extracted.pdf`);
|
||||
ctx.report(10, "Extracting pages");
|
||||
await qpdfPagesSpec(inPath, settings.range, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_extracted.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { pdfFlattenPy } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({});
|
||||
|
||||
export function registerFlattenPdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "flatten-pdf",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("flatten-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_flattened.pdf`);
|
||||
ctx.report(10, "Flattening");
|
||||
await pdfFlattenPy(inPath, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_flattened.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { gsGrayscalePdf } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({});
|
||||
|
||||
export function registerGrayscalePdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "grayscale-pdf",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("grayscale-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_grayscale.pdf`);
|
||||
ctx.report(10, "Converting to grayscale");
|
||||
await gsGrayscalePdf(inPath, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_grayscale.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { htmlToPdfPy } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({});
|
||||
|
||||
export function registerHtmlToPdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "html-to-pdf",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("html-to-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}.pdf`);
|
||||
ctx.report(10, "Converting");
|
||||
await htmlToPdfPy(inPath, outPath, "html");
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { registerAiCanvasExpand } from "./ai-canvas-expand.js";
|
||||
import { registerBarcodeRead } from "./barcode-read.js";
|
||||
import { registerBeautify } from "./beautify.js";
|
||||
import { registerBlurFaces } from "./blur-faces.js";
|
||||
import { registerBookletPdf } from "./booklet-pdf.js";
|
||||
import { registerBorder } from "./border.js";
|
||||
import { registerBulkRename } from "./bulk-rename.js";
|
||||
import { registerCollage } from "./collage.js";
|
||||
@@ -23,32 +24,50 @@ import { registerConvert } from "./convert.js";
|
||||
import { registerConvertAudio } from "./convert-audio.js";
|
||||
import { registerConvertVideo } from "./convert-video.js";
|
||||
import { registerCrop } from "./crop.js";
|
||||
import { registerCropPdf } from "./crop-pdf.js";
|
||||
import { registerCsvExcel } from "./csv-excel.js";
|
||||
import { registerCsvJson } from "./csv-json.js";
|
||||
import { registerEditMetadata } from "./edit-metadata.js";
|
||||
import { registerEnhanceFaces } from "./enhance-faces.js";
|
||||
import { registerEraseObject } from "./erase-object.js";
|
||||
import { registerExtractAudio } from "./extract-audio.js";
|
||||
import { registerExtractPages } from "./extract-pages.js";
|
||||
import { registerFavicon } from "./favicon.js";
|
||||
import { registerFindDuplicates } from "./find-duplicates.js";
|
||||
import { registerFlattenPdf } from "./flatten-pdf.js";
|
||||
import { registerGifTools } from "./gif-tools.js";
|
||||
import { registerGrayscalePdf } from "./grayscale-pdf.js";
|
||||
import { registerHtmlToImage } from "./html-to-image.js";
|
||||
import { registerHtmlToPdf } from "./html-to-pdf.js";
|
||||
import { registerImageEnhancement } from "./image-enhancement.js";
|
||||
import { registerImageToBase64 } from "./image-to-base64.js";
|
||||
import { registerImageToPdf } from "./image-to-pdf.js";
|
||||
import { registerInfo } from "./info.js";
|
||||
import { registerJsonXml } from "./json-xml.js";
|
||||
import { registerLinearizePdf } from "./linearize-pdf.js";
|
||||
import { registerMarkdownToPdf } from "./markdown-to-pdf.js";
|
||||
import { registerMemeGenerator } from "./meme-generator.js";
|
||||
import { registerMergePdf } from "./merge-pdf.js";
|
||||
import { registerMuteVideo } from "./mute-video.js";
|
||||
import { registerNoiseRemoval } from "./noise-removal.js";
|
||||
import { registerNupPdf } from "./nup-pdf.js";
|
||||
import { registerOcr } from "./ocr.js";
|
||||
import { registerOptimizeForWeb } from "./optimize-for-web.js";
|
||||
import { registerOrganizePdf } from "./organize-pdf.js";
|
||||
import { registerPassportPhoto } from "./passport-photo.js";
|
||||
import { registerPdfMetadata } from "./pdf-metadata.js";
|
||||
import { registerPdfPageNumbers } from "./pdf-page-numbers.js";
|
||||
import { registerPdfToImage } from "./pdf-to-image.js";
|
||||
import { registerPdfToText } from "./pdf-to-text.js";
|
||||
import { registerPdfToWord } from "./pdf-to-word.js";
|
||||
import { registerPdfaConvert } from "./pdfa-convert.js";
|
||||
import { registerProtectPdf } from "./protect-pdf.js";
|
||||
import { registerQrGenerate } from "./qr-generate.js";
|
||||
import { registerRedEyeRemoval } from "./red-eye-removal.js";
|
||||
import { registerRedactPdf } from "./redact-pdf.js";
|
||||
import { registerRemoveBackground } from "./remove-background.js";
|
||||
import { registerRemovePages } from "./remove-pages.js";
|
||||
import { registerRepairPdf } from "./repair-pdf.js";
|
||||
import { registerReplaceColor } from "./replace-color.js";
|
||||
import { registerResize } from "./resize.js";
|
||||
import { registerRestorePhoto } from "./restore-photo.js";
|
||||
@@ -66,10 +85,12 @@ import { registerTextOverlay } from "./text-overlay.js";
|
||||
import { registerTransparencyFixer } from "./transparency-fixer.js";
|
||||
import { registerTrimAudio } from "./trim-audio.js";
|
||||
import { registerTrimVideo } from "./trim-video.js";
|
||||
import { registerUnlockPdf } from "./unlock-pdf.js";
|
||||
import { registerUpscale } from "./upscale.js";
|
||||
import { registerVectorize } from "./vectorize.js";
|
||||
import { registerVideoToGif } from "./video-to-gif.js";
|
||||
import { registerWatermarkImage } from "./watermark-image.js";
|
||||
import { registerWatermarkPdf } from "./watermark-pdf.js";
|
||||
import { registerWatermarkText } from "./watermark-text.js";
|
||||
import { registerWordToPdf } from "./word-to-pdf.js";
|
||||
|
||||
@@ -174,6 +195,27 @@ export async function registerToolRoutes(app: FastifyInstance): Promise<void> {
|
||||
{ id: "compress-pdf", register: registerCompressPdf },
|
||||
{ id: "rotate-pdf", register: registerRotatePdf },
|
||||
{ id: "word-to-pdf", register: registerWordToPdf },
|
||||
{ id: "extract-pages", register: registerExtractPages },
|
||||
{ id: "remove-pages", register: registerRemovePages },
|
||||
{ id: "organize-pdf", register: registerOrganizePdf },
|
||||
{ id: "protect-pdf", register: registerProtectPdf },
|
||||
{ id: "unlock-pdf", register: registerUnlockPdf },
|
||||
{ id: "repair-pdf", register: registerRepairPdf },
|
||||
{ id: "linearize-pdf", register: registerLinearizePdf },
|
||||
{ id: "grayscale-pdf", register: registerGrayscalePdf },
|
||||
{ id: "pdfa-convert", register: registerPdfaConvert },
|
||||
{ id: "crop-pdf", register: registerCropPdf },
|
||||
{ id: "nup-pdf", register: registerNupPdf },
|
||||
{ id: "booklet-pdf", register: registerBookletPdf },
|
||||
{ id: "watermark-pdf", register: registerWatermarkPdf },
|
||||
{ id: "pdf-page-numbers", register: registerPdfPageNumbers },
|
||||
{ id: "flatten-pdf", register: registerFlattenPdf },
|
||||
{ id: "redact-pdf", register: registerRedactPdf },
|
||||
{ id: "pdf-to-text", register: registerPdfToText },
|
||||
{ id: "pdf-to-word", register: registerPdfToWord },
|
||||
{ id: "pdf-metadata", register: registerPdfMetadata },
|
||||
{ id: "html-to-pdf", register: registerHtmlToPdf },
|
||||
{ id: "markdown-to-pdf", register: registerMarkdownToPdf },
|
||||
|
||||
// Data Files
|
||||
{ id: "csv-excel", register: registerCsvExcel },
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { qpdfLinearize } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({});
|
||||
|
||||
export function registerLinearizePdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "linearize-pdf",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("linearize-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_linearized.pdf`);
|
||||
ctx.report(10, "Linearizing");
|
||||
await qpdfLinearize(inPath, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_linearized.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { htmlToPdfPy } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({});
|
||||
|
||||
export function registerMarkdownToPdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "markdown-to-pdf",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("markdown-to-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}.pdf`);
|
||||
ctx.report(10, "Converting");
|
||||
await htmlToPdfPy(inPath, outPath, "markdown");
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import type { NupValue } from "@snapotter/doc-engine";
|
||||
import { pdfcpuNup } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
perSheet: z
|
||||
.union([
|
||||
z.literal(2),
|
||||
z.literal(3),
|
||||
z.literal(4),
|
||||
z.literal(8),
|
||||
z.literal(9),
|
||||
z.literal(12),
|
||||
z.literal(16),
|
||||
])
|
||||
.default(2),
|
||||
});
|
||||
|
||||
export function registerNupPdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "nup-pdf",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("nup-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_nup.pdf`);
|
||||
ctx.report(10, "Arranging pages");
|
||||
await pdfcpuNup(inPath, settings.perSheet as NupValue, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_nup.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { qpdfPagesSpec } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const rangeField = z
|
||||
.string()
|
||||
.max(200)
|
||||
.regex(/^[0-9rz][0-9rz,-]*$/i, "Invalid page range");
|
||||
|
||||
const settingsSchema = z.object({
|
||||
order: rangeField,
|
||||
});
|
||||
|
||||
export function registerOrganizePdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "organize-pdf",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("organize-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_organized.pdf`);
|
||||
ctx.report(10, "Reordering pages");
|
||||
await qpdfPagesSpec(inPath, settings.order, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_organized.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { pdfMetadataGetPy, pdfMetadataSetPy } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
title: z.string().max(500).optional(),
|
||||
author: z.string().max(500).optional(),
|
||||
subject: z.string().max(500).optional(),
|
||||
keywords: z.string().max(500).optional(),
|
||||
});
|
||||
|
||||
export function registerPdfMetadata(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "pdf-metadata",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("pdf-metadata is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_metadata.pdf`);
|
||||
ctx.report(10, "Setting metadata");
|
||||
|
||||
// Build metadata record from DEFINED keys only (empty string = clear).
|
||||
const meta: Record<string, string> = {};
|
||||
if (settings.title !== undefined) meta.Title = settings.title;
|
||||
if (settings.author !== undefined) meta.Author = settings.author;
|
||||
if (settings.subject !== undefined) meta.Subject = settings.subject;
|
||||
if (settings.keywords !== undefined) meta.Keywords = settings.keywords;
|
||||
|
||||
await pdfMetadataSetPy(inPath, outPath, meta);
|
||||
const metadata = await pdfMetadataGetPy(outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_metadata.pdf`,
|
||||
contentType: "application/pdf",
|
||||
resultPayload: { metadata },
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { pdfcpuTextStamp } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
position: z.enum(["bl", "bc", "br", "tl", "tc", "tr"]).default("bc"),
|
||||
fontSize: z.number().int().min(6).max(24).default(10),
|
||||
});
|
||||
|
||||
export function registerPdfPageNumbers(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "pdf-page-numbers",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("pdf-page-numbers is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_numbered.pdf`);
|
||||
ctx.report(10, "Adding page numbers");
|
||||
await pdfcpuTextStamp(
|
||||
inPath,
|
||||
{
|
||||
text: "Page %p of %P",
|
||||
position: settings.position,
|
||||
fontSize: settings.fontSize,
|
||||
opacity: 1,
|
||||
rotation: 0,
|
||||
},
|
||||
outPath,
|
||||
);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_numbered.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { pdfTextPy } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({});
|
||||
|
||||
export function registerPdfToText(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "pdf-to-text",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("pdf-to-text is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}.txt`);
|
||||
ctx.report(10, "Extracting text");
|
||||
await pdfTextPy(inPath, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}.txt`,
|
||||
contentType: "text/plain",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { pdfToWordPy } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({});
|
||||
|
||||
export function registerPdfToWord(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "pdf-to-word",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("pdf-to-word is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}.docx`);
|
||||
ctx.report(10, "Converting");
|
||||
await pdfToWordPy(inPath, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}.docx`,
|
||||
contentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { gsPdfaConvert } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({});
|
||||
|
||||
export function registerPdfaConvert(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "pdfa-convert",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("pdfa-convert is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_pdfa.pdf`);
|
||||
ctx.report(10, "Converting to PDF/A");
|
||||
await gsPdfaConvert(inPath, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_pdfa.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { qpdfEncrypt } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
userPassword: z.string().min(1).max(256),
|
||||
ownerPassword: z.string().min(1).max(256).optional(),
|
||||
});
|
||||
|
||||
export function registerProtectPdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "protect-pdf",
|
||||
settingsSchema,
|
||||
redactSettingsForAudit: (settings) => {
|
||||
const s = settings as z.infer<typeof settingsSchema>;
|
||||
return {
|
||||
...s,
|
||||
userPassword: "<redacted>",
|
||||
ownerPassword: "<redacted>",
|
||||
};
|
||||
},
|
||||
process: async () => {
|
||||
throw new Error("protect-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_protected.pdf`);
|
||||
ctx.report(10, "Encrypting");
|
||||
await qpdfEncrypt(
|
||||
inPath,
|
||||
settings.userPassword,
|
||||
settings.ownerPassword ?? settings.userPassword,
|
||||
outPath,
|
||||
);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_protected.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { pdfRedactPy } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
terms: z.array(z.string().min(1).max(200)).min(1).max(50),
|
||||
caseSensitive: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export function registerRedactPdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "redact-pdf",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("redact-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_redacted.pdf`);
|
||||
ctx.report(10, "Redacting");
|
||||
const { found } = await pdfRedactPy(inPath, outPath, settings.terms, settings.caseSensitive);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_redacted.pdf`,
|
||||
contentType: "application/pdf",
|
||||
resultPayload: { found },
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { qpdfPageCount, qpdfPagesSpecUnchecked } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { compressPageRuns, parsePageSpec } from "../../lib/page-spec.js";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const rangeField = z
|
||||
.string()
|
||||
.max(200)
|
||||
.regex(/^[0-9rz][0-9rz,-]*$/i, "Invalid page range");
|
||||
|
||||
const settingsSchema = z.object({
|
||||
pages: rangeField,
|
||||
});
|
||||
|
||||
export function registerRemovePages(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "remove-pages",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("remove-pages is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
ctx.report(10, "Analyzing pages");
|
||||
const total = await qpdfPageCount(inPath);
|
||||
const removeSet = parsePageSpec(settings.pages, total);
|
||||
|
||||
// Build the keep list: all pages NOT in the remove set
|
||||
const keepPages: number[] = [];
|
||||
for (let i = 1; i <= total; i++) {
|
||||
if (!removeSet.has(i)) {
|
||||
keepPages.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (keepPages.length === 0) {
|
||||
throw new Error("Cannot remove every page from the document");
|
||||
}
|
||||
|
||||
const keepSpec = compressPageRuns(keepPages);
|
||||
const outPath = join(ctx.scratchDir, `${base}_removed.pdf`);
|
||||
ctx.report(30, "Removing pages");
|
||||
await qpdfPagesSpecUnchecked(inPath, keepSpec, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_removed.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { qpdfRepair } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({});
|
||||
|
||||
export function registerRepairPdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "repair-pdf",
|
||||
settingsSchema,
|
||||
skipStructuralValidation: true,
|
||||
process: async () => {
|
||||
throw new Error("repair-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_repaired.pdf`);
|
||||
ctx.report(10, "Repairing");
|
||||
await qpdfRepair(inPath, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_repaired.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { qpdfDecrypt } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
password: z.string().min(1).max(256),
|
||||
});
|
||||
|
||||
export function registerUnlockPdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "unlock-pdf",
|
||||
settingsSchema,
|
||||
redactSettingsForAudit: (settings) => {
|
||||
const s = settings as z.infer<typeof settingsSchema>;
|
||||
return {
|
||||
...s,
|
||||
password: "<redacted>",
|
||||
};
|
||||
},
|
||||
process: async () => {
|
||||
throw new Error("unlock-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_unlocked.pdf`);
|
||||
ctx.report(10, "Decrypting");
|
||||
await qpdfDecrypt(inPath, settings.password, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_unlocked.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { pdfcpuTextStamp } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
text: z.string().min(1).max(200),
|
||||
position: z.enum(["tl", "tc", "tr", "l", "c", "r", "bl", "bc", "br"]).default("c"),
|
||||
fontSize: z.number().int().min(6).max(72).default(48),
|
||||
opacity: z.number().min(0.05).max(1).default(0.3),
|
||||
rotation: z.number().min(-180).max(180).default(45),
|
||||
});
|
||||
|
||||
export function registerWatermarkPdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "watermark-pdf",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("watermark-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_watermarked.pdf`);
|
||||
ctx.report(10, "Stamping watermark");
|
||||
await pdfcpuTextStamp(
|
||||
inPath,
|
||||
{
|
||||
text: settings.text,
|
||||
position: settings.position,
|
||||
fontSize: settings.fontSize,
|
||||
opacity: settings.opacity,
|
||||
rotation: settings.rotation,
|
||||
},
|
||||
outPath,
|
||||
);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_watermarked.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user