From 2191c955007faf7970de6cbf6f2e2f9bffe8a246 Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Mon, 14 Apr 2025 23:24:12 +0100 Subject: [PATCH 01/18] feat: experimental audio to video --- src/lib/converters/converter.svelte.ts | 5 +- src/lib/converters/ffmpeg.svelte.ts | 103 ++++++++++++++++++++++--- src/lib/converters/pandoc.svelte.ts | 24 +++--- src/lib/converters/vertd.svelte.ts | 16 ++-- src/lib/converters/vips.svelte.ts | 34 ++++---- src/lib/types/file.svelte.ts | 23 ++++-- src/routes/convert/+page.svelte | 10 ++- 7 files changed, 158 insertions(+), 57 deletions(-) diff --git a/src/lib/converters/converter.svelte.ts b/src/lib/converters/converter.svelte.ts index 4c0fc85..f321b56 100644 --- a/src/lib/converters/converter.svelte.ts +++ b/src/lib/converters/converter.svelte.ts @@ -5,8 +5,9 @@ export class FormatInfo { constructor( name: string, - public fromSupported: boolean, - public toSupported: boolean, + public fromSupported = true, + public toSupported = true, + public isNative = true, ) { this.name = name; if (!this.name.startsWith(".")) { diff --git a/src/lib/converters/ffmpeg.svelte.ts b/src/lib/converters/ffmpeg.svelte.ts index 9ce3e69..b6c5122 100644 --- a/src/lib/converters/ffmpeg.svelte.ts +++ b/src/lib/converters/ffmpeg.svelte.ts @@ -5,23 +5,27 @@ import { browser } from "$app/environment"; import { error, log } from "$lib/logger"; import { addToast } from "$lib/store/ToastProvider"; +const videoFormats = ["mp4", "mkv", "avi", "mov", "webm"]; + export class FFmpegConverter extends Converter { private ffmpeg: FFmpeg = null!; public name = "ffmpeg"; public ready = $state(false); public supportedFormats = [ - new FormatInfo("mp3", true, true), - new FormatInfo("wav", true, true), - new FormatInfo("flac", true, true), - new FormatInfo("ogg", true, true), - new FormatInfo("aac", true, true), - new FormatInfo("m4a", true, true), - new FormatInfo("wma", true, true), - new FormatInfo("amr", true, true), - new FormatInfo("ac3", true, true), - new FormatInfo("alac", true, true), - new FormatInfo("aiff", true, true), + new FormatInfo("mp3"), + new FormatInfo("wav"), + new FormatInfo("flac"), + new FormatInfo("ogg"), + new FormatInfo("aac"), + new FormatInfo("m4a"), + // TODO: audio to video where it uses the album cover + ...videoFormats.map((f) => new FormatInfo(f, true, true, false)), + new FormatInfo("wma"), + new FormatInfo("amr"), + new FormatInfo("ac3"), + new FormatInfo("alac"), + new FormatInfo("aiff"), ]; public readonly reportsProgress = true; @@ -57,6 +61,9 @@ export class FFmpegConverter extends Converter { ffmpeg.on("progress", (progress) => { input.progress = progress.progress * 100; }); + ffmpeg.on("log", (l) => { + log(["converters", this.name], l.message); + }); const baseURL = "https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.6/dist/esm"; await ffmpeg.load({ @@ -69,7 +76,33 @@ export class FFmpegConverter extends Converter { ["converters", this.name], `wrote ${input.name} to ffmpeg virtual fs`, ); - await ffmpeg.exec(["-i", "input", "output" + to]); + if (videoFormats.includes(input.from.slice(1))) { + // create an audio track from the video + await ffmpeg.exec(["-i", "input", "-map", "0:a:0", "output" + to]); + } else if (videoFormats.includes(to.slice(1))) { + // nab the album art + await ffmpeg.exec([ + "-i", + "input", + "-an", + "-vcodec", + "copy", + "cover.png", + ]); + await ffmpeg.exec([ + "-i", + "input", + "-i", + "cover.png", + "-loop", + "1", + ...toArgs(to), + "output" + to, + ]); + } else { + await ffmpeg.exec(["-i", "input", "output" + to]); + } + log(["converters", this.name], `executed ffmpeg command`); const output = (await ffmpeg.readFile( "output" + to, @@ -82,3 +115,49 @@ export class FFmpegConverter extends Converter { return new VertFile(new File([output], input.name), to); } } + +// and here i was, thinking i'd be done with ffmpeg after finishing vertd +// but OH NO we just HAD to have someone suggest to allow album art video generation. +// +// i hate you SO much. +// - love, maddie +const toArgs = (ext: string): string[] => { + const encoder = getEncoder(ext); + const args = ["-c:v", encoder]; + switch (encoder) { + case "libx264": { + args.push( + "-preset", + "ultrafast", + "-crf", + "18", + "-tune", + "stillimage", + "-c:a", + "aac", + ); + break; + } + + case "libvpx": { + args.push("-c:v", "libvpx-vp9", "-c:a", "libvorbis"); + break; + } + } + + return args; +}; + +const getEncoder = (ext: string): string => { + switch (ext) { + case ".mkv": + case ".mp4": + case ".avi": + case ".mov": + return "libx264"; + case ".webm": + return "libvpx"; + default: + return "copy"; + } +}; diff --git a/src/lib/converters/pandoc.svelte.ts b/src/lib/converters/pandoc.svelte.ts index 8984e25..cf1459b 100644 --- a/src/lib/converters/pandoc.svelte.ts +++ b/src/lib/converters/pandoc.svelte.ts @@ -62,18 +62,18 @@ export class PandocConverter extends Converter { } public supportedFormats = [ - new FormatInfo("docx", true, true), - new FormatInfo("doc", true, true), - new FormatInfo("md", true, true), - new FormatInfo("html", true, true), - new FormatInfo("rtf", true, true), - new FormatInfo("csv", true, true), - new FormatInfo("tsv", true, true), - new FormatInfo("json", true, true), - new FormatInfo("rst", true, true), - new FormatInfo("epub", true, true), - new FormatInfo("odt", true, true), - new FormatInfo("docbook", true, true), + new FormatInfo("docx"), + new FormatInfo("doc"), + new FormatInfo("md"), + new FormatInfo("html"), + new FormatInfo("rtf"), + new FormatInfo("csv"), + new FormatInfo("tsv"), + new FormatInfo("json"), + new FormatInfo("rst"), + new FormatInfo("epub"), + new FormatInfo("odt"), + new FormatInfo("docbook"), ]; } diff --git a/src/lib/converters/vertd.svelte.ts b/src/lib/converters/vertd.svelte.ts index 94f9d20..b8b022e 100644 --- a/src/lib/converters/vertd.svelte.ts +++ b/src/lib/converters/vertd.svelte.ts @@ -202,14 +202,14 @@ export class VertdConverter extends Converter { public reportsProgress = true; public supportedFormats = [ - new FormatInfo("mkv", true, true), - new FormatInfo("mp4", true, true), - new FormatInfo("webm", true, true), - new FormatInfo("avi", true, true), - new FormatInfo("wmv", true, true), - new FormatInfo("mov", true, true), - new FormatInfo("gif", true, true), - new FormatInfo("mts", true, true), + new FormatInfo("mkv"), + new FormatInfo("mp4"), + new FormatInfo("webm"), + new FormatInfo("avi"), + new FormatInfo("wmv"), + new FormatInfo("mov"), + new FormatInfo("gif"), + new FormatInfo("mts"), ]; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/lib/converters/vips.svelte.ts b/src/lib/converters/vips.svelte.ts index c8b3c0d..46e7022 100644 --- a/src/lib/converters/vips.svelte.ts +++ b/src/lib/converters/vips.svelte.ts @@ -17,31 +17,31 @@ export class VipsConverter extends Converter { public ready = $state(false); public supportedFormats = [ - new FormatInfo("png", true, true), - new FormatInfo("jpeg", true, true), - new FormatInfo("jpg", true, true), - new FormatInfo("webp", true, true), - new FormatInfo("gif", true, true), + new FormatInfo("png"), + new FormatInfo("jpeg"), + new FormatInfo("jpg"), + new FormatInfo("webp"), + new FormatInfo("gif"), new FormatInfo("ico", true, false), new FormatInfo("cur", true, false), new FormatInfo("ani", true, false), new FormatInfo("heic", true, false), new FormatInfo("nef", true, false), new FormatInfo("cr2", true, false), - new FormatInfo("hdr", true, true), - new FormatInfo("jpe", true, true), + new FormatInfo("hdr"), + new FormatInfo("jpe"), new FormatInfo("dng", true, false), - new FormatInfo("mat", true, true), - new FormatInfo("pbm", true, true), - new FormatInfo("pfm", true, true), - new FormatInfo("pgm", true, true), - new FormatInfo("pnm", true, true), - new FormatInfo("ppm", true, true), + new FormatInfo("mat"), + new FormatInfo("pbm"), + new FormatInfo("pfm"), + new FormatInfo("pgm"), + new FormatInfo("pnm"), + new FormatInfo("ppm"), new FormatInfo("raw", false, true), - new FormatInfo("tif", true, true), - new FormatInfo("tiff", true, true), - new FormatInfo("jfif", true, true), - new FormatInfo("avif", true, true), + new FormatInfo("tif"), + new FormatInfo("tiff"), + new FormatInfo("jfif"), + new FormatInfo("avif"), ]; public readonly reportsProgress = false; diff --git a/src/lib/types/file.svelte.ts b/src/lib/types/file.svelte.ts index f77156c..3ff57c1 100644 --- a/src/lib/types/file.svelte.ts +++ b/src/lib/types/file.svelte.ts @@ -34,11 +34,24 @@ export class VertFile { } public findConverter() { - const converter = this.converters.find( - (converter) => - converter.formatStrings().includes(this.from) && - converter.formatStrings().includes(this.to), - ); + const converter = this.converters.find((converter) => { + if ( + !converter.formatStrings().includes(this.from) || + !converter.formatStrings().includes(this.to) + ) { + return false; + } + + const theirFrom = converter.supportedFormats.find( + (f) => f.name === this.from, + ); + const theirTo = converter.supportedFormats.find( + (f) => f.name === this.to, + ); + if (!theirFrom || !theirTo) return false; + if (!theirFrom.isNative && !theirTo.isNative) return false; + return true; + }); return converter; } diff --git a/src/routes/convert/+page.svelte b/src/routes/convert/+page.svelte index a058220..33932a0 100644 --- a/src/routes/convert/+page.svelte +++ b/src/routes/convert/+page.svelte @@ -227,7 +227,15 @@ - c.formatStrings((f) => f.toSupported), + // c.formatStrings((f) => f.toSupported), + c.supportedFormats.find( + (f) => f.name === file.from, + )?.isNative + ? c.formatStrings((f) => f.toSupported) + : c.formatStrings( + (f) => + f.toSupported && f.isNative, + ), ) .filter( (format) => From ea53fc7b9ba8ea5fedac975abb8a1cb85fa71015 Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Mon, 14 Apr 2025 23:30:31 +0100 Subject: [PATCH 02/18] feat: SOME sorting --- src/routes/convert/+page.svelte | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/routes/convert/+page.svelte b/src/routes/convert/+page.svelte index 33932a0..1862d72 100644 --- a/src/routes/convert/+page.svelte +++ b/src/routes/convert/+page.svelte @@ -231,12 +231,21 @@ c.supportedFormats.find( (f) => f.name === file.from, )?.isNative - ? c.formatStrings((f) => f.toSupported) - : c.formatStrings( + ? c.supportedFormats.filter( + (f) => f.toSupported, + ) + : c.supportedFormats.filter( (f) => f.toSupported && f.isNative, ), ) + .sort((a, b) => { + // sort native formats first + if (a.isNative && !b.isNative) return -1; + if (!a.isNative && b.isNative) return 1; + return 0; + }) + .map((f) => f.name) .filter( (format) => format !== ".svg" && format !== ".heif", From 4d2378e7efaff43cfce25eec4d8687a623840540 Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Mon, 14 Apr 2025 23:54:20 +0100 Subject: [PATCH 03/18] feat: sorting by nativity --- src/lib/converters/index.ts | 13 +++++++++++++ src/lib/store/index.svelte.ts | 18 ++++++++++-------- src/lib/types/file.svelte.ts | 12 ++++++++---- src/routes/convert/+page.svelte | 12 ++++++++---- 4 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/lib/converters/index.ts b/src/lib/converters/index.ts index dd43695..434a08a 100644 --- a/src/lib/converters/index.ts +++ b/src/lib/converters/index.ts @@ -1,3 +1,4 @@ +import type { Converter, FormatInfo } from "./converter.svelte"; import { FFmpegConverter } from "./ffmpeg.svelte"; import { PandocConverter } from "./pandoc.svelte"; import { VertdConverter } from "./vertd.svelte"; @@ -9,3 +10,15 @@ export const converters = [ new VertdConverter(), new PandocConverter(), ]; + +export const byNative = (format: string) => { + return (a: Converter, b: Converter) => { + const aFormat = a.supportedFormats.find((f) => f.name === format); + const bFormat = b.supportedFormats.find((f) => f.name === format); + + if (aFormat && bFormat) { + return aFormat.isNative ? -1 : 1; + } + return 0; + }; +}; diff --git a/src/lib/store/index.svelte.ts b/src/lib/store/index.svelte.ts index bf99690..55f5f5d 100644 --- a/src/lib/store/index.svelte.ts +++ b/src/lib/store/index.svelte.ts @@ -1,5 +1,5 @@ import { browser } from "$app/environment"; -import { converters } from "$lib/converters"; +import { byNative, converters } from "$lib/converters"; import { error, log } from "$lib/logger"; import { VertFile } from "$lib/types"; import { parseBlob, selectCover } from "music-metadata"; @@ -32,11 +32,13 @@ class Files { this.thumbnailQueue.add(async () => { const isAudio = converters .find((c) => c.name === "ffmpeg") - ?.formatStrings() + ?.supportedFormats.filter((f) => f.isNative) + .map((f) => f.name) ?.includes(file.from.toLowerCase()); const isVideo = converters .find((c) => c.name === "vertd") - ?.formatStrings() + ?.supportedFormats.filter((f) => f.isNative) + .map((f) => f.name) ?.includes(file.from.toLowerCase()); try { @@ -120,11 +122,11 @@ class Files { log(["files"], `no extension found for ${file.name}`); return; } - const converter = converters.find((c) => - c - .formatStrings() - .includes(format || ".somenonexistentextension"), - ); + const converter = converters + .sort(byNative(format)) + .find((converter) => + converter.formatStrings().includes(format), + ); if (!converter) { log(["files"], `no converter found for ${file.name}`); this.files.push(new VertFile(file, format)); diff --git a/src/lib/types/file.svelte.ts b/src/lib/types/file.svelte.ts index 3ff57c1..8d59959 100644 --- a/src/lib/types/file.svelte.ts +++ b/src/lib/types/file.svelte.ts @@ -1,4 +1,4 @@ -import { converters } from "$lib/converters"; +import { byNative, converters } from "$lib/converters"; import type { Converter } from "$lib/converters/converter.svelte"; import { error } from "$lib/logger"; import { addToast } from "$lib/store/ToastProvider"; @@ -27,9 +27,13 @@ export class VertFile { public converters: Converter[] = []; public findConverters(supportedFormats: string[] = [this.from]) { - const converter = this.converters.filter((converter) => - converter.formatStrings().map((f) => supportedFormats.includes(f)), - ); + const converter = this.converters + .filter((converter) => + converter + .formatStrings() + .map((f) => supportedFormats.includes(f)), + ) + .sort(byNative(this.from)); return converter; } diff --git a/src/routes/convert/+page.svelte b/src/routes/convert/+page.svelte index 1862d72..52784bb 100644 --- a/src/routes/convert/+page.svelte +++ b/src/routes/convert/+page.svelte @@ -94,19 +94,23 @@ )} {@const isAudio = converters .find((c) => c.name === "ffmpeg") - ?.formatStrings((f) => f.fromSupported) + ?.supportedFormats.filter((f) => f.isNative) + .map((f) => f.name) .includes(file.from)} {@const isVideo = converters .find((c) => c.name === "vertd") - ?.formatStrings((f) => f.fromSupported) + ?.supportedFormats.filter((f) => f.isNative) + .map((f) => f.name) .includes(file.from)} {@const isImage = converters .find((c) => c.name === "libvips") - ?.formatStrings((f) => f.fromSupported) + ?.supportedFormats.filter((f) => f.isNative) + .map((f) => f.name) .includes(file.from)} {@const isDocument = converters .find((c) => c.name === "pandoc") - ?.formatStrings((f) => f.fromSupported) + ?.supportedFormats.filter((f) => f.isNative) + .map((f) => f.name) .includes(file.from)}
From 52ae6fd58a4754c995e2d826fd50e02c3f7714d7 Mon Sep 17 00:00:00 2001 From: azure Date: Mon, 14 Apr 2025 08:43:36 -0400 Subject: [PATCH 04/18] docs: update docker instructions * Update build instructions to use fully lowercase image tag * Map 3000/tcp (host) to 80/tcp (container) instead * Add `docker run` command explanation * Add pre-built image instructions --- README.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dd16edb..c06cadb 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ If using nginx, you can use the [nginx.conf](./nginx.conf) file as a starting po Clone the repository, then build a Docker image with: ```shell -$ docker build -t VERT-sh/vert \ +$ docker build -t vert-sh/vert \ --build-arg PUB_ENV=production \ --build-arg PUB_HOSTNAME=vert.sh \ --build-arg PUB_PLAUSIBLE_URL=https://plausible.example.com . @@ -67,10 +67,25 @@ $ docker build -t VERT-sh/vert \ You can then run it by using: ```shell -$ docker run --restart unless-stopped -p 3000:3000 -d --name "vert" VERT-sh/vert +$ docker run -d \ + --restart unless-stopped \ + -p 3000:80 \ + --name "vert" \ + vert-sh/vert ``` -We also have a `docker-compose.yml` file available. Use `docker compose up` if you want to start the stack, or `docker compose down` to bring it down. You can pass `--build` to `docker compose up` to rebuild the Docker image (useful if you've changed any of the environment variables) as well as `-d` to start it in detached mode. You can read more about Docker Compose in general [here](https://docs.docker.com/compose/intro/compose-application-model/). +This will run the image in dettached mode, restart the container unless it is manually stopped, map 3000/tcp (host) to 80/tcp (container), name the container `vert` and use the previously built image. + +We also have a [`docker-compose.yml`](./docker-compose.yml) file available. Use `docker compose up` if you want to start the stack, or `docker compose down` to bring it down. You can pass `--build` to `docker compose up` to rebuild the Docker image (useful if you've changed any of the environment variables) as well as `-d` to start it in detached mode. You can read more about Docker Compose in general [here](https://docs.docker.com/compose/intro/compose-application-model/). + +While there's an image you can pull instead of having to clone this repository and build the image yourself, you will not be able to update any of the environment variables (e.g. `PUB_PLAUSIBLE_URL`) as they're baked directly into the image and not obtained during runtime. If you're okay with this, you can simply run this command instead: +```shell +$ docker run -d \ + --restart unless-stopped \ + -p 3000:80 \ + --name "vert" \ + ghcr.io/vert-sh/vert:latest +``` ## License From 68875d4d3bf18080c6d06d1afd66ca2a1b18d15f Mon Sep 17 00:00:00 2001 From: azure Date: Mon, 14 Apr 2025 15:26:12 -0400 Subject: [PATCH 05/18] docs: apply suggested changes --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c06cadb..cad49ee 100644 --- a/README.md +++ b/README.md @@ -74,11 +74,14 @@ $ docker run -d \ vert-sh/vert ``` -This will run the image in dettached mode, restart the container unless it is manually stopped, map 3000/tcp (host) to 80/tcp (container), name the container `vert` and use the previously built image. +This will do the following: +- Use the previously built image as the container `vert`, in detached mode +- Continuously restart the container until manually stopped +- Map `3000/tcp` (host) to `80/tcp` (container) We also have a [`docker-compose.yml`](./docker-compose.yml) file available. Use `docker compose up` if you want to start the stack, or `docker compose down` to bring it down. You can pass `--build` to `docker compose up` to rebuild the Docker image (useful if you've changed any of the environment variables) as well as `-d` to start it in detached mode. You can read more about Docker Compose in general [here](https://docs.docker.com/compose/intro/compose-application-model/). -While there's an image you can pull instead of having to clone this repository and build the image yourself, you will not be able to update any of the environment variables (e.g. `PUB_PLAUSIBLE_URL`) as they're baked directly into the image and not obtained during runtime. If you're okay with this, you can simply run this command instead: +While there's an image you can pull instead of cloning the repo and building the image yourself, you will not be able to update any of the environment variables (e.g. `PUB_PLAUSIBLE_URL`) as they're baked directly into the image and not obtained during runtime. If you're okay with this, you can simply run this command instead: ```shell $ docker run -d \ --restart unless-stopped \ From fc6e14109ca89cef8f72ac0fad53afbe4aa11e2c Mon Sep 17 00:00:00 2001 From: azure Date: Mon, 14 Apr 2025 15:41:54 -0400 Subject: [PATCH 06/18] fix(compose): make image name fully lowercase --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index ce2c8b6..13afdea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: vert: container_name: vert - image: VERT-sh/vert:latest + image: vert-sh/vert:latest environment: - PUB_HOSTNAME=${PUB_HOSTNAME:-vert.sh} - PUB_PLAUSIBLE_URL=${PUB_PLAUSIBLE_URL:-https://plausible.example.com} From db7b9406a72d54fb8c44e82bbbf70fabe100cf89 Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Tue, 15 Apr 2025 15:11:05 +0100 Subject: [PATCH 07/18] feat: .icns support --- bun.lock | 7 + package.json | 1 + src/app.html | 94 ++-- src/lib/components/layout/Navbar/Base.svelte | 394 ++++++------- src/lib/converters/index.ts | 2 +- src/lib/parse/ani.ts | 300 +++++----- src/lib/types/conversion-worker.ts | 64 +-- src/lib/workers/vips.ts | 562 ++++++++++--------- src/routes/jpegify/+page.svelte | 226 ++++---- 9 files changed, 832 insertions(+), 818 deletions(-) diff --git a/bun.lock b/bun.lock index 94833c0..1a41b8b 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "@bjorn3/browser_wasi_shim": "^0.4.1", "@ffmpeg/ffmpeg": "^0.12.15", "@ffmpeg/util": "^0.12.2", + "@fiahfy/icns": "^0.0.7", "@fontsource/azeret-mono": "^5.1.1", "@fontsource/lexend": "^5.1.2", "@fontsource/radio-canada-big": "^5.1.1", @@ -120,6 +121,10 @@ "@ffmpeg/util": ["@ffmpeg/util@0.12.2", "", {}, "sha512-ouyoW+4JB7WxjeZ2y6KpRvB+dLp7Cp4ro8z0HIVpZVCM7AwFlHa0c4R8Y/a4M3wMqATpYKhC7lSFHQ0T11MEDw=="], + "@fiahfy/icns": ["@fiahfy/icns@0.0.7", "", { "dependencies": { "@fiahfy/packbits": "^0.0.6", "pngjs": "^6.0.0" } }, "sha512-0apAtbUXTU3Opy/Z4h69o53voBa+am8FmdZauyagUMskAVYN1a5yIRk48Sf+tEdBLlefbvqLWPJ4pxr/Y/QtTg=="], + + "@fiahfy/packbits": ["@fiahfy/packbits@0.0.6", "", {}, "sha512-XuhF/edg+iIvXjkCWgfj6fWtRi/KrEPg2ILXj1l86EN4EssuOiPcLKgkMDr9cL8jTGtVd/MKUWW6Y0/ZVf1PGA=="], + "@fontsource/azeret-mono": ["@fontsource/azeret-mono@5.2.5", "", {}, "sha512-GRzKYuD1CVOS6Jag/ohDCycLV9a3TK6y1T73A8q0JoDZTVO85DNapqLK+SV2gYtTFldahNAlDSIaizv9MLhR1A=="], "@fontsource/lexend": ["@fontsource/lexend@5.2.5", "", {}, "sha512-Mv2XQ+B4ek2lNCGRW5ddLTW8T3xTT17AnCk1IETpoef57XHz+e42fUfLAYMrmiJLOGpR44qnyJ5S6D323A5EIw=="], @@ -608,6 +613,8 @@ "pirates": ["pirates@4.0.6", "", {}, "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="], + "pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="], + "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], diff --git a/package.json b/package.json index 38ce68c..6eeb872 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@bjorn3/browser_wasi_shim": "^0.4.1", "@ffmpeg/ffmpeg": "^0.12.15", "@ffmpeg/util": "^0.12.2", + "@fiahfy/icns": "^0.0.7", "@fontsource/azeret-mono": "^5.1.1", "@fontsource/lexend": "^5.1.2", "@fontsource/radio-canada-big": "^5.1.1", diff --git a/src/app.html b/src/app.html index ef2158d..a8f439a 100644 --- a/src/app.html +++ b/src/app.html @@ -1,47 +1,47 @@ - - - - - - - - - - - - - - %sveltekit.head% - - - -
%sveltekit.body%
- - + + + + + + + + + + + + + + %sveltekit.head% + + + +
%sveltekit.body%
+ + diff --git a/src/lib/components/layout/Navbar/Base.svelte b/src/lib/components/layout/Navbar/Base.svelte index 1e2d4ca..515bae3 100644 --- a/src/lib/components/layout/Navbar/Base.svelte +++ b/src/lib/components/layout/Navbar/Base.svelte @@ -1,197 +1,197 @@ - - -{#snippet link(item: (typeof items)[0], index: number)} - {@const Icon = item.icon} - -
- {#key item.name} -
-
- - {#if item.badge} -
- {#key item.badge} -
- {item.badge} -
- {/key} -
- {/if} -
- -
- {/key} -
-
-{/snippet} - -
- - {@const linkRect = linkRects.at(selectedIndex) || linkRects[0]} - {#if linkRect} -
- {/if} - - {#each items as item, i (item.url)} - {@render link(item, i)} - {/each} - - -
-
+ + +{#snippet link(item: (typeof items)[0], index: number)} + {@const Icon = item.icon} + +
+ {#key item.name} +
+
+ + {#if item.badge} +
+ {#key item.badge} +
+ {item.badge} +
+ {/key} +
+ {/if} +
+ +
+ {/key} +
+
+{/snippet} + +
+ + {@const linkRect = linkRects.at(selectedIndex) || linkRects[0]} + {#if linkRect} +
+ {/if} + + {#each items as item, i (item.url)} + {@render link(item, i)} + {/each} + + +
+
diff --git a/src/lib/converters/index.ts b/src/lib/converters/index.ts index 434a08a..3ccb073 100644 --- a/src/lib/converters/index.ts +++ b/src/lib/converters/index.ts @@ -1,4 +1,4 @@ -import type { Converter, FormatInfo } from "./converter.svelte"; +import type { Converter } from "./converter.svelte"; import { FFmpegConverter } from "./ffmpeg.svelte"; import { PandocConverter } from "./pandoc.svelte"; import { VertdConverter } from "./vertd.svelte"; diff --git a/src/lib/parse/ani.ts b/src/lib/parse/ani.ts index 814b069..09b9712 100644 --- a/src/lib/parse/ani.ts +++ b/src/lib/parse/ani.ts @@ -1,150 +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( - 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 }; -} +// 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( + 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 }; +} diff --git a/src/lib/types/conversion-worker.ts b/src/lib/types/conversion-worker.ts index 14c8346..820f2ae 100644 --- a/src/lib/types/conversion-worker.ts +++ b/src/lib/types/conversion-worker.ts @@ -1,32 +1,32 @@ -import { VertFile } from "./file.svelte"; - -interface ConvertMessage { - type: "convert"; - input: VertFile; - to: string; - compression: number | null; -} - -interface FinishedMessage { - type: "finished"; - output: ArrayBufferLike; - zip?: boolean; -} - -interface LoadedMessage { - type: "loaded"; -} - -interface ErrorMessage { - type: "error"; - error: string; -} - -export type WorkerMessage = ( - | ConvertMessage - | FinishedMessage - | LoadedMessage - | ErrorMessage -) & { - id: number; -}; +import { VertFile } from "./file.svelte"; + +interface ConvertMessage { + type: "convert"; + input: VertFile; + to: string; + compression: number | null; +} + +interface FinishedMessage { + type: "finished"; + output: ArrayBufferLike; + zip?: boolean; +} + +interface LoadedMessage { + type: "loaded"; +} + +interface ErrorMessage { + type: "error"; + error: string; +} + +export type WorkerMessage = ( + | ConvertMessage + | FinishedMessage + | LoadedMessage + | ErrorMessage +) & { + id: number; +}; diff --git a/src/lib/workers/vips.ts b/src/lib/workers/vips.ts index c73abdf..8c76276 100644 --- a/src/lib/workers/vips.ts +++ b/src/lib/workers/vips.ts @@ -1,278 +1,284 @@ -import Vips from "wasm-vips"; -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: [], -}); - -const magickPromise = initializeImageMagick(new URL(wasm, import.meta.url)); - -const magickRequiredFormats = [ - ".dng", - ".heic", - ".ico", - ".cur", - ".ani", - ".cr2", - ".nef", -]; -const unsupportedFrom: string[] = []; -const unsupportedTo = [...magickRequiredFormats]; - -vipsPromise - .then(() => { - postMessage({ type: "loaded" }); - }) - .catch((error) => { - postMessage({ type: "error", error }); - }); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const handleMessage = async (message: any): Promise => { - const vips = await vipsPromise; - 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", - error: `Unsupported input format ${message.input.from}`, - }; - } - - if (unsupportedTo.includes(message.to)) { - return { - type: "error", - error: `Unsupported output format ${message.to}`, - }; - } - - const buffer = await message.input.file.arrayBuffer(); - if ( - magickRequiredFormats.includes(message.input.from) || - magickRequiredFormats.includes(message.to) - ) { - // only wait when we need to - await magickPromise; - - // 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 - .slice(1) - .toUpperCase() as MagickFormat, - }), - ); - - const converted = await magickConvert(img, message.to); - - return { - type: "finished", - output: converted, - }; - } - - let image = vips.Image.newFromBuffer(buffer, ""); - - // check if animated image & keep it animated when converting - if (image.getTypeof("n-pages") > 0) { - image = vips.Image.newFromBuffer(buffer, "[n=-1]"); - } - - const opts: { [key: string]: string } = {}; - if (typeof message.compression !== "undefined") { - opts["Q"] = Math.min(100, message.compression + 1).toString(); - } - - const output = image.writeToBuffer(message.to, opts); - image.delete(); - return { - type: "finished", - output: output.buffer, - }; - } - } -}; - -const readToEnd = async (reader: ReadableStreamDefaultReader) => { - 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 => { - const canvas = new OffscreenCanvas(img.width, img.height); - return new Promise((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 { - const res = await handleMessage(message); - if (!res) return; - postMessage({ - ...res, - id: message.id, - }); - } catch (e) { - postMessage({ - type: "error", - error: e, - id: message.id, - }); - } -}; +import Vips from "wasm-vips"; +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"; +import { Icns } from "@fiahfy/icns/dist"; + +const vipsPromise = Vips({ + dynamicLibraries: [], +}); + +const magickPromise = initializeImageMagick(new URL(wasm, import.meta.url)); + +const magickRequiredFormats = [ + ".dng", + ".heic", + ".ico", + ".cur", + ".ani", + ".cr2", + ".nef", +]; +const unsupportedFrom: string[] = []; +const unsupportedTo = [...magickRequiredFormats]; + +vipsPromise + .then(() => { + postMessage({ type: "loaded" }); + }) + .catch((error) => { + postMessage({ type: "error", error }); + }); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const handleMessage = async (message: any): Promise => { + const vips = await vipsPromise; + 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", + error: `Unsupported input format ${message.input.from}`, + }; + } + + if (unsupportedTo.includes(message.to)) { + return { + type: "error", + error: `Unsupported output format ${message.to}`, + }; + } + + const buffer = await message.input.file.arrayBuffer(); + if ( + magickRequiredFormats.includes(message.input.from) || + magickRequiredFormats.includes(message.to) + ) { + // only wait when we need to + await magickPromise; + + // 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 + .slice(1) + .toUpperCase() as MagickFormat, + }), + ); + + const converted = await magickConvert(img, message.to); + + return { + type: "finished", + output: converted, + }; + } + + if (message.input.from === ".icns") { + const icns = Icns.from(new Uint8Array(buffer)); + console.log(icns); + } + + let image = vips.Image.newFromBuffer(buffer, ""); + + // check if animated image & keep it animated when converting + if (image.getTypeof("n-pages") > 0) { + image = vips.Image.newFromBuffer(buffer, "[n=-1]"); + } + + const opts: { [key: string]: string } = {}; + if (typeof message.compression !== "undefined") { + opts["Q"] = Math.min(100, message.compression + 1).toString(); + } + + const output = image.writeToBuffer(message.to, opts); + image.delete(); + return { + type: "finished", + output: output.buffer, + }; + } + } +}; + +const readToEnd = async (reader: ReadableStreamDefaultReader) => { + 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 => { + const canvas = new OffscreenCanvas(img.width, img.height); + return new Promise((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 { + const res = await handleMessage(message); + if (!res) return; + postMessage({ + ...res, + id: message.id, + }); + } catch (e) { + postMessage({ + type: "error", + error: e, + id: message.id, + }); + } +}; diff --git a/src/routes/jpegify/+page.svelte b/src/routes/jpegify/+page.svelte index 0937a81..739abdd 100644 --- a/src/routes/jpegify/+page.svelte +++ b/src/routes/jpegify/+page.svelte @@ -1,113 +1,113 @@ - - -
-

SECRET JPEGIFY!!!

-

- (shh... don't tell anyone!) -

- - - -
- {#each images as file, i (file.id)} -
- -
- {file.name} - {file.name} -
-
- - -
-
-
- {/each} -
-
+ + +
+

SECRET JPEGIFY!!!

+

+ (shh... don't tell anyone!) +

+ + + +
+ {#each images as file, i (file.id)} +
+ +
+ {file.name} + {file.name} +
+
+ + +
+
+
+ {/each} +
+
From d3d1f6e9e63dc78f994458cb6c9e5323816480c9 Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Tue, 15 Apr 2025 15:11:13 +0100 Subject: [PATCH 08/18] feat: oops wait no THIS is icns support --- bun.lock | 13 ++++--- package.json | 3 +- src/lib/converters/index.ts | 1 - src/lib/converters/vips.svelte.ts | 3 +- src/lib/parse/icns/index.ts | 0 src/lib/workers/vips.ts | 58 +++++++++++++++++++++++++++---- vite.config.ts | 2 ++ 7 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 src/lib/parse/icns/index.ts diff --git a/bun.lock b/bun.lock index 1a41b8b..805d549 100644 --- a/bun.lock +++ b/bun.lock @@ -7,7 +7,6 @@ "@bjorn3/browser_wasi_shim": "^0.4.1", "@ffmpeg/ffmpeg": "^0.12.15", "@ffmpeg/util": "^0.12.2", - "@fiahfy/icns": "^0.0.7", "@fontsource/azeret-mono": "^5.1.1", "@fontsource/lexend": "^5.1.2", "@fontsource/radio-canada-big": "^5.1.1", @@ -19,7 +18,9 @@ "music-metadata": "^11.0.0", "p-queue": "^8.1.0", "riff-file": "^1.0.3", + "vert-wasm": "^0.0.2", "vite-plugin-static-copy": "^2.2.0", + "vite-plugin-wasm": "^3.4.1", "wasm-vips": "^0.0.11", }, "devDependencies": { @@ -121,10 +122,6 @@ "@ffmpeg/util": ["@ffmpeg/util@0.12.2", "", {}, "sha512-ouyoW+4JB7WxjeZ2y6KpRvB+dLp7Cp4ro8z0HIVpZVCM7AwFlHa0c4R8Y/a4M3wMqATpYKhC7lSFHQ0T11MEDw=="], - "@fiahfy/icns": ["@fiahfy/icns@0.0.7", "", { "dependencies": { "@fiahfy/packbits": "^0.0.6", "pngjs": "^6.0.0" } }, "sha512-0apAtbUXTU3Opy/Z4h69o53voBa+am8FmdZauyagUMskAVYN1a5yIRk48Sf+tEdBLlefbvqLWPJ4pxr/Y/QtTg=="], - - "@fiahfy/packbits": ["@fiahfy/packbits@0.0.6", "", {}, "sha512-XuhF/edg+iIvXjkCWgfj6fWtRi/KrEPg2ILXj1l86EN4EssuOiPcLKgkMDr9cL8jTGtVd/MKUWW6Y0/ZVf1PGA=="], - "@fontsource/azeret-mono": ["@fontsource/azeret-mono@5.2.5", "", {}, "sha512-GRzKYuD1CVOS6Jag/ohDCycLV9a3TK6y1T73A8q0JoDZTVO85DNapqLK+SV2gYtTFldahNAlDSIaizv9MLhR1A=="], "@fontsource/lexend": ["@fontsource/lexend@5.2.5", "", {}, "sha512-Mv2XQ+B4ek2lNCGRW5ddLTW8T3xTT17AnCk1IETpoef57XHz+e42fUfLAYMrmiJLOGpR44qnyJ5S6D323A5EIw=="], @@ -613,8 +610,6 @@ "pirates": ["pirates@4.0.6", "", {}, "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="], - "pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="], - "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], @@ -739,10 +734,14 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "vert-wasm": ["vert-wasm@0.0.2", "", {}, "sha512-QCkhkATZkWp3OPpIrlcLVd5S4DQXeDEWIPI1eV+Ku+sUVoCM4s9DQbNKtnJ8s4leMW3AD4E7cY/gXB/9TsI3WA=="], + "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=="], "vite-plugin-static-copy": ["vite-plugin-static-copy@2.3.0", "", { "dependencies": { "chokidar": "^3.5.3", "fast-glob": "^3.2.11", "fs-extra": "^11.1.0", "p-map": "^7.0.3", "picocolors": "^1.0.0" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0" } }, "sha512-LLKwhhHetGaCnWz4mas4qqjjguDka6/6b4+SeIohRroj8aCE7QTfiZECfPecslFQkWZ3HdQuq5kOPmWZjNYlKA=="], + "vite-plugin-wasm": ["vite-plugin-wasm@3.4.1", "", { "peerDependencies": { "vite": "^2 || ^3 || ^4 || ^5 || ^6" } }, "sha512-ja3nSo2UCkVeitltJGkS3pfQHAanHv/DqGatdI39ja6McgABlpsZ5hVgl6wuR8Qx5etY3T5qgDQhOWzc5RReZA=="], + "vitefu": ["vitefu@1.0.6", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["vite"] }, "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA=="], "wasm-vips": ["wasm-vips@0.0.11", "", {}, "sha512-bzFU7WcimMY4WeqnZk7whKVpSXxpagISXPJwsk2VHF4lgIN9rl4uUo5sF9x6jOlACuCH6ITZUJ7QPTYmy60NCQ=="], diff --git a/package.json b/package.json index 6eeb872..384b169 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "@bjorn3/browser_wasi_shim": "^0.4.1", "@ffmpeg/ffmpeg": "^0.12.15", "@ffmpeg/util": "^0.12.2", - "@fiahfy/icns": "^0.0.7", "@fontsource/azeret-mono": "^5.1.1", "@fontsource/lexend": "^5.1.2", "@fontsource/radio-canada-big": "^5.1.1", @@ -49,7 +48,9 @@ "music-metadata": "^11.0.0", "p-queue": "^8.1.0", "riff-file": "^1.0.3", + "vert-wasm": "^0.0.2", "vite-plugin-static-copy": "^2.2.0", + "vite-plugin-wasm": "^3.4.1", "wasm-vips": "^0.0.11" } } diff --git a/src/lib/converters/index.ts b/src/lib/converters/index.ts index 3ccb073..d5e60a0 100644 --- a/src/lib/converters/index.ts +++ b/src/lib/converters/index.ts @@ -1,4 +1,3 @@ -import type { Converter } from "./converter.svelte"; import { FFmpegConverter } from "./ffmpeg.svelte"; import { PandocConverter } from "./pandoc.svelte"; import { VertdConverter } from "./vertd.svelte"; diff --git a/src/lib/converters/vips.svelte.ts b/src/lib/converters/vips.svelte.ts index 46e7022..c874add 100644 --- a/src/lib/converters/vips.svelte.ts +++ b/src/lib/converters/vips.svelte.ts @@ -22,10 +22,11 @@ export class VipsConverter extends Converter { new FormatInfo("jpg"), new FormatInfo("webp"), new FormatInfo("gif"), + new FormatInfo("heic", true, false), new FormatInfo("ico", true, false), new FormatInfo("cur", true, false), new FormatInfo("ani", true, false), - new FormatInfo("heic", true, false), + new FormatInfo("icns", true, false), new FormatInfo("nef", true, false), new FormatInfo("cr2", true, false), new FormatInfo("hdr"), diff --git a/src/lib/parse/icns/index.ts b/src/lib/parse/icns/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/workers/vips.ts b/src/lib/workers/vips.ts index 8c76276..f38bede 100644 --- a/src/lib/workers/vips.ts +++ b/src/lib/workers/vips.ts @@ -10,7 +10,7 @@ import { import { makeZip } from "client-zip"; import wasm from "@imagemagick/magick-wasm/magick.wasm?url"; import { parseAni } from "$lib/parse/ani"; -import { Icns } from "@fiahfy/icns/dist"; +import { parseIcns } from "vert-wasm"; const vipsPromise = Vips({ dynamicLibraries: [], @@ -158,8 +158,6 @@ const handleMessage = async (message: any): Promise => { } } - console.log(message.input.from); - const img = MagickImage.create( new Uint8Array(buffer), new MagickReadSettings({ @@ -178,11 +176,59 @@ const handleMessage = async (message: any): Promise => { } if (message.input.from === ".icns") { - const icns = Icns.from(new Uint8Array(buffer)); - console.log(icns); + const icns: Uint8Array[] = parseIcns(new Uint8Array(buffer)); + // Result in vert-wasm maps to a string in JS + if (typeof icns === "string") { + return { + type: "error", + error: `Failed to read ICNS -- ${icns}`, + }; + } + const formats = [ + MagickFormat.Png, + MagickFormat.Jpeg, + MagickFormat.Rgba, + MagickFormat.Rgb, + ]; + const outputs: Uint8Array[] = []; + for (const file of icns) { + for (const format of formats) { + try { + const img = MagickImage.create( + file, + new MagickReadSettings({ + format: format, + }), + ); + const converted = await magickConvert( + img, + message.to, + ); + outputs.push(converted); + break; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { + continue; + } + } + } + + const zip = makeZip( + outputs.map( + (img, i) => + new File([img], `image${i}.${message.to.slice(1)}`), + ), + "images.zip", + ); + const zipBytes = await readToEnd(zip.getReader()); + return { + type: "finished", + output: zipBytes, + zip: true, + }; } - let image = vips.Image.newFromBuffer(buffer, ""); + let image = vips.Image.newFromBuffer(buffer); // check if animated image & keep it animated when converting if (image.getTypeof("n-pages") > 0) { diff --git a/vite.config.ts b/vite.config.ts index c4ec1e8..47d9978 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,6 +2,7 @@ import { sveltekit } from "@sveltejs/kit/vite"; import { defineConfig } from "vite"; import { viteStaticCopy } from "vite-plugin-static-copy"; import svg from "@poppanator/sveltekit-svg"; +import wasm from "vite-plugin-wasm"; export default defineConfig({ plugins: [ @@ -44,6 +45,7 @@ export default defineConfig({ }, ], }), + wasm(), ], optimizeDeps: { exclude: [ From 39d83f65f0dc889e2eb14a0ceb761f0b77eece16 Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Tue, 15 Apr 2025 15:15:10 +0100 Subject: [PATCH 09/18] fix: i'm so stupid --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 47d9978..3c3ab3b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,6 +6,7 @@ import wasm from "vite-plugin-wasm"; export default defineConfig({ plugins: [ + wasm(), sveltekit(), { name: "vips-request-middleware", @@ -45,7 +46,6 @@ export default defineConfig({ }, ], }), - wasm(), ], optimizeDeps: { exclude: [ From eef8e818729f2b3320fa78d893bca617c5c9574a Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:34:23 +0100 Subject: [PATCH 10/18] fix: build?? --- bun.lock | 33 +++++++++++++++++++++++++++++ package.json | 3 ++- src/lib/converters/pandoc.svelte.ts | 6 ++++-- vite.config.ts | 9 +++++++- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index 805d549..98bb611 100644 --- a/bun.lock +++ b/bun.lock @@ -44,6 +44,7 @@ "typescript": "^5.7.3", "typescript-eslint": "^8.20.0", "vite": "^5.4.11", + "vite-plugin-top-level-await": "^1.5.0", }, }, }, @@ -190,6 +191,8 @@ "@poppanator/sveltekit-svg": ["@poppanator/sveltekit-svg@5.0.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0" }, "peerDependencies": { "svelte": ">=5.x", "svgo": ">=3.x", "vite": ">=5.x" } }, "sha512-b7hk55SF0HjTS+xFgMG20hy6W0F/m+yRA/ZWcjnsa391rB3Ys3desCiUyIKQYcfvcyuRiQCPedUJMYgu00VdCA=="], + "@rollup/plugin-virtual": ["@rollup/plugin-virtual@3.0.2", "", { "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A=="], + "@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.34.9", "", { "os": "android", "cpu": "arm" }, "sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA=="], @@ -242,6 +245,32 @@ "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@3.0.1", "", { "dependencies": { "debug": "^4.3.7" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^4.0.0-next.0||^4.0.0", "svelte": "^5.0.0-next.96 || ^5.0.0", "vite": "^5.0.0" } }, "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ=="], + "@swc/core": ["@swc/core@1.11.21", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.21" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.11.21", "@swc/core-darwin-x64": "1.11.21", "@swc/core-linux-arm-gnueabihf": "1.11.21", "@swc/core-linux-arm64-gnu": "1.11.21", "@swc/core-linux-arm64-musl": "1.11.21", "@swc/core-linux-x64-gnu": "1.11.21", "@swc/core-linux-x64-musl": "1.11.21", "@swc/core-win32-arm64-msvc": "1.11.21", "@swc/core-win32-ia32-msvc": "1.11.21", "@swc/core-win32-x64-msvc": "1.11.21" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-/Y3BJLcwd40pExmdar8MH2UGGvCBrqNN7hauOMckrEX2Ivcbv3IMhrbGX4od1dnF880Ed8y/E9aStZCIQi0EGw=="], + + "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.11.21", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v6gjw9YFWvKulCw3ZA1dY+LGMafYzJksm1mD4UZFZ9b36CyHFowYVYug1ajYRIRqEvvfIhHUNV660zTLoVFR8g=="], + + "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.11.21", "", { "os": "darwin", "cpu": "x64" }, "sha512-CUiTiqKlzskwswrx9Ve5NhNoab30L1/ScOfQwr1duvNlFvarC8fvQSgdtpw2Zh3MfnfNPpyLZnYg7ah4kbT9JQ=="], + + "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.11.21", "", { "os": "linux", "cpu": "arm" }, "sha512-YyBTAFM/QPqt1PscD8hDmCLnqPGKmUZpqeE25HXY8OLjl2MUs8+O4KjwPZZ+OGxpdTbwuWFyMoxjcLy80JODvg=="], + + "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.11.21", "", { "os": "linux", "cpu": "arm64" }, "sha512-DQD+ooJmwpNsh4acrftdkuwl5LNxxg8U4+C/RJNDd7m5FP9Wo4c0URi5U0a9Vk/6sQNh9aSGcYChDpqCDWEcBw=="], + + "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.11.21", "", { "os": "linux", "cpu": "arm64" }, "sha512-y1L49+snt1a1gLTYPY641slqy55QotPdtRK9Y6jMi4JBQyZwxC8swWYlQWb+MyILwxA614fi62SCNZNznB3XSA=="], + + "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.11.21", "", { "os": "linux", "cpu": "x64" }, "sha512-NesdBXv4CvVEaFUlqKj+GA4jJMNUzK2NtKOrUNEtTbXaVyNiXjFCSaDajMTedEB0jTAd9ybB0aBvwhgkJUWkWA=="], + + "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.11.21", "", { "os": "linux", "cpu": "x64" }, "sha512-qFV60pwpKVOdmX67wqQzgtSrUGWX9Cibnp1CXyqZ9Mmt8UyYGvmGu7p6PMbTyX7vdpVUvWVRf8DzrW2//wmVHg=="], + + "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.11.21", "", { "os": "win32", "cpu": "arm64" }, "sha512-DJJe9k6gXR/15ZZVLv1SKhXkFst8lYCeZRNHH99SlBodvu4slhh/MKQ6YCixINRhCwliHrpXPym8/5fOq8b7Ig=="], + + "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.11.21", "", { "os": "win32", "cpu": "ia32" }, "sha512-TqEXuy6wedId7bMwLIr9byds+mKsaXVHctTN88R1UIBPwJA92Pdk0uxDgip0pEFzHB/ugU27g6d8cwUH3h2eIw=="], + + "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.11.21", "", { "os": "win32", "cpu": "x64" }, "sha512-BT9BNNbMxdpUM1PPAkYtviaV0A8QcXttjs2MDtOeSqqvSJaPtyM+Fof2/+xSwQDmDEFzbGCcn75M5+xy3lGqpA=="], + + "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], + + "@swc/types": ["@swc/types@0.1.21", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ=="], + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], "@trysound/sax": ["@trysound/sax@0.2.0", "", {}, "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="], @@ -734,12 +763,16 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], + "vert-wasm": ["vert-wasm@0.0.2", "", {}, "sha512-QCkhkATZkWp3OPpIrlcLVd5S4DQXeDEWIPI1eV+Ku+sUVoCM4s9DQbNKtnJ8s4leMW3AD4E7cY/gXB/9TsI3WA=="], "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=="], "vite-plugin-static-copy": ["vite-plugin-static-copy@2.3.0", "", { "dependencies": { "chokidar": "^3.5.3", "fast-glob": "^3.2.11", "fs-extra": "^11.1.0", "p-map": "^7.0.3", "picocolors": "^1.0.0" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0" } }, "sha512-LLKwhhHetGaCnWz4mas4qqjjguDka6/6b4+SeIohRroj8aCE7QTfiZECfPecslFQkWZ3HdQuq5kOPmWZjNYlKA=="], + "vite-plugin-top-level-await": ["vite-plugin-top-level-await@1.5.0", "", { "dependencies": { "@rollup/plugin-virtual": "^3.0.2", "@swc/core": "^1.10.16", "uuid": "^10.0.0" }, "peerDependencies": { "vite": ">=2.8" } }, "sha512-r/DtuvHrSqUVk23XpG2cl8gjt1aATMG5cjExXL1BUTcSNab6CzkcPua9BPEc9fuTP5UpwClCxUe3+dNGL0yrgQ=="], + "vite-plugin-wasm": ["vite-plugin-wasm@3.4.1", "", { "peerDependencies": { "vite": "^2 || ^3 || ^4 || ^5 || ^6" } }, "sha512-ja3nSo2UCkVeitltJGkS3pfQHAanHv/DqGatdI39ja6McgABlpsZ5hVgl6wuR8Qx5etY3T5qgDQhOWzc5RReZA=="], "vitefu": ["vitefu@1.0.6", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["vite"] }, "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA=="], diff --git a/package.json b/package.json index 384b169..697d6c9 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "tailwindcss": "^3.4.17", "typescript": "^5.7.3", "typescript-eslint": "^8.20.0", - "vite": "^5.4.11" + "vite": "^5.4.11", + "vite-plugin-top-level-await": "^1.5.0" }, "dependencies": { "@bjorn3/browser_wasi_shim": "^0.4.1", diff --git a/src/lib/converters/pandoc.svelte.ts b/src/lib/converters/pandoc.svelte.ts index cf1459b..db5b6e0 100644 --- a/src/lib/converters/pandoc.svelte.ts +++ b/src/lib/converters/pandoc.svelte.ts @@ -1,7 +1,7 @@ import { VertFile } from "$lib/types"; import { Converter, FormatInfo } from "./converter.svelte"; import { browser } from "$app/environment"; -import PandocWorker from "$lib/workers/pandoc?worker"; +import PandocWorker from "$lib/workers/pandoc?worker&url"; export class PandocConverter extends Converter { public name = "pandoc"; @@ -20,7 +20,9 @@ export class PandocConverter extends Converter { } public async convert(input: VertFile, to: string): Promise { - const worker = new PandocWorker(); + const worker = new Worker(PandocWorker, { + type: "module", + }); worker.postMessage({ type: "load", wasm: this.wasm }); await waitForMessage(worker, "loaded"); worker.postMessage({ diff --git a/vite.config.ts b/vite.config.ts index 3c3ab3b..901c21e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,10 +3,10 @@ import { defineConfig } from "vite"; import { viteStaticCopy } from "vite-plugin-static-copy"; import svg from "@poppanator/sveltekit-svg"; import wasm from "vite-plugin-wasm"; +import topLevelAwait from "vite-plugin-top-level-await"; export default defineConfig({ plugins: [ - wasm(), sveltekit(), { name: "vips-request-middleware", @@ -47,6 +47,10 @@ export default defineConfig({ ], }), ], + worker: { + plugins: () => [wasm(), topLevelAwait()], + format: "es", + }, optimizeDeps: { exclude: [ "wasm-vips", @@ -62,4 +66,7 @@ export default defineConfig({ }, }, }, + build: { + target: "esnext", + }, }); From 0306fdbb5d1dd0d449436fcc031a706453c8c30f Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:36:48 +0100 Subject: [PATCH 11/18] fix: frowns --- vite.config.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 901c21e..814e547 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,7 +3,6 @@ import { defineConfig } from "vite"; import { viteStaticCopy } from "vite-plugin-static-copy"; import svg from "@poppanator/sveltekit-svg"; import wasm from "vite-plugin-wasm"; -import topLevelAwait from "vite-plugin-top-level-await"; export default defineConfig({ plugins: [ @@ -48,7 +47,7 @@ export default defineConfig({ }), ], worker: { - plugins: () => [wasm(), topLevelAwait()], + plugins: () => [wasm()], format: "es", }, optimizeDeps: { From 48c84b4b9fd726b5dc9895c93c205137c90c4212 Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:49:47 +0100 Subject: [PATCH 12/18] fix: dev --- vite.config.ts | 56 ++++++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 814e547..0537844 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,11 +1,11 @@ import { sveltekit } from "@sveltejs/kit/vite"; -import { defineConfig } from "vite"; +import { defineConfig, type PluginOption } from "vite"; import { viteStaticCopy } from "vite-plugin-static-copy"; import svg from "@poppanator/sveltekit-svg"; import wasm from "vite-plugin-wasm"; -export default defineConfig({ - plugins: [ +export default defineConfig(({ command }) => { + const plugins: PluginOption[] = [ sveltekit(), { name: "vips-request-middleware", @@ -45,27 +45,35 @@ export default defineConfig({ }, ], }), - ], - worker: { - plugins: () => [wasm()], - format: "es", - }, - optimizeDeps: { - exclude: [ - "wasm-vips", - "@ffmpeg/core-mt", - "@ffmpeg/ffmpeg", - "@ffmpeg/util", - ], - }, - css: { - preprocessorOptions: { - scss: { - api: "modern", + ]; + + if (command === "serve") { + plugins.unshift(wasm()); + } + + return { + plugins, + worker: { + plugins: () => [wasm()], + format: "es", + }, + optimizeDeps: { + exclude: [ + "wasm-vips", + "@ffmpeg/core-mt", + "@ffmpeg/ffmpeg", + "@ffmpeg/util", + ], + }, + css: { + preprocessorOptions: { + scss: { + api: "modern", + }, }, }, - }, - build: { - target: "esnext", - }, + build: { + target: "esnext", + }, + }; }); From 4e892adc10292f9b74e92191e03ed7ba346fd30a Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:51:43 +0100 Subject: [PATCH 13/18] Revert "fix: dev" This reverts commit 48c84b4b9fd726b5dc9895c93c205137c90c4212. --- vite.config.ts | 56 ++++++++++++++++++++++---------------------------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 0537844..814e547 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,11 +1,11 @@ import { sveltekit } from "@sveltejs/kit/vite"; -import { defineConfig, type PluginOption } from "vite"; +import { defineConfig } from "vite"; import { viteStaticCopy } from "vite-plugin-static-copy"; import svg from "@poppanator/sveltekit-svg"; import wasm from "vite-plugin-wasm"; -export default defineConfig(({ command }) => { - const plugins: PluginOption[] = [ +export default defineConfig({ + plugins: [ sveltekit(), { name: "vips-request-middleware", @@ -45,35 +45,27 @@ export default defineConfig(({ command }) => { }, ], }), - ]; - - if (command === "serve") { - plugins.unshift(wasm()); - } - - return { - plugins, - worker: { - plugins: () => [wasm()], - format: "es", - }, - optimizeDeps: { - exclude: [ - "wasm-vips", - "@ffmpeg/core-mt", - "@ffmpeg/ffmpeg", - "@ffmpeg/util", - ], - }, - css: { - preprocessorOptions: { - scss: { - api: "modern", - }, + ], + worker: { + plugins: () => [wasm()], + format: "es", + }, + optimizeDeps: { + exclude: [ + "wasm-vips", + "@ffmpeg/core-mt", + "@ffmpeg/ffmpeg", + "@ffmpeg/util", + ], + }, + css: { + preprocessorOptions: { + scss: { + api: "modern", }, }, - build: { - target: "esnext", - }, - }; + }, + build: { + target: "esnext", + }, }); From cf4d56f4d627cb2cd9a3329c53b2a4b27cb2c410 Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Tue, 15 Apr 2025 17:09:35 +0100 Subject: [PATCH 14/18] fix: pix fmt --- src/lib/converters/ffmpeg.svelte.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib/converters/ffmpeg.svelte.ts b/src/lib/converters/ffmpeg.svelte.ts index b6c5122..1fd4a81 100644 --- a/src/lib/converters/ffmpeg.svelte.ts +++ b/src/lib/converters/ffmpeg.svelte.ts @@ -89,16 +89,20 @@ export class FFmpegConverter extends Converter { "copy", "cover.png", ]); - await ffmpeg.exec([ + const cmd = [ "-i", "input", "-i", "cover.png", "-loop", "1", + "-pix_fmt", + "yuv420p", ...toArgs(to), "output" + to, - ]); + ]; + console.log(cmd); + await ffmpeg.exec(cmd); } else { await ffmpeg.exec(["-i", "input", "output" + to]); } From 6e853276bc1eefea5a89b602a0cfcb397f394be8 Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Tue, 15 Apr 2025 17:35:02 +0100 Subject: [PATCH 15/18] fix: OOPS --- src/lib/converters/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/converters/index.ts b/src/lib/converters/index.ts index d5e60a0..3ccb073 100644 --- a/src/lib/converters/index.ts +++ b/src/lib/converters/index.ts @@ -1,3 +1,4 @@ +import type { Converter } from "./converter.svelte"; import { FFmpegConverter } from "./ffmpeg.svelte"; import { PandocConverter } from "./pandoc.svelte"; import { VertdConverter } from "./vertd.svelte"; From 3ad8244e9720f63f837fad8fa346b2316a34b8c5 Mon Sep 17 00:00:00 2001 From: JovannMC Date: Tue, 15 Apr 2025 19:47:39 +0300 Subject: [PATCH 16/18] fix: fix gradients changing due to new format selected --- src/routes/convert/+page.svelte | 43 ++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/routes/convert/+page.svelte b/src/routes/convert/+page.svelte index 52784bb..75d9aa8 100644 --- a/src/routes/convert/+page.svelte +++ b/src/routes/convert/+page.svelte @@ -5,7 +5,7 @@ import Panel from "$lib/components/visual/Panel.svelte"; import ProgressBar from "$lib/components/visual/ProgressBar.svelte"; import Tooltip from "$lib/components/visual/Tooltip.svelte"; - import { converters } from "$lib/converters"; + import { converters, byNative } from "$lib/converters"; import { effects, files, @@ -44,21 +44,30 @@ $effect(() => { // Set gradient color depending on the file types // TODO: if more file types added, add a "fileType" property to the file object - const allAudio = files.files.every( - (file) => file.findConverter()?.name === "ffmpeg", - ); - const allImages = files.files.every( - (file) => - file.findConverter()?.name !== "ffmpeg" && - file.findConverter()?.name !== "vertd", - ); - const allVideos = files.files.every( - (file) => file.findConverter()?.name === "vertd", - ); - - const allDocuments = files.files.every( - (file) => file.findConverter()?.name === "pandoc", - ); + const allAudio = files.files.every((file) => { + const converter = file + .findConverters() + .sort(byNative(file.from))[0]; + return converter?.name === "ffmpeg"; + }); + const allImages = files.files.every((file) => { + const converter = file + .findConverters() + .sort(byNative(file.from))[0]; + return converter?.name === "libvips"; + }); + const allVideos = files.files.every((file) => { + const converter = file + .findConverters() + .sort(byNative(file.from))[0]; + return converter?.name === "vertd"; + }); + const allDocuments = files.files.every((file) => { + const converter = file + .findConverters() + .sort(byNative(file.from))[0]; + return converter?.name === "pandoc"; + }); if (files.files.length === 1 && files.files[0].blobUrl && !allVideos) { showGradient.set(false); @@ -68,7 +77,7 @@ if ( files.files.length === 0 || - (!allAudio && !allImages && !allVideos) + (!allAudio && !allImages && !allVideos && !allDocuments) ) { gradientColor.set(""); } else { From 21d9358a17af9df5d197630c09d35648a1974a30 Mon Sep 17 00:00:00 2001 From: JovannMC Date: Wed, 28 May 2025 18:30:53 +0300 Subject: [PATCH 17/18] fix: catch no audio streams --- src/lib/converters/ffmpeg.svelte.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/lib/converters/ffmpeg.svelte.ts b/src/lib/converters/ffmpeg.svelte.ts index 1fd4a81..b866f6d 100644 --- a/src/lib/converters/ffmpeg.svelte.ts +++ b/src/lib/converters/ffmpeg.svelte.ts @@ -19,7 +19,6 @@ export class FFmpegConverter extends Converter { new FormatInfo("ogg"), new FormatInfo("aac"), new FormatInfo("m4a"), - // TODO: audio to video where it uses the album cover ...videoFormats.map((f) => new FormatInfo(f, true, true, false)), new FormatInfo("wma"), new FormatInfo("amr"), @@ -63,6 +62,14 @@ export class FFmpegConverter extends Converter { }); ffmpeg.on("log", (l) => { log(["converters", this.name], l.message); + + if (l.message.includes("Stream map '0:a:0' matches no streams.")) { + error( + ["converters", this.name], + `No audio stream found in ${input.name}.`, + ); + addToast("error", `No audio stream found in ${input.name}.`); + } }); const baseURL = "https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.6/dist/esm"; From 4d9a11f262ddb48ca55029d2eb4788f9505f8504 Mon Sep 17 00:00:00 2001 From: JovannMC Date: Wed, 28 May 2025 19:05:14 +0300 Subject: [PATCH 18/18] fix: update supported audio formats .alacs don't exist in the wild, you'd only find them as .m4a amr can only be converted from, not to (need to build ffmpeg with the encoder) --- src/lib/converters/ffmpeg.svelte.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/converters/ffmpeg.svelte.ts b/src/lib/converters/ffmpeg.svelte.ts index b866f6d..99489b5 100644 --- a/src/lib/converters/ffmpeg.svelte.ts +++ b/src/lib/converters/ffmpeg.svelte.ts @@ -21,9 +21,8 @@ export class FFmpegConverter extends Converter { new FormatInfo("m4a"), ...videoFormats.map((f) => new FormatInfo(f, true, true, false)), new FormatInfo("wma"), - new FormatInfo("amr"), + new FormatInfo("amr", true, false), new FormatInfo("ac3"), - new FormatInfo("alac"), new FormatInfo("aiff"), ];