mirror of https://github.com/VERT-sh/VERT.git
feat: ico, heic, dng, cur, ani
This commit is contained in:
parent
0d330f221a
commit
118aaa1745
14
bun.lock
14
bun.lock
|
@ -11,11 +11,13 @@
|
|||
"@fontsource/lexend": "^5.1.2",
|
||||
"@fontsource/radio-canada-big": "^5.1.1",
|
||||
"@imagemagick/magick-wasm": "^0.0.34",
|
||||
"byte-data": "^19.0.1",
|
||||
"client-zip": "^2.4.6",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-svelte": "^0.475.0",
|
||||
"music-metadata": "^11.0.0",
|
||||
"p-queue": "^8.1.0",
|
||||
"riff-file": "^1.0.3",
|
||||
"vite-plugin-static-copy": "^2.2.0",
|
||||
"wasm-vips": "^0.0.11",
|
||||
},
|
||||
|
@ -302,6 +304,8 @@
|
|||
|
||||
"browserslist": ["browserslist@4.24.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" } }, "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A=="],
|
||||
|
||||
"byte-data": ["byte-data@19.0.1", "", {}, "sha512-xRvkTvO28wr0+0rErSETHD8Cw+P444Az3/jkTezaMw5R+TTW8ZNXuvPZf9/ZhnSRRvlMnJsVhc+ecYvOMy/MQQ=="],
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
|
||||
|
@ -368,6 +372,8 @@
|
|||
|
||||
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"endianness": ["endianness@8.0.2", "", {}, "sha512-IU+77+jJ7lpw2qZ3NUuqBZFy3GuioNgXUdsL1L9tooDNTaw0TgOnwNuc+8Ns+haDaTifK97QLzmOANJtI/rGvw=="],
|
||||
|
||||
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
|
||||
|
@ -458,6 +464,8 @@
|
|||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"ieee754-buffer": ["ieee754-buffer@2.0.0", "", {}, "sha512-AXUAT0nMEi7h1Is8HXGXof3eejl/GabZFKSj8Ym6kVRUSwrAb52EkAXywiCQYSHGQMRn7lvfY7vhPMjVc+Kybg=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"immutable": ["immutable@5.0.3", "", {}, "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw=="],
|
||||
|
@ -640,6 +648,8 @@
|
|||
|
||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"riff-file": ["riff-file@1.0.3", "", { "dependencies": { "byte-data": "^18.0.3" } }, "sha512-Vv8wwGr0BCks7VMI3Lv0houZee4DaHFjjTT0LMhMJKio2YmLncLeIVpK63ydSverngNk8XQPU3fbeP3bWgSIig=="],
|
||||
|
||||
"rollup": ["rollup@4.34.9", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.34.9", "@rollup/rollup-android-arm64": "4.34.9", "@rollup/rollup-darwin-arm64": "4.34.9", "@rollup/rollup-darwin-x64": "4.34.9", "@rollup/rollup-freebsd-arm64": "4.34.9", "@rollup/rollup-freebsd-x64": "4.34.9", "@rollup/rollup-linux-arm-gnueabihf": "4.34.9", "@rollup/rollup-linux-arm-musleabihf": "4.34.9", "@rollup/rollup-linux-arm64-gnu": "4.34.9", "@rollup/rollup-linux-arm64-musl": "4.34.9", "@rollup/rollup-linux-loongarch64-gnu": "4.34.9", "@rollup/rollup-linux-powerpc64le-gnu": "4.34.9", "@rollup/rollup-linux-riscv64-gnu": "4.34.9", "@rollup/rollup-linux-s390x-gnu": "4.34.9", "@rollup/rollup-linux-x64-gnu": "4.34.9", "@rollup/rollup-linux-x64-musl": "4.34.9", "@rollup/rollup-win32-arm64-msvc": "4.34.9", "@rollup/rollup-win32-ia32-msvc": "4.34.9", "@rollup/rollup-win32-x64-msvc": "4.34.9", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ=="],
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
@ -718,6 +728,8 @@
|
|||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"utf8-buffer": ["utf8-buffer@1.0.0", "", {}, "sha512-ueuhzvWnp5JU5CiGSY4WdKbiN/PO2AZ/lpeLiz2l38qwdLy/cW40XobgyuIWucNyum0B33bVB0owjFCeGBSLqg=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"vite": ["vite@5.4.14", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA=="],
|
||||
|
@ -764,6 +776,8 @@
|
|||
|
||||
"postcss-load-config/lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
|
||||
|
||||
"riff-file/byte-data": ["byte-data@18.1.1", "", { "dependencies": { "endianness": "^8.0.2", "ieee754-buffer": "^2.0.0", "utf8-buffer": "^1.0.0" } }, "sha512-Kv/B0r7adgnCcrs/y703sac2XFLdHW5kPfis1j8+Ij/hmEcWhBKf+1pNTv+vsNqXb207Uiyri8bpnogNxR/4Lg=="],
|
||||
|
||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
|
|
@ -41,11 +41,13 @@
|
|||
"@fontsource/lexend": "^5.1.2",
|
||||
"@fontsource/radio-canada-big": "^5.1.1",
|
||||
"@imagemagick/magick-wasm": "^0.0.34",
|
||||
"byte-data": "^19.0.1",
|
||||
"client-zip": "^2.4.6",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-svelte": "^0.475.0",
|
||||
"music-metadata": "^11.0.0",
|
||||
"p-queue": "^8.1.0",
|
||||
"riff-file": "^1.0.3",
|
||||
"vite-plugin-static-copy": "^2.2.0",
|
||||
"wasm-vips": "^0.0.11"
|
||||
}
|
||||
|
|
|
@ -15,27 +15,35 @@ export class VipsConverter extends Converter {
|
|||
private id = 0;
|
||||
public name = "libvips";
|
||||
public ready = $state(false);
|
||||
public supportedFormats = [
|
||||
".png",
|
||||
".jpeg",
|
||||
".jpg",
|
||||
".webp",
|
||||
".gif",
|
||||
".hdr",
|
||||
".jpe",
|
||||
".dng",
|
||||
".mat",
|
||||
".pbm",
|
||||
".pfm",
|
||||
".pgm",
|
||||
".pnm",
|
||||
".ppm",
|
||||
".raw",
|
||||
".tif",
|
||||
".tiff",
|
||||
".jfif",
|
||||
public static supportedFormatsStatic = [
|
||||
...new Set([
|
||||
".png",
|
||||
".jpeg",
|
||||
".jpg",
|
||||
".webp",
|
||||
".gif",
|
||||
".ico",
|
||||
".cur",
|
||||
".ani",
|
||||
".heic",
|
||||
".hdr",
|
||||
".jpe",
|
||||
".dng",
|
||||
".mat",
|
||||
".pbm",
|
||||
".pfm",
|
||||
".pgm",
|
||||
".pnm",
|
||||
".ppm",
|
||||
".raw",
|
||||
".tif",
|
||||
".tiff",
|
||||
".jfif",
|
||||
]),
|
||||
];
|
||||
|
||||
public supportedFormats = VipsConverter.supportedFormatsStatic;
|
||||
|
||||
public readonly reportsProgress = false;
|
||||
|
||||
constructor() {
|
||||
|
@ -87,7 +95,7 @@ export class VipsConverter extends Converter {
|
|||
log(["converters", this.name], `converted ${input.name} to ${to}`);
|
||||
return new VertFile(
|
||||
new File([res.output as unknown as BlobPart], input.name),
|
||||
to,
|
||||
res.zip ? ".zip" : to,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
// THIS CODE IS FROM https://github.com/captbaritone/webamp/blob/15b0312cb794973a0e615d894df942452e920c36/packages/ani-cursor/src/parser.ts
|
||||
// LICENSED UNDER MIT. (c) Jordan Eldredge and Webamp contributors
|
||||
|
||||
// this code is ripped from their project because i didn't want to
|
||||
// re-invent the wheel, BUT the library they provide (ani-cursor)
|
||||
// doesn't expose the internals.
|
||||
|
||||
import { RIFFFile } from "riff-file";
|
||||
import { unpackArray, unpackString } from "byte-data";
|
||||
|
||||
type Chunk = {
|
||||
format: string;
|
||||
chunkId: string;
|
||||
chunkData: {
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
subChunks: Chunk[];
|
||||
};
|
||||
|
||||
// https://www.informit.com/articles/article.aspx?p=1189080&seqNum=3
|
||||
type AniMetadata = {
|
||||
cbSize: number; // Data structure size (in bytes)
|
||||
nFrames: number; // Number of images (also known as frames) stored in the file
|
||||
nSteps: number; // Number of frames to be displayed before the animation repeats
|
||||
iWidth: number; // Width of frame (in pixels)
|
||||
iHeight: number; // Height of frame (in pixels)
|
||||
iBitCount: number; // Number of bits per pixel
|
||||
nPlanes: number; // Number of color planes
|
||||
iDispRate: number; // Default frame display rate (measured in 1/60th-of-a-second units)
|
||||
bfAttributes: number; // ANI attribute bit flags
|
||||
};
|
||||
|
||||
type ParsedAni = {
|
||||
rate: number[] | null;
|
||||
seq: number[] | null;
|
||||
images: Uint8Array[];
|
||||
metadata: AniMetadata;
|
||||
artist: string | null;
|
||||
title: string | null;
|
||||
};
|
||||
|
||||
const DWORD = { bits: 32, be: false, signed: false, fp: false };
|
||||
|
||||
export function parseAni(arr: Uint8Array): ParsedAni {
|
||||
const riff = new RIFFFile();
|
||||
|
||||
riff.setSignature(arr);
|
||||
|
||||
const signature = riff.signature as Chunk;
|
||||
if (signature.format !== "ACON") {
|
||||
throw new Error(
|
||||
`Expected format. Expected "ACON", got "${signature.format}"`,
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to get a chunk by chunkId and transform it if it's non-null.
|
||||
function mapChunk<T>(
|
||||
chunkId: string,
|
||||
mapper: (chunk: Chunk) => T,
|
||||
): T | null {
|
||||
const chunk = riff.findChunk(chunkId) as Chunk | null;
|
||||
return chunk == null ? null : mapper(chunk);
|
||||
}
|
||||
|
||||
function readImages(chunk: Chunk, frameCount: number): Uint8Array[] {
|
||||
return chunk.subChunks.slice(0, frameCount).map((c) => {
|
||||
if (c.chunkId !== "icon") {
|
||||
throw new Error(`Unexpected chunk type in fram: ${c.chunkId}`);
|
||||
}
|
||||
return arr.slice(c.chunkData.start, c.chunkData.end);
|
||||
});
|
||||
}
|
||||
|
||||
const metadata = mapChunk("anih", (c) => {
|
||||
const words = unpackArray(
|
||||
arr,
|
||||
DWORD,
|
||||
c.chunkData.start,
|
||||
c.chunkData.end,
|
||||
);
|
||||
return {
|
||||
cbSize: words[0],
|
||||
nFrames: words[1],
|
||||
nSteps: words[2],
|
||||
iWidth: words[3],
|
||||
iHeight: words[4],
|
||||
iBitCount: words[5],
|
||||
nPlanes: words[6],
|
||||
iDispRate: words[7],
|
||||
bfAttributes: words[8],
|
||||
};
|
||||
});
|
||||
|
||||
if (metadata == null) {
|
||||
throw new Error("Did not find anih");
|
||||
}
|
||||
|
||||
const rate = mapChunk("rate", (c) => {
|
||||
return unpackArray(arr, DWORD, c.chunkData.start, c.chunkData.end);
|
||||
});
|
||||
// chunkIds are always four chars, hence the trailing space.
|
||||
const seq = mapChunk("seq ", (c) => {
|
||||
return unpackArray(arr, DWORD, c.chunkData.start, c.chunkData.end);
|
||||
});
|
||||
|
||||
const lists = riff.findChunk("LIST", true) as Chunk[] | null;
|
||||
const imageChunk = lists?.find((c) => c.format === "fram");
|
||||
if (imageChunk == null) {
|
||||
throw new Error("Did not find fram LIST");
|
||||
}
|
||||
|
||||
let images = readImages(imageChunk, metadata.nFrames);
|
||||
|
||||
let title = null;
|
||||
let artist = null;
|
||||
|
||||
const infoChunk = lists?.find((c) => c.format === "INFO");
|
||||
if (infoChunk != null) {
|
||||
infoChunk.subChunks.forEach((c) => {
|
||||
switch (c.chunkId) {
|
||||
case "INAM":
|
||||
title = unpackString(
|
||||
arr,
|
||||
c.chunkData.start,
|
||||
c.chunkData.end,
|
||||
);
|
||||
break;
|
||||
case "IART":
|
||||
artist = unpackString(
|
||||
arr,
|
||||
c.chunkData.start,
|
||||
c.chunkData.end,
|
||||
);
|
||||
break;
|
||||
case "LIST":
|
||||
// Some cursors with an artist of "Created with Take ONE 3.5 (unregisterred version)" seem to have their frames here for some reason?
|
||||
if (c.format === "fram") {
|
||||
images = readImages(c, metadata.nFrames);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Unexpected subchunk
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { images, rate, seq, metadata, artist, title };
|
||||
}
|
|
@ -10,6 +10,7 @@ interface ConvertMessage {
|
|||
interface FinishedMessage {
|
||||
type: "finished";
|
||||
output: ArrayBufferLike;
|
||||
zip?: boolean;
|
||||
}
|
||||
|
||||
interface LoadedMessage {
|
||||
|
|
|
@ -3,9 +3,13 @@ import {
|
|||
initializeImageMagick,
|
||||
MagickFormat,
|
||||
MagickImage,
|
||||
MagickImageCollection,
|
||||
MagickReadSettings,
|
||||
type IMagickImage,
|
||||
} from "@imagemagick/magick-wasm";
|
||||
import { makeZip } from "client-zip";
|
||||
import wasm from "@imagemagick/magick-wasm/magick.wasm?url";
|
||||
import { parseAni } from "$lib/parse/ani";
|
||||
|
||||
const vipsPromise = Vips({
|
||||
dynamicLibraries: [],
|
||||
|
@ -13,9 +17,9 @@ const vipsPromise = Vips({
|
|||
|
||||
const magickPromise = initializeImageMagick(new URL(wasm, import.meta.url));
|
||||
|
||||
const magickRequiredFormats = [".dng"];
|
||||
const magickRequiredFormats = [".dng", ".heic", ".ico", ".cur", ".ani"];
|
||||
const unsupportedFrom: string[] = [];
|
||||
const unsupportedTo = [".dng"];
|
||||
const unsupportedTo = [...magickRequiredFormats];
|
||||
|
||||
vipsPromise
|
||||
.then(() => {
|
||||
|
@ -31,6 +35,7 @@ const handleMessage = async (message: any): Promise<any> => {
|
|||
switch (message.type) {
|
||||
case "convert": {
|
||||
if (!message.to.startsWith(".")) message.to = `.${message.to}`;
|
||||
console.log(message);
|
||||
if (unsupportedFrom.includes(message.input.from)) {
|
||||
return {
|
||||
type: "error",
|
||||
|
@ -52,7 +57,101 @@ const handleMessage = async (message: any): Promise<any> => {
|
|||
) {
|
||||
// only wait when we need to
|
||||
await magickPromise;
|
||||
const magick = MagickImage.create(
|
||||
|
||||
// special ico handling to split them all into separate images
|
||||
if (message.input.from === ".ico") {
|
||||
const imgs = MagickImageCollection.create();
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const img = MagickImage.create(
|
||||
new Uint8Array(buffer),
|
||||
new MagickReadSettings({
|
||||
format: MagickFormat.Ico,
|
||||
frameIndex: imgs.length,
|
||||
}),
|
||||
);
|
||||
imgs.push(img);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (_) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (imgs.length === 0) {
|
||||
return {
|
||||
type: "error",
|
||||
error: `Failed to read ICO -- no images found inside?`,
|
||||
};
|
||||
}
|
||||
|
||||
const convertedImgs: Uint8Array[] = [];
|
||||
await Promise.all(
|
||||
imgs.map(async (img, i) => {
|
||||
const output = await magickConvert(img, message.to);
|
||||
convertedImgs[i] = output;
|
||||
}),
|
||||
);
|
||||
|
||||
const zip = makeZip(
|
||||
convertedImgs.map(
|
||||
(img, i) =>
|
||||
new File(
|
||||
[img],
|
||||
`image${i}.${message.to.slice(1)}`,
|
||||
),
|
||||
),
|
||||
"images.zip",
|
||||
);
|
||||
|
||||
// read the ReadableStream to the end
|
||||
const zipBytes = await readToEnd(zip.getReader());
|
||||
|
||||
imgs.dispose();
|
||||
|
||||
return {
|
||||
type: "finished",
|
||||
output: zipBytes,
|
||||
zip: true,
|
||||
};
|
||||
} else if (message.input.from === ".ani") {
|
||||
console.log("Parsing ANI file");
|
||||
try {
|
||||
const parsedAni = parseAni(new Uint8Array(buffer));
|
||||
const files: File[] = [];
|
||||
await Promise.all(
|
||||
parsedAni.images.map(async (img, i) => {
|
||||
const blob = await magickConvert(
|
||||
MagickImage.create(
|
||||
img,
|
||||
new MagickReadSettings({
|
||||
format: MagickFormat.Ico,
|
||||
}),
|
||||
),
|
||||
message.to,
|
||||
);
|
||||
files.push(
|
||||
new File([blob], `image${i}${message.to}`),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const zip = makeZip(files, "images.zip");
|
||||
const zipBytes = await readToEnd(zip.getReader());
|
||||
|
||||
return {
|
||||
type: "finished",
|
||||
output: zipBytes,
|
||||
zip: true,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(message.input.from);
|
||||
|
||||
const img = MagickImage.create(
|
||||
new Uint8Array(buffer),
|
||||
new MagickReadSettings({
|
||||
format: message.input.from
|
||||
|
@ -61,17 +160,14 @@ const handleMessage = async (message: any): Promise<any> => {
|
|||
}),
|
||||
);
|
||||
|
||||
const dngBuffer = await new Promise<Uint8Array>((resolve) =>
|
||||
magick.write(message.to.slice(1).toUpperCase(), (data) => {
|
||||
resolve(data);
|
||||
}),
|
||||
);
|
||||
const converted = await magickConvert(img, message.to);
|
||||
|
||||
return {
|
||||
type: "finished",
|
||||
output: dngBuffer,
|
||||
output: converted,
|
||||
};
|
||||
}
|
||||
|
||||
let image = vips.Image.newFromBuffer(buffer, "");
|
||||
|
||||
// check if animated image & keep it animated when converting
|
||||
|
@ -94,6 +190,67 @@ const handleMessage = async (message: any): Promise<any> => {
|
|||
}
|
||||
};
|
||||
|
||||
const readToEnd = async (reader: ReadableStreamDefaultReader<Uint8Array>) => {
|
||||
const chunks: Uint8Array[] = [];
|
||||
let done = false;
|
||||
while (!done) {
|
||||
const { value, done: d } = await reader.read();
|
||||
if (value) chunks.push(value);
|
||||
done = d;
|
||||
}
|
||||
const blob = new Blob(chunks, { type: "application/zip" });
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
return new Uint8Array(arrayBuffer);
|
||||
};
|
||||
|
||||
const magickToBlob = async (img: IMagickImage): Promise<Blob> => {
|
||||
const canvas = new OffscreenCanvas(img.width, img.height);
|
||||
return new Promise<Blob>((resolve, reject) =>
|
||||
img.getPixels(async (p) => {
|
||||
const area = p.getArea(0, 0, img.width, img.height);
|
||||
const chunkSize = img.hasAlpha ? 4 : 3;
|
||||
const chunks = Math.ceil(area.length / chunkSize);
|
||||
const data = new Uint8ClampedArray(chunks * 4);
|
||||
|
||||
for (let j = 0, k = 0; j < area.length; j += chunkSize, k += 4) {
|
||||
data[k] = area[j];
|
||||
data[k + 1] = area[j + 1];
|
||||
data[k + 2] = area[j + 2];
|
||||
data[k + 3] = img.hasAlpha ? area[j + 3] : 255;
|
||||
}
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) {
|
||||
reject(new Error("Failed to get canvas context"));
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.putImageData(new ImageData(data, img.width, img.height), 0, 0);
|
||||
|
||||
const blob = await canvas.convertToBlob({
|
||||
type: "image/png",
|
||||
});
|
||||
|
||||
resolve(blob);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const magickConvert = async (img: IMagickImage, to: string) => {
|
||||
const vips = await vipsPromise;
|
||||
|
||||
const intermediary = await magickToBlob(img);
|
||||
const buf = await intermediary.arrayBuffer();
|
||||
|
||||
const imgVips = vips.Image.newFromBuffer(buf);
|
||||
const output = imgVips.writeToBuffer(to);
|
||||
|
||||
imgVips.delete();
|
||||
img.dispose();
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
onmessage = async (e) => {
|
||||
const message = e.data;
|
||||
try {
|
||||
|
|
Loading…
Reference in New Issue