mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: add decode pipelines for JP2, EPS, QOI, DDS, CUR, DPX, FITS, PPM, SVGZ, APNG
Add input decode support for 10 new image format families: - JPEG 2000 (JP2/J2K): opj_decompress with ImageMagick fallback - EPS: ImageMagick + Ghostscript delegate with 50MB size guard - DDS: ImageMagick decode, first frame extraction - CUR: reuses ICO decoder (structurally identical) - DPX/Cineon: ImageMagick with sRGB colorspace conversion - FITS: ImageMagick with normalize + sRGB conversion - QOI: stub decoder (real codec deferred to Task 4) - PPM/PGM/PBM/PFM: Sharp-native via libvips (no CLI decoder needed) - SVGZ: gzip decompression with bomb protection before SVG sanitization - APNG: accepted via extension, decoded as PNG first frame by Sharp Updates magic bytes, MIME mappings, and frontend accept lists across all file picker entry points (dropzone, tool page, file upload, editor).
This commit is contained in:
@@ -20,6 +20,17 @@ const SUPPORTED_INPUT_FORMATS = new Set([
|
||||
"psd",
|
||||
"exr",
|
||||
"hdr",
|
||||
"jp2",
|
||||
"qoi",
|
||||
"eps",
|
||||
"dds",
|
||||
"cur",
|
||||
"dpx",
|
||||
"fits",
|
||||
"ppm",
|
||||
"pgm",
|
||||
"pbm",
|
||||
"pfm",
|
||||
]);
|
||||
|
||||
interface MagicEntry {
|
||||
@@ -62,6 +73,47 @@ const MAGIC_BYTES: MagicEntry[] = [
|
||||
// OpenEXR
|
||||
{ bytes: [0x76, 0x2f, 0x31, 0x01], offset: 0, format: "exr" },
|
||||
// TGA has no reliable magic bytes — detected by extension only
|
||||
// JPEG 2000 JP2 box signature (NOT ISOBMFF)
|
||||
{
|
||||
bytes: [0x00, 0x00, 0x00, 0x0c, 0x6a, 0x50, 0x20, 0x20, 0x0d, 0x0a, 0x87, 0x0a],
|
||||
offset: 0,
|
||||
format: "jp2",
|
||||
},
|
||||
// JPEG 2000 raw codestream (J2K/J2C)
|
||||
{ bytes: [0xff, 0x4f, 0xff, 0x51], offset: 0, format: "jp2" },
|
||||
// QOI: "qoif" at offset 0
|
||||
{ bytes: [0x71, 0x6f, 0x69, 0x66], offset: 0, format: "qoi" },
|
||||
// DDS: "DDS " at offset 0
|
||||
{ bytes: [0x44, 0x44, 0x53, 0x20], offset: 0, format: "dds" },
|
||||
// CUR: Windows cursor (ICO variant, byte 3 = 0x02 vs ICO's 0x01)
|
||||
{ bytes: [0x00, 0x00, 0x02, 0x00], offset: 0, format: "cur" },
|
||||
// DPX forward: "SDPX"
|
||||
{ bytes: [0x53, 0x44, 0x50, 0x58], offset: 0, format: "dpx" },
|
||||
// DPX reverse: "XPDS"
|
||||
{ bytes: [0x58, 0x50, 0x44, 0x53], offset: 0, format: "dpx" },
|
||||
// Cineon
|
||||
{ bytes: [0x80, 0x2a, 0x5f, 0xd7], offset: 0, format: "dpx" },
|
||||
// FITS: "SIMPLE" at offset 0
|
||||
{ bytes: [0x53, 0x49, 0x4d, 0x50, 0x4c, 0x45], offset: 0, format: "fits" },
|
||||
// EPS ASCII header: "%!PS-Adobe"
|
||||
{
|
||||
bytes: [0x25, 0x21, 0x50, 0x53, 0x2d, 0x41, 0x64, 0x6f, 0x62, 0x65],
|
||||
offset: 0,
|
||||
format: "eps",
|
||||
},
|
||||
// EPS binary (DOS EPS)
|
||||
{ bytes: [0xc5, 0xd0, 0xd3, 0xc6], offset: 0, format: "eps" },
|
||||
// Netpbm: P1-P7 headers (these MUST go AFTER the PNG entry to avoid false matches on 0x50)
|
||||
{ bytes: [0x50, 0x31], offset: 0, format: "pbm" },
|
||||
{ bytes: [0x50, 0x34], offset: 0, format: "pbm" },
|
||||
{ bytes: [0x50, 0x32], offset: 0, format: "pgm" },
|
||||
{ bytes: [0x50, 0x35], offset: 0, format: "pgm" },
|
||||
{ bytes: [0x50, 0x33], offset: 0, format: "ppm" },
|
||||
{ bytes: [0x50, 0x36], offset: 0, format: "ppm" },
|
||||
{ bytes: [0x50, 0x37], offset: 0, format: "ppm" },
|
||||
// PFM (Portable FloatMap)
|
||||
{ bytes: [0x50, 0x46], offset: 0, format: "pfm" },
|
||||
{ bytes: [0x50, 0x66], offset: 0, format: "pfm" },
|
||||
];
|
||||
|
||||
export interface ValidationResult {
|
||||
@@ -104,7 +156,23 @@ const RAW_EXTENSIONS = new Set([
|
||||
]);
|
||||
|
||||
/** Formats that Sharp cannot decode natively — skip dimension check. */
|
||||
const CLI_DECODED_FORMATS = new Set(["raw", "ico", "tga", "psd", "exr", "hdr", "bmp", "jxl"]);
|
||||
const CLI_DECODED_FORMATS = new Set([
|
||||
"raw",
|
||||
"ico",
|
||||
"tga",
|
||||
"psd",
|
||||
"exr",
|
||||
"hdr",
|
||||
"bmp",
|
||||
"jxl",
|
||||
"jp2",
|
||||
"qoi",
|
||||
"eps",
|
||||
"dds",
|
||||
"cur",
|
||||
"dpx",
|
||||
"fits",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Check whether a file extension corresponds to a Camera RAW format.
|
||||
@@ -158,6 +226,18 @@ export async function validateImageBuffer(
|
||||
detectedFormat = "tga";
|
||||
}
|
||||
|
||||
// SVGZ: gzip-compressed SVG, detected by extension + gzip magic
|
||||
if (!detectedFormat && ext === "svgz") {
|
||||
if (buffer.length >= 2 && buffer[0] === 0x1f && buffer[1] === 0x8b) {
|
||||
detectedFormat = "svg";
|
||||
}
|
||||
}
|
||||
|
||||
// APNG: Sharp handles as PNG first frame. Accept .apng extension.
|
||||
if (!detectedFormat && ext === "apng") {
|
||||
detectedFormat = "png";
|
||||
}
|
||||
|
||||
if (!detectedFormat) {
|
||||
return { valid: false, reason: "Unrecognized image format" };
|
||||
}
|
||||
|
||||
@@ -8,7 +8,23 @@ import { promisify } from "node:util";
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/** Formats that need external CLI tools (not decodable by Sharp). */
|
||||
const CLI_DECODED_FORMATS = new Set(["raw", "ico", "tga", "psd", "exr", "hdr", "bmp", "jxl"]);
|
||||
const CLI_DECODED_FORMATS = new Set([
|
||||
"raw",
|
||||
"ico",
|
||||
"tga",
|
||||
"psd",
|
||||
"exr",
|
||||
"hdr",
|
||||
"bmp",
|
||||
"jxl",
|
||||
"jp2",
|
||||
"qoi",
|
||||
"eps",
|
||||
"dds",
|
||||
"cur",
|
||||
"dpx",
|
||||
"fits",
|
||||
]);
|
||||
|
||||
export function needsCliDecode(format: string): boolean {
|
||||
return CLI_DECODED_FORMATS.has(format);
|
||||
@@ -47,6 +63,20 @@ export async function decodeToSharpCompat(
|
||||
return decodeBmp(buffer);
|
||||
case "jxl":
|
||||
return decodeJxl(buffer);
|
||||
case "jp2":
|
||||
return decodeJp2(buffer);
|
||||
case "eps":
|
||||
return decodeEps(buffer);
|
||||
case "dds":
|
||||
return decodeDds(buffer);
|
||||
case "cur":
|
||||
return decodeIco(buffer); // CUR is structurally identical to ICO
|
||||
case "dpx":
|
||||
return decodeDpx(buffer);
|
||||
case "fits":
|
||||
return decodeFits(buffer);
|
||||
case "qoi":
|
||||
return decodeQoi(buffer);
|
||||
default:
|
||||
return buffer;
|
||||
}
|
||||
@@ -284,3 +314,134 @@ async function decodeJxl(buffer: Buffer): Promise<Buffer> {
|
||||
await rm(outputPath, { force: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// ── JPEG 2000 decoder (opj_decompress-first, ImageMagick fallback) ──
|
||||
|
||||
async function decodeJp2(buffer: Buffer): Promise<Buffer> {
|
||||
const id = randomUUID();
|
||||
const inputPath = join(tmpdir(), `jp2-in-${id}.jp2`);
|
||||
const outputPath = join(tmpdir(), `jp2-out-${id}.png`);
|
||||
try {
|
||||
await writeFile(inputPath, buffer);
|
||||
try {
|
||||
await execFileAsync("opj_decompress", ["-i", inputPath, "-o", outputPath], {
|
||||
timeout: 60_000,
|
||||
});
|
||||
return await readFile(outputPath);
|
||||
} catch {
|
||||
// opj_decompress not available, fall back to ImageMagick
|
||||
}
|
||||
const cmd = await findMagickCmd();
|
||||
await execFileAsync(cmd, magickArgs(cmd, [inputPath, `png:${outputPath}`]), {
|
||||
timeout: 120_000,
|
||||
});
|
||||
return await readFile(outputPath);
|
||||
} finally {
|
||||
await rm(inputPath, { force: true }).catch(() => {});
|
||||
await rm(outputPath, { force: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// ── EPS decoder (ImageMagick + Ghostscript delegate) ──
|
||||
|
||||
const MAX_EPS_SIZE = 50 * 1024 * 1024;
|
||||
|
||||
async function decodeEps(buffer: Buffer): Promise<Buffer> {
|
||||
if (buffer.length > MAX_EPS_SIZE) {
|
||||
throw new Error(
|
||||
`EPS file too large (${(buffer.length / 1024 / 1024).toFixed(1)}MB, limit: 50MB)`,
|
||||
);
|
||||
}
|
||||
const cmd = await findMagickCmd();
|
||||
const id = randomUUID();
|
||||
const inputPath = join(tmpdir(), `eps-in-${id}.eps`);
|
||||
const outputPath = join(tmpdir(), `eps-out-${id}.png`);
|
||||
try {
|
||||
await writeFile(inputPath, buffer);
|
||||
await execFileAsync(
|
||||
cmd,
|
||||
magickArgs(cmd, [
|
||||
"-density",
|
||||
"300",
|
||||
"-define",
|
||||
"gs:MaxBitmap=500000000",
|
||||
inputPath,
|
||||
"-colorspace",
|
||||
"sRGB",
|
||||
`png:${outputPath}`,
|
||||
]),
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
return await readFile(outputPath);
|
||||
} finally {
|
||||
await rm(inputPath, { force: true }).catch(() => {});
|
||||
await rm(outputPath, { force: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// ── DDS decoder ──
|
||||
|
||||
async function decodeDds(buffer: Buffer): Promise<Buffer> {
|
||||
const cmd = await findMagickCmd();
|
||||
const id = randomUUID();
|
||||
const inputPath = join(tmpdir(), `dds-in-${id}.dds`);
|
||||
const outputPath = join(tmpdir(), `dds-out-${id}.png`);
|
||||
try {
|
||||
await writeFile(inputPath, buffer);
|
||||
await execFileAsync(cmd, magickArgs(cmd, [`${inputPath}[0]`, `png:${outputPath}`]), {
|
||||
timeout: 120_000,
|
||||
});
|
||||
return await readFile(outputPath);
|
||||
} finally {
|
||||
await rm(inputPath, { force: true }).catch(() => {});
|
||||
await rm(outputPath, { force: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// ── DPX / Cineon decoder ──
|
||||
|
||||
async function decodeDpx(buffer: Buffer): Promise<Buffer> {
|
||||
const cmd = await findMagickCmd();
|
||||
const id = randomUUID();
|
||||
const inputPath = join(tmpdir(), `dpx-in-${id}.dpx`);
|
||||
const outputPath = join(tmpdir(), `dpx-out-${id}.png`);
|
||||
try {
|
||||
await writeFile(inputPath, buffer);
|
||||
await execFileAsync(
|
||||
cmd,
|
||||
magickArgs(cmd, [inputPath, "-colorspace", "sRGB", `png:${outputPath}`]),
|
||||
{ timeout: 120_000 },
|
||||
);
|
||||
return await readFile(outputPath);
|
||||
} finally {
|
||||
await rm(inputPath, { force: true }).catch(() => {});
|
||||
await rm(outputPath, { force: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// ── FITS decoder ──
|
||||
|
||||
async function decodeFits(buffer: Buffer): Promise<Buffer> {
|
||||
const cmd = await findMagickCmd();
|
||||
const id = randomUUID();
|
||||
const inputPath = join(tmpdir(), `fits-in-${id}.fits`);
|
||||
const outputPath = join(tmpdir(), `fits-out-${id}.png`);
|
||||
try {
|
||||
await writeFile(inputPath, buffer);
|
||||
await execFileAsync(
|
||||
cmd,
|
||||
magickArgs(cmd, [inputPath, "-normalize", "-colorspace", "sRGB", `png:${outputPath}`]),
|
||||
{ timeout: 120_000 },
|
||||
);
|
||||
return await readFile(outputPath);
|
||||
} finally {
|
||||
await rm(inputPath, { force: true }).catch(() => {});
|
||||
await rm(outputPath, { force: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// ── QOI decoder (stub -- real codec comes in Task 4) ──
|
||||
|
||||
async function decodeQoi(_buffer: Buffer): Promise<Buffer> {
|
||||
throw new Error("QOI decode not yet implemented");
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { gunzipSync } from "node:zlib";
|
||||
import { env } from "../config.js";
|
||||
|
||||
/**
|
||||
@@ -36,6 +37,24 @@ export function sanitizeSvg(buffer: Buffer): Buffer {
|
||||
return Buffer.from(svg, "utf-8");
|
||||
}
|
||||
|
||||
const MAX_SVGZ_DECOMPRESSED_SIZE = 50 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Decompress an SVGZ (gzip-compressed SVG) buffer.
|
||||
* Returns the buffer unchanged if it is not gzip-compressed.
|
||||
* Throws on decompression bomb or invalid SVG content.
|
||||
*/
|
||||
export function decompressSvgz(buffer: Buffer): Buffer {
|
||||
if (buffer.length < 2 || buffer[0] !== 0x1f || buffer[1] !== 0x8b) {
|
||||
return buffer;
|
||||
}
|
||||
const decompressed = gunzipSync(buffer, { maxOutputLength: MAX_SVGZ_DECOMPRESSED_SIZE });
|
||||
if (!isSvgBuffer(decompressed)) {
|
||||
throw new Error("SVGZ file does not contain valid SVG content after decompression");
|
||||
}
|
||||
return decompressed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a buffer looks like SVG content.
|
||||
* Examines the first 4KB for an <svg tag.
|
||||
|
||||
@@ -16,7 +16,7 @@ import { sanitizeFilename } from "../lib/filename.js";
|
||||
import { decodeToSharpCompat, needsCliDecode } from "../lib/format-decoders.js";
|
||||
import { decodeHeic } from "../lib/heic-converter.js";
|
||||
import type { WorkerInput, WorkerOutput } from "../lib/image-worker.js";
|
||||
import { sanitizeSvg } from "../lib/svg-sanitize.js";
|
||||
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";
|
||||
@@ -212,6 +212,7 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
const isSvg = validation.format === "svg";
|
||||
if (isSvg) {
|
||||
try {
|
||||
fileBuffer = decompressSvgz(fileBuffer);
|
||||
fileBuffer = sanitizeSvg(fileBuffer);
|
||||
} catch (err) {
|
||||
return reply.status(400).send({
|
||||
@@ -323,6 +324,12 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
"image/x-exr": ".exr",
|
||||
"image/vnd.radiance": ".hdr",
|
||||
"image/x-targa": ".tga",
|
||||
"image/jp2": ".jp2",
|
||||
"image/qoi": ".qoi",
|
||||
"application/postscript": ".eps",
|
||||
"image/vnd.ms-dds": ".dds",
|
||||
"image/x-dpx": ".dpx",
|
||||
"image/fits": ".fits",
|
||||
};
|
||||
const expectedExt = CONTENT_TYPE_TO_EXT[result.contentType];
|
||||
if (expectedExt) {
|
||||
|
||||
@@ -16,7 +16,7 @@ interface DropzoneProps {
|
||||
// Append explicit extensions so they are selectable.
|
||||
function expandAccept(accept?: string): string | undefined {
|
||||
if (!accept?.includes("image/*")) return accept;
|
||||
return `${accept},.heic,.heif,.hif,.jxl,.ico,.dng,.cr2,.nef,.arw,.orf,.rw2,.tga,.psd,.exr,.hdr`;
|
||||
return `${accept},.heic,.heif,.hif,.jxl,.ico,.dng,.cr2,.cr3,.nef,.nrw,.arw,.orf,.rw2,.raf,.pef,.3fr,.iiq,.srw,.x3f,.rwl,.gpr,.fff,.mrw,.mef,.kdc,.dcr,.erf,.ptx,.tga,.psd,.exr,.hdr,.svgz,.jp2,.j2k,.j2c,.jpc,.jpf,.jpx,.qoi,.eps,.epsf,.dds,.cur,.apng,.dpx,.cin,.fits,.fit,.fts,.ppm,.pgm,.pbm,.pnm,.pam,.pfm`;
|
||||
}
|
||||
|
||||
export function Dropzone({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useCallback, useState } from "react";
|
||||
import { useEditorStore } from "@/stores/editor-store";
|
||||
import { NewDocumentDialog } from "./new-document-dialog";
|
||||
|
||||
const ACCEPTED_TYPES = ".png,.jpg,.jpeg,.webp,.gif,.bmp,.tiff,.svg";
|
||||
const ACCEPTED_TYPES = ".png,.jpg,.jpeg,.webp,.gif,.bmp,.tiff,.svg,.avif,.svgz";
|
||||
|
||||
export function WelcomeScreen() {
|
||||
const [showNewDoc, setShowNewDoc] = useState(false);
|
||||
|
||||
@@ -57,7 +57,7 @@ export function FileUploadArea() {
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*,.heic,.heif,.hif,.jxl,.dng,.cr2,.nef,.arw,.orf,.rw2,.tga,.psd,.exr,.hdr"
|
||||
accept="image/*,.heic,.heif,.hif,.jxl,.dng,.cr2,.cr3,.nef,.nrw,.arw,.orf,.rw2,.raf,.pef,.3fr,.iiq,.srw,.x3f,.rwl,.gpr,.fff,.mrw,.mef,.kdc,.dcr,.erf,.ptx,.tga,.psd,.exr,.hdr,.svgz,.jp2,.j2k,.qoi,.eps,.dds,.cur,.apng,.dpx,.cin,.fits,.ppm,.pgm,.pbm,.pfm"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleInputChange}
|
||||
|
||||
@@ -251,7 +251,7 @@ export function ToolPage() {
|
||||
input.type = "file";
|
||||
input.multiple = true;
|
||||
input.accept =
|
||||
"image/*,.heic,.heif,.hif,.jxl,.dng,.cr2,.nef,.arw,.orf,.rw2,.tga,.psd,.exr,.hdr";
|
||||
"image/*,.heic,.heif,.hif,.jxl,.dng,.cr2,.cr3,.nef,.nrw,.arw,.orf,.rw2,.raf,.pef,.3fr,.iiq,.srw,.x3f,.rwl,.gpr,.fff,.mrw,.mef,.kdc,.dcr,.erf,.ptx,.tga,.psd,.exr,.hdr,.svgz,.jp2,.j2k,.qoi,.eps,.dds,.cur,.apng,.dpx,.cin,.fits,.ppm,.pgm,.pbm,.pfm";
|
||||
input.onchange = (e) => {
|
||||
const newFiles = Array.from((e.target as HTMLInputElement).files || []);
|
||||
if (newFiles.length > 0) addFiles(newFiles);
|
||||
|
||||
@@ -34,6 +34,39 @@ const MAGIC_BYTES: Array<{ bytes: number[]; offset: number; format: string }> =
|
||||
{ bytes: [0x46, 0x4f, 0x56, 0x62], offset: 0, format: "x3f" },
|
||||
// Minolta MRW: "\x00MRM" at offset 0
|
||||
{ bytes: [0x00, 0x4d, 0x52, 0x4d], offset: 0, format: "mrw" },
|
||||
// JP2 box signature
|
||||
{
|
||||
bytes: [0x00, 0x00, 0x00, 0x0c, 0x6a, 0x50, 0x20, 0x20, 0x0d, 0x0a, 0x87, 0x0a],
|
||||
offset: 0,
|
||||
format: "jp2",
|
||||
},
|
||||
// J2K raw codestream
|
||||
{ bytes: [0xff, 0x4f, 0xff, 0x51], offset: 0, format: "jp2" },
|
||||
// QOI
|
||||
{ bytes: [0x71, 0x6f, 0x69, 0x66], offset: 0, format: "qoi" },
|
||||
// DDS
|
||||
{ bytes: [0x44, 0x44, 0x53, 0x20], offset: 0, format: "dds" },
|
||||
// CUR
|
||||
{ bytes: [0x00, 0x00, 0x02, 0x00], offset: 0, format: "cur" },
|
||||
// DPX forward
|
||||
{ bytes: [0x53, 0x44, 0x50, 0x58], offset: 0, format: "dpx" },
|
||||
// DPX reverse
|
||||
{ bytes: [0x58, 0x50, 0x44, 0x53], offset: 0, format: "dpx" },
|
||||
// Cineon
|
||||
{ bytes: [0x80, 0x2a, 0x5f, 0xd7], offset: 0, format: "cin" },
|
||||
// FITS
|
||||
{ bytes: [0x53, 0x49, 0x4d, 0x50, 0x4c, 0x45], offset: 0, format: "fits" },
|
||||
// EPS ASCII
|
||||
{
|
||||
bytes: [0x25, 0x21, 0x50, 0x53, 0x2d, 0x41, 0x64, 0x6f, 0x62, 0x65],
|
||||
offset: 0,
|
||||
format: "eps",
|
||||
},
|
||||
// EPS binary (DOS)
|
||||
{ bytes: [0xc5, 0xd0, 0xd3, 0xc6], offset: 0, format: "eps" },
|
||||
// PPM (P3/P6)
|
||||
{ bytes: [0x50, 0x33], offset: 0, format: "ppm" },
|
||||
{ bytes: [0x50, 0x36], offset: 0, format: "ppm" },
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,6 +40,30 @@ const EXT_TO_MIME: Record<string, string> = {
|
||||
psd: "image/vnd.adobe.photoshop",
|
||||
exr: "image/x-exr",
|
||||
hdr: "image/vnd.radiance",
|
||||
jp2: "image/jp2",
|
||||
j2k: "image/jp2",
|
||||
j2c: "image/jp2",
|
||||
jpc: "image/jp2",
|
||||
jpf: "image/jp2",
|
||||
jpx: "image/jpx",
|
||||
qoi: "image/qoi",
|
||||
eps: "application/postscript",
|
||||
epsf: "application/postscript",
|
||||
dds: "image/vnd.ms-dds",
|
||||
cur: "image/x-icon",
|
||||
apng: "image/apng",
|
||||
dpx: "image/x-dpx",
|
||||
cin: "image/x-cineon",
|
||||
fits: "image/fits",
|
||||
fit: "image/fits",
|
||||
fts: "image/fits",
|
||||
ppm: "image/x-portable-pixmap",
|
||||
pgm: "image/x-portable-graymap",
|
||||
pbm: "image/x-portable-bitmap",
|
||||
pnm: "image/x-portable-anymap",
|
||||
pam: "image/x-portable-anymap",
|
||||
pfm: "image/x-portable-floatmap",
|
||||
svgz: "image/svg+xml",
|
||||
};
|
||||
|
||||
const MIME_TO_EXT: Record<string, string> = {
|
||||
@@ -82,6 +106,20 @@ const MIME_TO_EXT: Record<string, string> = {
|
||||
"image/vnd.adobe.photoshop": "psd",
|
||||
"image/x-exr": "exr",
|
||||
"image/vnd.radiance": "hdr",
|
||||
"image/jp2": "jp2",
|
||||
"image/jpx": "jpx",
|
||||
"image/qoi": "qoi",
|
||||
"application/postscript": "eps",
|
||||
"image/vnd.ms-dds": "dds",
|
||||
"image/apng": "apng",
|
||||
"image/x-dpx": "dpx",
|
||||
"image/x-cineon": "cin",
|
||||
"image/fits": "fits",
|
||||
"image/x-portable-pixmap": "ppm",
|
||||
"image/x-portable-graymap": "pgm",
|
||||
"image/x-portable-bitmap": "pbm",
|
||||
"image/x-portable-anymap": "pnm",
|
||||
"image/x-portable-floatmap": "pfm",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user