From e8a5005cd9814d63780455c4c72e31c2c8c3bfef Mon Sep 17 00:00:00 2001 From: azure Date: Mon, 14 Apr 2025 08:43:36 -0400 Subject: [PATCH 1/9] 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 8837f41c114d0e564b27be93ec6f94e021ab0356 Mon Sep 17 00:00:00 2001 From: azure Date: Mon, 14 Apr 2025 15:26:12 -0400 Subject: [PATCH 2/9] 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 5a2beebd004f1b7b4c457bd7250c85763abb9a35 Mon Sep 17 00:00:00 2001 From: azure Date: Mon, 14 Apr 2025 15:41:54 -0400 Subject: [PATCH 3/9] 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 8616651d2af720c652631aca65cfcdec2a789965 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 4/9] 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 | 1 + 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(+), 817 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 dd43695..bd4fa44 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"; 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 d9f3b21db37da94690cb09f51336d634eeb9bb0e 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 5/9] 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 bd4fa44..dd43695 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 c8b3c0d..bbbec7e 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", true, true), new FormatInfo("webp", true, true), new FormatInfo("gif", true, true), + 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", true, true), 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 0d74c2adf529c1f58d50c48197f058497fe66d79 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 6/9] 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 42cb829f76ff1a09c1169e1594551f25251c4295 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 7/9] 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 8984e25..108e657 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 62d303db68feedb9e6782ab25b0cc19138bb248c 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 8/9] 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 1bfb21c37715ce59c0870bb6c457839e6027293c Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:52:18 +0100 Subject: [PATCH 9/9] 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", + }, + }; });