mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
* feat: add resolveOutputFormat utility for input format preservation * fix: preserve file order in batch processing with X-File-Results header Collect all results before streaming the ZIP to guarantee upload order. Replace X-File-Order with index-based X-File-Results header that maps each upload index to its processed filename, handling failures and duplicate filenames correctly. Closes #13 * fix: use X-File-Results for index-based batch file matching The frontend now matches processed files to entries by upload index instead of fragile name/position matching. * feat: preserve input format in smart-crop with quality control Smart crop now outputs in the same format as the input (JPG in, JPG out) instead of always converting to PNG. Adds an optional quality setting (default 95) for lossy formats. Closes #14 * feat: add output quality slider to smart crop settings UI * feat: preserve input format in crop tool * feat: preserve input format in color adjustment tools Applies to brightness-contrast, saturation, color-channels, and color-effects tool routes. * refactor: avoid double encode in smart-crop content mode For the simple trim path (no pad-to-square), chain .toFormat() on the trim pipeline directly instead of creating a second Sharp instance. This eliminates a redundant intermediate encode that degraded quality for lossy formats. Also use trimmed.info dimensions instead of a separate metadata() call for the pad-to-square path. --------- Co-authored-by: Siddharth Kumar Sah <siddharth123sk@gmail.com>
55 lines
2.1 KiB
TypeScript
55 lines
2.1 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import { resolveOutputFormat } from "../../../apps/api/src/lib/output-format.js";
|
|
|
|
const FIXTURES = join(__dirname, "..", "..", "fixtures");
|
|
const JPG = readFileSync(join(FIXTURES, "test-100x100.jpg"));
|
|
const PNG = readFileSync(join(FIXTURES, "test-200x150.png"));
|
|
const WEBP = readFileSync(join(FIXTURES, "test-50x50.webp"));
|
|
|
|
describe("resolveOutputFormat", () => {
|
|
it("detects JPEG input and returns jpeg config", async () => {
|
|
const result = await resolveOutputFormat(JPG, "photo.jpg");
|
|
expect(result.format).toBe("jpeg");
|
|
expect(result.extension).toBe("jpg");
|
|
expect(result.contentType).toBe("image/jpeg");
|
|
expect(result.quality).toBe(95);
|
|
});
|
|
|
|
it("detects PNG input and returns png config", async () => {
|
|
const result = await resolveOutputFormat(PNG, "image.png");
|
|
expect(result.format).toBe("png");
|
|
expect(result.extension).toBe("png");
|
|
expect(result.contentType).toBe("image/png");
|
|
expect(result.quality).toBe(95);
|
|
});
|
|
|
|
it("detects WebP input and returns webp config", async () => {
|
|
const result = await resolveOutputFormat(WEBP, "image.webp");
|
|
expect(result.format).toBe("webp");
|
|
expect(result.extension).toBe("webp");
|
|
expect(result.contentType).toBe("image/webp");
|
|
expect(result.quality).toBe(95);
|
|
});
|
|
|
|
it("falls back to PNG for unknown format", async () => {
|
|
const garbage = Buffer.from("not an image at all");
|
|
const result = await resolveOutputFormat(garbage, "mystery.bin");
|
|
expect(result.format).toBe("png");
|
|
expect(result.extension).toBe("png");
|
|
expect(result.contentType).toBe("image/png");
|
|
});
|
|
|
|
it("respects quality override for lossy formats", async () => {
|
|
const result = await resolveOutputFormat(JPG, "photo.jpg", 50);
|
|
expect(result.quality).toBe(50);
|
|
});
|
|
|
|
it("accepts quality override for PNG without error", async () => {
|
|
const result = await resolveOutputFormat(PNG, "image.png", 50);
|
|
expect(result.format).toBe("png");
|
|
expect(result.quality).toBe(50);
|
|
});
|
|
});
|