mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
refactor: improve tool processing, dropzone, seam carving, and format encoding
- Refactor use-tool-processor and use-pipeline-processor hooks - Enhance dropzone component with improved UX - Improve seam carving with better error handling and tests - Add JXL format encoding support to format-encoders - Update tool routes for consistent format handling - Add dropzone unit tests
This commit is contained in:
@@ -103,3 +103,31 @@ export async function encodeQoi(inputBuffer: Buffer): Promise<Buffer> {
|
||||
const encoded = qoiEncode(new Uint8Array(data), info.width, info.height, 4);
|
||||
return Buffer.from(encoded);
|
||||
}
|
||||
|
||||
export async function encodeJxl(inputBuffer: Buffer, quality?: number): Promise<Buffer> {
|
||||
const id = randomUUID();
|
||||
const inputPath = join(tmpdir(), `jxl-enc-in-${id}.png`);
|
||||
const outputPath = join(tmpdir(), `jxl-enc-out-${id}.jxl`);
|
||||
try {
|
||||
const pngBuffer = await sharp(inputBuffer).png().toBuffer();
|
||||
await writeFile(inputPath, pngBuffer);
|
||||
try {
|
||||
const q = String(quality ?? 75);
|
||||
await execFileAsync("cjxl", [inputPath, outputPath, "-q", q], {
|
||||
timeout: 120_000,
|
||||
});
|
||||
return await readFile(outputPath);
|
||||
} catch {
|
||||
/* cjxl not available, fall back to ImageMagick */
|
||||
}
|
||||
const cmd = await findMagickCmd();
|
||||
const q = quality ? ["-quality", String(quality)] : [];
|
||||
await execFileAsync(cmd, magickArgs(cmd, [inputPath, ...q, `jxl:${outputPath}`]), {
|
||||
timeout: 120_000,
|
||||
});
|
||||
return await readFile(outputPath);
|
||||
} finally {
|
||||
await rm(inputPath, { force: true }).catch(() => {});
|
||||
await rm(outputPath, { force: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import { isSvgBuffer, sanitizeSvg } from "../lib/svg-sanitize.js";
|
||||
import { createWorkspace } from "../lib/workspace.js";
|
||||
import { hasEffectivePermission } from "../permissions.js";
|
||||
import { requireAuth } from "../plugins/auth.js";
|
||||
import { type JobProgress, updateJobProgress } from "./progress.js";
|
||||
import { type JobProgress, updateJobProgress, updateSingleFileProgress } from "./progress.js";
|
||||
import { getRegisteredToolIds, getToolConfig } from "./tool-factory.js";
|
||||
|
||||
/** Schema for a single pipeline step. */
|
||||
@@ -75,6 +75,7 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
|
||||
let fileBuffer: Buffer | null = null;
|
||||
let filename = "image";
|
||||
let pipelineRaw: string | null = null;
|
||||
let clientJobId: string | null = null;
|
||||
|
||||
// Parse multipart
|
||||
try {
|
||||
@@ -89,6 +90,8 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
|
||||
filename = sanitizeFilename(part.filename ?? "image");
|
||||
} else if (part.fieldname === "pipeline") {
|
||||
pipelineRaw = part.value as string;
|
||||
} else if (part.fieldname === "clientJobId") {
|
||||
clientJobId = part.value as string;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -215,10 +218,23 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
|
||||
let currentBuffer = fileBuffer;
|
||||
let currentFilename = filename;
|
||||
const stepResults: Array<{ step: number; toolId: string; size: number }> = [];
|
||||
const totalSteps = pipeline.steps.length;
|
||||
|
||||
const reportProgress = (percent: number, stage?: string) => {
|
||||
if (!clientJobId) return;
|
||||
updateSingleFileProgress({
|
||||
jobId: clientJobId,
|
||||
phase: "processing",
|
||||
percent,
|
||||
stage,
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
for (let i = 0; i < pipeline.steps.length; i++) {
|
||||
for (let i = 0; i < totalSteps; i++) {
|
||||
const step = pipeline.steps[i];
|
||||
const stepPercent = Math.round((i / totalSteps) * 90);
|
||||
reportProgress(stepPercent, `Step ${i + 1}/${totalSteps}: ${step.toolId}`);
|
||||
|
||||
// Route content-aware resize to its dedicated tool
|
||||
const resolvedToolId =
|
||||
@@ -250,6 +266,8 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
|
||||
throw new Error(`Step ${i + 1} (${step.toolId}): ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
reportProgress(95, "Saving...");
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Pipeline processing failed";
|
||||
trackEvent(request, ANALYTICS_EVENTS.PIPELINE_EXECUTED, {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { decompressSvgz, sanitizeSvg } from "../lib/svg-sanitize.js";
|
||||
import { computeTimeout } from "../lib/timeout.js";
|
||||
import { getWorkerPool } from "../lib/worker-pool.js";
|
||||
import { createWorkspace } from "../lib/workspace.js";
|
||||
import { updateSingleFileProgress } from "./progress.js";
|
||||
|
||||
export interface ToolRouteConfig<T> {
|
||||
/** Unique tool identifier, used as the URL path segment. */
|
||||
@@ -112,6 +113,7 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
let filename = "image";
|
||||
let settingsRaw: string | null = null;
|
||||
let fileId: string | null = null;
|
||||
let clientJobId: string | null = null;
|
||||
let fileCount = 0;
|
||||
|
||||
// Parse multipart parts
|
||||
@@ -143,6 +145,9 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
if (part.fieldname === "fileId") {
|
||||
fileId = part.value as string;
|
||||
}
|
||||
if (part.fieldname === "clientJobId") {
|
||||
clientJobId = part.value as string;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -167,6 +172,18 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
// mutates fileBuffer into a larger intermediate PNG.
|
||||
const uploadedSize = fileBuffer.length;
|
||||
|
||||
const reportProgress = (percent: number, stage?: string) => {
|
||||
if (!clientJobId) return;
|
||||
updateSingleFileProgress({
|
||||
jobId: clientJobId,
|
||||
phase: "processing",
|
||||
percent,
|
||||
stage,
|
||||
});
|
||||
};
|
||||
|
||||
reportProgress(5, "Validating...");
|
||||
|
||||
// Validate the uploaded image
|
||||
const validation = await validateImageBuffer(fileBuffer, filename);
|
||||
if (!validation.valid) {
|
||||
@@ -178,6 +195,7 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
// The decoded buffer is PNG, so update the filename extension to match.
|
||||
const isHeif = validation.format === "heif";
|
||||
if (isHeif) {
|
||||
reportProgress(10, "Decoding HEIC...");
|
||||
try {
|
||||
fileBuffer = await decodeHeic(fileBuffer);
|
||||
const ext = filename.match(/\.[^.]+$/)?.[0];
|
||||
@@ -195,6 +213,7 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
// Pass the original file extension so RAW decoder can use the correct
|
||||
// temp file suffix (e.g. .cr3, .nef) for format identification.
|
||||
if (needsCliDecode(validation.format)) {
|
||||
reportProgress(10, "Decoding...");
|
||||
try {
|
||||
const fileExt = filename.split(".").pop()?.toLowerCase();
|
||||
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format, fileExt);
|
||||
@@ -225,6 +244,8 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
}
|
||||
}
|
||||
|
||||
reportProgress(15, "Preparing...");
|
||||
|
||||
// Parse and validate settings
|
||||
let settings: T;
|
||||
try {
|
||||
@@ -259,6 +280,8 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
try {
|
||||
let result: { buffer: Buffer; filename: string; contentType: string };
|
||||
|
||||
reportProgress(20, "Processing...");
|
||||
|
||||
// Offload to worker thread for non-AI tools.
|
||||
// Falls back to main-thread processing on any worker error.
|
||||
// Disabled in test environments where worker_threads can't load .ts files.
|
||||
@@ -299,6 +322,8 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
result = await config.process(processBuffer, settings, filename);
|
||||
}
|
||||
|
||||
reportProgress(75, "Saving...");
|
||||
|
||||
// Add a tool-specific suffix to the filename so the download
|
||||
// doesn't silently overwrite the user's original file.
|
||||
// Skip if the tool already changed the filename (e.g. convert, split).
|
||||
@@ -362,6 +387,7 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
]);
|
||||
let previewUrl: string | undefined;
|
||||
if (!BROWSER_PREVIEWABLE.has(result.contentType)) {
|
||||
reportProgress(85, "Generating preview...");
|
||||
try {
|
||||
let previewInput = result.buffer;
|
||||
// Sharp can't decode HEIC - use system decoder first
|
||||
@@ -372,8 +398,21 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
const previewPath = join(workspacePath, "output", "preview.webp");
|
||||
await writeFile(previewPath, previewBuffer);
|
||||
previewUrl = `/api/v1/download/${jobId}/preview.webp`;
|
||||
} catch {
|
||||
// Non-fatal - frontend will show the success card fallback
|
||||
} catch (previewErr) {
|
||||
request.log.warn(
|
||||
{ previewErr, contentType: result.contentType, toolId: config.toolId },
|
||||
"Failed to generate preview thumbnail, falling back to input buffer",
|
||||
);
|
||||
// Retry with the original input buffer (pre-processing) which
|
||||
// was already validated and decoded during the intake phase.
|
||||
try {
|
||||
const fallbackBuffer = await sharp(fileBuffer).webp({ quality: 80 }).toBuffer();
|
||||
const previewPath = join(workspacePath, "output", "preview.webp");
|
||||
await writeFile(previewPath, fallbackBuffer);
|
||||
previewUrl = `/api/v1/download/${jobId}/preview.webp`;
|
||||
} catch {
|
||||
// Both attempts failed - frontend will use the upload preview as fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,6 +420,8 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
const inputPath = join(workspacePath, "input", filename);
|
||||
await writeFile(inputPath, fileBuffer);
|
||||
|
||||
reportProgress(95, "Finishing...");
|
||||
|
||||
// Auto-save to persistent file store when a fileId is provided
|
||||
let savedFileId: string | undefined;
|
||||
if (fileId) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { encodeJxl } from "../../lib/format-encoders.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
|
||||
@@ -643,7 +644,7 @@ export function registerCollage(app: FastifyInstance) {
|
||||
outputExt = "avif";
|
||||
break;
|
||||
case "jxl":
|
||||
pipeline = pipeline.jxl({ quality: settings.quality });
|
||||
pipeline = pipeline.png();
|
||||
outputExt = "jxl";
|
||||
break;
|
||||
default:
|
||||
@@ -653,18 +654,19 @@ export function registerCollage(app: FastifyInstance) {
|
||||
}
|
||||
|
||||
const result = await pipeline.toBuffer();
|
||||
const finalBuffer = outputExt === "jxl" ? await encodeJxl(result, settings.quality) : result;
|
||||
|
||||
const jobId = randomUUID();
|
||||
const workspacePath = await createWorkspace(jobId);
|
||||
const filename = `collage.${outputExt}`;
|
||||
const outputPath = join(workspacePath, "output", filename);
|
||||
await writeFile(outputPath, result);
|
||||
await writeFile(outputPath, finalBuffer);
|
||||
|
||||
return reply.send({
|
||||
jobId,
|
||||
downloadUrl: `/api/v1/download/${jobId}/${filename}`,
|
||||
originalSize: files.reduce((s, f) => s + f.buffer.length, 0),
|
||||
processedSize: result.length,
|
||||
processedSize: finalBuffer.length,
|
||||
});
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
|
||||
@@ -8,7 +8,13 @@ import { convert } from "@snapotter/image-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { encodeBmp, encodeIco, encodeJp2, encodeQoi } from "../../lib/format-encoders.js";
|
||||
import {
|
||||
encodeBmp,
|
||||
encodeIco,
|
||||
encodeJp2,
|
||||
encodeJxl,
|
||||
encodeQoi,
|
||||
} from "../../lib/format-encoders.js";
|
||||
import { encodeHeic } from "../../lib/heic-converter.js";
|
||||
import { isSvgBuffer } from "../../lib/svg-sanitize.js";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
@@ -56,6 +62,7 @@ const CLI_ENCODERS: Record<string, (buf: Buffer, quality?: number) => Promise<Bu
|
||||
bmp: encodeBmp,
|
||||
ico: encodeIco,
|
||||
jp2: encodeJp2,
|
||||
jxl: encodeJxl,
|
||||
qoi: encodeQoi,
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { isToolInstalled } from "../../lib/feature-status.js";
|
||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { decodeToSharpCompat, needsCliDecode } from "../../lib/format-decoders.js";
|
||||
import { encodeJxl } from "../../lib/format-encoders.js";
|
||||
import { decodeHeic, encodeHeic } from "../../lib/heic-converter.js";
|
||||
import { resolveOutputFormat } from "../../lib/output-format.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
@@ -213,7 +214,7 @@ export function registerEraseObject(app: FastifyInstance) {
|
||||
outputBuffer = await encodeHeic(resultBuffer, quality);
|
||||
finalFormat = format;
|
||||
} else if (format === "jxl") {
|
||||
outputBuffer = await sharp(resultBuffer).jxl({ quality }).toBuffer();
|
||||
outputBuffer = await encodeJxl(resultBuffer, quality);
|
||||
finalFormat = "jxl";
|
||||
} else {
|
||||
outputBuffer = await sharp(resultBuffer).avif({ quality }).toBuffer();
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { encodeJxl } from "../../lib/format-encoders.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
@@ -145,10 +146,12 @@ export function registerImageToBase64(app: FastifyInstance) {
|
||||
outputBuffer = await pipeline.avif({ quality: opts.quality, effort: 4 }).toBuffer();
|
||||
mimeType = "image/avif";
|
||||
break;
|
||||
case "jxl":
|
||||
outputBuffer = await pipeline.jxl({ quality: opts.quality }).toBuffer();
|
||||
case "jxl": {
|
||||
const pngBuf = await pipeline.png().toBuffer();
|
||||
outputBuffer = await encodeJxl(pngBuf, opts.quality);
|
||||
mimeType = "image/jxl";
|
||||
break;
|
||||
}
|
||||
default:
|
||||
outputBuffer = await pipeline.toBuffer();
|
||||
mimeType = detectMimeType(ext);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { decodeToSharpCompat, needsCliDecode } from "../../lib/format-decoders.js";
|
||||
import { encodeJxl } from "../../lib/format-encoders.js";
|
||||
import { decodeHeic } from "../../lib/heic-converter.js";
|
||||
import { sanitizeSvg } from "../../lib/svg-sanitize.js";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
@@ -40,9 +41,16 @@ const settingsSchema = z.object({
|
||||
type Settings = z.infer<typeof settingsSchema>;
|
||||
|
||||
async function processImage(inputBuffer: Buffer, settings: Settings, filename: string) {
|
||||
const isJxl = settings.format === "jxl";
|
||||
const engineSettings = isJxl ? { ...settings, format: "png" as const } : settings;
|
||||
|
||||
const image = sharp(inputBuffer);
|
||||
const result = await optimizeForWeb(image, settings);
|
||||
const buffer = await result.toBuffer();
|
||||
const result = await optimizeForWeb(image, engineSettings);
|
||||
let buffer = await result.toBuffer();
|
||||
|
||||
if (isJxl) {
|
||||
buffer = await encodeJxl(buffer, settings.quality);
|
||||
}
|
||||
|
||||
const ext = extname(filename);
|
||||
const baseName = ext ? filename.slice(0, -ext.length) : filename;
|
||||
|
||||
@@ -9,6 +9,7 @@ import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { env } from "../../config.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { encodeJxl } from "../../lib/format-encoders.js";
|
||||
import { encodeHeic } from "../../lib/heic-converter.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
|
||||
@@ -117,8 +118,10 @@ async function convertWithSharp(
|
||||
return s.tiff().toBuffer();
|
||||
case "gif":
|
||||
return s.gif().toBuffer();
|
||||
case "jxl":
|
||||
return s.jxl({ quality }).toBuffer();
|
||||
case "jxl": {
|
||||
const pngBuf = await s.png().toBuffer();
|
||||
return encodeJxl(pngBuf, quality);
|
||||
}
|
||||
case "heic":
|
||||
case "heif": {
|
||||
const pngBuf = await s.png().toBuffer();
|
||||
|
||||
@@ -7,6 +7,7 @@ import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { encodeJxl } from "../../lib/format-encoders.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { registerToolProcessFn } from "../tool-factory.js";
|
||||
|
||||
@@ -31,7 +32,7 @@ function resolveOutputFormat(
|
||||
jpg: { sharpFormat: "jpeg", ext: ".jpg" },
|
||||
webp: { sharpFormat: "webp", ext: ".webp" },
|
||||
avif: { sharpFormat: "avif", ext: ".avif" },
|
||||
jxl: { sharpFormat: "jxl", ext: ".jxl" },
|
||||
jxl: { sharpFormat: "png", ext: ".jxl" },
|
||||
};
|
||||
return map[outputFormat] ?? { sharpFormat: null, ext: originalExt };
|
||||
}
|
||||
@@ -150,7 +151,11 @@ export function registerSplit(app: FastifyInstance) {
|
||||
}
|
||||
|
||||
const partBuffer = await pipeline.toBuffer();
|
||||
archive.append(partBuffer, {
|
||||
const finalBuffer =
|
||||
settings.outputFormat === "jxl"
|
||||
? await encodeJxl(partBuffer, settings.quality)
|
||||
: partBuffer;
|
||||
archive.append(finalBuffer, {
|
||||
name: `${baseName}_r${row + 1}_c${col + 1}${outputExt}`,
|
||||
});
|
||||
}
|
||||
@@ -236,7 +241,11 @@ export function registerSplit(app: FastifyInstance) {
|
||||
}
|
||||
|
||||
const partBuffer = await pipeline.toBuffer();
|
||||
archive.append(partBuffer, {
|
||||
const finalBuffer =
|
||||
settings.outputFormat === "jxl"
|
||||
? await encodeJxl(partBuffer, settings.quality)
|
||||
: partBuffer;
|
||||
archive.append(finalBuffer, {
|
||||
name: `${baseName}_r${row + 1}_c${col + 1}${outputExt}`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { encodeJxl } from "../../lib/format-encoders.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
|
||||
@@ -204,13 +205,17 @@ export function registerStitch(app: FastifyInstance) {
|
||||
} else if (settings.format === "avif") {
|
||||
pipeline = pipeline.avif({ quality: settings.quality, effort: 4 });
|
||||
} else if (settings.format === "jxl") {
|
||||
pipeline = pipeline.jxl({ quality: settings.quality });
|
||||
pipeline = pipeline.png();
|
||||
} else {
|
||||
pipeline = pipeline.png();
|
||||
}
|
||||
|
||||
let result = await pipeline.toBuffer();
|
||||
|
||||
if (settings.format === "jxl") {
|
||||
result = await encodeJxl(result, settings.quality);
|
||||
}
|
||||
|
||||
if (settings.cornerRadius > 0) {
|
||||
const meta = await sharp(result).metadata();
|
||||
if (!meta.width || !meta.height) throw new Error("Cannot read image dimensions");
|
||||
@@ -238,7 +243,7 @@ export function registerStitch(app: FastifyInstance) {
|
||||
} else if (settings.format === "avif") {
|
||||
result = await sharp(result).avif({ quality: settings.quality, effort: 4 }).toBuffer();
|
||||
} else if (settings.format === "jxl") {
|
||||
result = await sharp(result).jxl({ quality: settings.quality }).toBuffer();
|
||||
result = await encodeJxl(result, settings.quality);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { env } from "../../config.js";
|
||||
import { resolveConcurrency } from "../../lib/env.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { encodeJxl } from "../../lib/format-encoders.js";
|
||||
import { decodeHeic, encodeHeic } from "../../lib/heic-converter.js";
|
||||
import { isSvgBuffer, sanitizeSvg } from "../../lib/svg-sanitize.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
@@ -77,10 +78,12 @@ async function convertSvg(
|
||||
buffer = await image.gif().toBuffer();
|
||||
ext = "gif";
|
||||
break;
|
||||
case "jxl":
|
||||
buffer = await image.jxl({ quality: settings.quality }).toBuffer();
|
||||
case "jxl": {
|
||||
const pngBuf = await image.png().toBuffer();
|
||||
buffer = await encodeJxl(pngBuf, settings.quality);
|
||||
ext = "jxl";
|
||||
break;
|
||||
}
|
||||
case "heif": {
|
||||
const pngBuffer = await image.png().toBuffer();
|
||||
buffer = await encodeHeic(pngBuffer, settings.quality);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { isToolInstalled } from "../../lib/feature-status.js";
|
||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { decodeToSharpCompat, needsCliDecode } from "../../lib/format-decoders.js";
|
||||
import { encodeJxl } from "../../lib/format-encoders.js";
|
||||
import { decodeHeic, encodeHeic } from "../../lib/heic-converter.js";
|
||||
import { resolveOutputFormat } from "../../lib/output-format.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
@@ -186,7 +187,7 @@ export function registerUpscale(app: FastifyInstance) {
|
||||
outputBuffer = await encodeHeic(result.buffer, outputQuality);
|
||||
finalFormat = format;
|
||||
} else if (format === "jxl") {
|
||||
outputBuffer = await sharp(result.buffer).jxl({ quality: outputQuality }).toBuffer();
|
||||
outputBuffer = await encodeJxl(result.buffer, outputQuality);
|
||||
finalFormat = "jxl";
|
||||
} else if (format === "avif") {
|
||||
outputBuffer = await sharp(result.buffer).avif({ quality: outputQuality }).toBuffer();
|
||||
|
||||
Reference in New Issue
Block a user