mirror of https://github.com/VERT-sh/VERT.git
feat: .icns support
This commit is contained in:
parent
fc6e14109c
commit
db7b9406a7
7
bun.lock
7
bun.lock
|
@ -7,6 +7,7 @@
|
||||||
"@bjorn3/browser_wasi_shim": "^0.4.1",
|
"@bjorn3/browser_wasi_shim": "^0.4.1",
|
||||||
"@ffmpeg/ffmpeg": "^0.12.15",
|
"@ffmpeg/ffmpeg": "^0.12.15",
|
||||||
"@ffmpeg/util": "^0.12.2",
|
"@ffmpeg/util": "^0.12.2",
|
||||||
|
"@fiahfy/icns": "^0.0.7",
|
||||||
"@fontsource/azeret-mono": "^5.1.1",
|
"@fontsource/azeret-mono": "^5.1.1",
|
||||||
"@fontsource/lexend": "^5.1.2",
|
"@fontsource/lexend": "^5.1.2",
|
||||||
"@fontsource/radio-canada-big": "^5.1.1",
|
"@fontsource/radio-canada-big": "^5.1.1",
|
||||||
|
@ -120,6 +121,10 @@
|
||||||
|
|
||||||
"@ffmpeg/util": ["@ffmpeg/util@0.12.2", "", {}, "sha512-ouyoW+4JB7WxjeZ2y6KpRvB+dLp7Cp4ro8z0HIVpZVCM7AwFlHa0c4R8Y/a4M3wMqATpYKhC7lSFHQ0T11MEDw=="],
|
"@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/azeret-mono": ["@fontsource/azeret-mono@5.2.5", "", {}, "sha512-GRzKYuD1CVOS6Jag/ohDCycLV9a3TK6y1T73A8q0JoDZTVO85DNapqLK+SV2gYtTFldahNAlDSIaizv9MLhR1A=="],
|
||||||
|
|
||||||
"@fontsource/lexend": ["@fontsource/lexend@5.2.5", "", {}, "sha512-Mv2XQ+B4ek2lNCGRW5ddLTW8T3xTT17AnCk1IETpoef57XHz+e42fUfLAYMrmiJLOGpR44qnyJ5S6D323A5EIw=="],
|
"@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=="],
|
"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": ["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=="],
|
"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=="],
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
"@bjorn3/browser_wasi_shim": "^0.4.1",
|
"@bjorn3/browser_wasi_shim": "^0.4.1",
|
||||||
"@ffmpeg/ffmpeg": "^0.12.15",
|
"@ffmpeg/ffmpeg": "^0.12.15",
|
||||||
"@ffmpeg/util": "^0.12.2",
|
"@ffmpeg/util": "^0.12.2",
|
||||||
|
"@fiahfy/icns": "^0.0.7",
|
||||||
"@fontsource/azeret-mono": "^5.1.1",
|
"@fontsource/azeret-mono": "^5.1.1",
|
||||||
"@fontsource/lexend": "^5.1.2",
|
"@fontsource/lexend": "^5.1.2",
|
||||||
"@fontsource/radio-canada-big": "^5.1.1",
|
"@fontsource/radio-canada-big": "^5.1.1",
|
||||||
|
|
94
src/app.html
94
src/app.html
|
@ -1,47 +1,47 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
<link rel="apple-touch-icon" href="%sveltekit.assets%/favicon.png">
|
<link rel="apple-touch-icon" href="%sveltekit.assets%/favicon.png">
|
||||||
|
|
||||||
<link rel="apple-touch-startup-image" href="%sveltekit.assets%/lettermark.jpg">
|
<link rel="apple-touch-startup-image" href="%sveltekit.assets%/lettermark.jpg">
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
// Apply theme before DOM is loaded
|
// Apply theme before DOM is loaded
|
||||||
let theme = localStorage.getItem("theme");
|
let theme = localStorage.getItem("theme");
|
||||||
const prefersDark = window.matchMedia(
|
const prefersDark = window.matchMedia(
|
||||||
"(prefers-color-scheme: dark)",
|
"(prefers-color-scheme: dark)",
|
||||||
).matches;
|
).matches;
|
||||||
console.log(
|
console.log(
|
||||||
`Theme: ${theme || "N/A"}, prefers dark: ${prefersDark}`,
|
`Theme: ${theme || "N/A"}, prefers dark: ${prefersDark}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (theme !== "light" && theme !== "dark") {
|
if (theme !== "light" && theme !== "dark") {
|
||||||
console.log("Invalid theme, setting to default");
|
console.log("Invalid theme, setting to default");
|
||||||
theme = prefersDark ? "dark" : "light";
|
theme = prefersDark ? "dark" : "light";
|
||||||
localStorage.setItem("theme", theme);
|
localStorage.setItem("theme", theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Applying theme: ${theme}`);
|
console.log(`Applying theme: ${theme}`);
|
||||||
document.documentElement.classList.add(theme);
|
document.documentElement.classList.add(theme);
|
||||||
|
|
||||||
// Lock dark reader if it's set to dark mode
|
// Lock dark reader if it's set to dark mode
|
||||||
if (theme === "dark") {
|
if (theme === "dark") {
|
||||||
const lock = document.createElement('meta');
|
const lock = document.createElement('meta');
|
||||||
lock.name = 'darkreader-lock';
|
lock.name = 'darkreader-lock';
|
||||||
document.head.appendChild(lock);
|
document.head.appendChild(lock);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,197 +1,197 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
import { page } from "$app/state";
|
import { page } from "$app/state";
|
||||||
import { duration, fade } from "$lib/animation";
|
import { duration, fade } from "$lib/animation";
|
||||||
import {
|
import {
|
||||||
effects,
|
effects,
|
||||||
files,
|
files,
|
||||||
goingLeft,
|
goingLeft,
|
||||||
setTheme,
|
setTheme,
|
||||||
} from "$lib/store/index.svelte";
|
} from "$lib/store/index.svelte";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {
|
import {
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
MoonIcon,
|
MoonIcon,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
SunIcon,
|
SunIcon,
|
||||||
UploadIcon,
|
UploadIcon,
|
||||||
} from "lucide-svelte";
|
} from "lucide-svelte";
|
||||||
import { quintOut } from "svelte/easing";
|
import { quintOut } from "svelte/easing";
|
||||||
import Panel from "../../visual/Panel.svelte";
|
import Panel from "../../visual/Panel.svelte";
|
||||||
import Logo from "../../visual/svg/Logo.svelte";
|
import Logo from "../../visual/svg/Logo.svelte";
|
||||||
import { beforeNavigate } from "$app/navigation";
|
import { beforeNavigate } from "$app/navigation";
|
||||||
|
|
||||||
const items = $derived<
|
const items = $derived<
|
||||||
{
|
{
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
activeMatch: (pathname: string) => boolean;
|
activeMatch: (pathname: string) => boolean;
|
||||||
icon: any;
|
icon: any;
|
||||||
badge?: number;
|
badge?: number;
|
||||||
}[]
|
}[]
|
||||||
>([
|
>([
|
||||||
{
|
{
|
||||||
name: "Upload",
|
name: "Upload",
|
||||||
url: "/",
|
url: "/",
|
||||||
activeMatch: (pathname) => pathname === "/",
|
activeMatch: (pathname) => pathname === "/",
|
||||||
icon: UploadIcon,
|
icon: UploadIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Convert",
|
name: "Convert",
|
||||||
url: "/convert/",
|
url: "/convert/",
|
||||||
activeMatch: (pathname) =>
|
activeMatch: (pathname) =>
|
||||||
pathname === "/convert/" || pathname === "/convert",
|
pathname === "/convert/" || pathname === "/convert",
|
||||||
icon: RefreshCw,
|
icon: RefreshCw,
|
||||||
badge: files.files.length,
|
badge: files.files.length,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Settings",
|
name: "Settings",
|
||||||
url: "/settings/",
|
url: "/settings/",
|
||||||
activeMatch: (pathname) => pathname.startsWith("/settings"),
|
activeMatch: (pathname) => pathname.startsWith("/settings"),
|
||||||
icon: SettingsIcon,
|
icon: SettingsIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "About",
|
name: "About",
|
||||||
url: "/about/",
|
url: "/about/",
|
||||||
activeMatch: (pathname) => pathname.startsWith("/about"),
|
activeMatch: (pathname) => pathname.startsWith("/about"),
|
||||||
icon: InfoIcon,
|
icon: InfoIcon,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let links = $state<HTMLAnchorElement[]>([]);
|
let links = $state<HTMLAnchorElement[]>([]);
|
||||||
let container = $state<HTMLDivElement>();
|
let container = $state<HTMLDivElement>();
|
||||||
let containerRect = $derived(container?.getBoundingClientRect());
|
let containerRect = $derived(container?.getBoundingClientRect());
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
$inspect(containerRect);
|
$inspect(containerRect);
|
||||||
});
|
});
|
||||||
|
|
||||||
const linkRects = $derived(links.map((l) => l.getBoundingClientRect()));
|
const linkRects = $derived(links.map((l) => l.getBoundingClientRect()));
|
||||||
|
|
||||||
const selectedIndex = $derived(
|
const selectedIndex = $derived(
|
||||||
items.findIndex((i) => i.activeMatch(page.url.pathname)),
|
items.findIndex((i) => i.activeMatch(page.url.pathname)),
|
||||||
);
|
);
|
||||||
|
|
||||||
const isSecretPage = $derived(selectedIndex === -1);
|
const isSecretPage = $derived(selectedIndex === -1);
|
||||||
|
|
||||||
beforeNavigate((e) => {
|
beforeNavigate((e) => {
|
||||||
const oldIndex = items.findIndex((i) =>
|
const oldIndex = items.findIndex((i) =>
|
||||||
i.activeMatch(e.from?.url.pathname || ""),
|
i.activeMatch(e.from?.url.pathname || ""),
|
||||||
);
|
);
|
||||||
const newIndex = items.findIndex((i) =>
|
const newIndex = items.findIndex((i) =>
|
||||||
i.activeMatch(e.to?.url.pathname || ""),
|
i.activeMatch(e.to?.url.pathname || ""),
|
||||||
);
|
);
|
||||||
if (newIndex < oldIndex) {
|
if (newIndex < oldIndex) {
|
||||||
goingLeft.set(true);
|
goingLeft.set(true);
|
||||||
} else {
|
} else {
|
||||||
goingLeft.set(false);
|
goingLeft.set(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet link(item: (typeof items)[0], index: number)}
|
{#snippet link(item: (typeof items)[0], index: number)}
|
||||||
{@const Icon = item.icon}
|
{@const Icon = item.icon}
|
||||||
<a
|
<a
|
||||||
bind:this={links[index]}
|
bind:this={links[index]}
|
||||||
href={item.url}
|
href={item.url}
|
||||||
aria-label={item.name}
|
aria-label={item.name}
|
||||||
class={clsx(
|
class={clsx(
|
||||||
"min-w-16 md:min-w-32 h-full relative z-10 rounded-xl flex flex-1 items-center justify-center gap-3 overflow-hidden",
|
"min-w-16 md:min-w-32 h-full relative z-10 rounded-xl flex flex-1 items-center justify-center gap-3 overflow-hidden",
|
||||||
{
|
{
|
||||||
"bg-panel-highlight":
|
"bg-panel-highlight":
|
||||||
item.activeMatch(page.url.pathname) && !browser,
|
item.activeMatch(page.url.pathname) && !browser,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
>
|
>
|
||||||
<div class="grid grid-rows-1 grid-cols-1">
|
<div class="grid grid-rows-1 grid-cols-1">
|
||||||
{#key item.name}
|
{#key item.name}
|
||||||
<div
|
<div
|
||||||
class="w-full row-start-1 col-start-1 h-full flex items-center justify-center gap-3"
|
class="w-full row-start-1 col-start-1 h-full flex items-center justify-center gap-3"
|
||||||
in:fade={{
|
in:fade={{
|
||||||
duration,
|
duration,
|
||||||
easing: quintOut,
|
easing: quintOut,
|
||||||
}}
|
}}
|
||||||
out:fade={{
|
out:fade={{
|
||||||
duration,
|
duration,
|
||||||
easing: quintOut,
|
easing: quintOut,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<Icon />
|
<Icon />
|
||||||
{#if item.badge}
|
{#if item.badge}
|
||||||
<div
|
<div
|
||||||
class="absolute overflow-hidden grid grid-rows-1 grid-cols-1 -top-1 font-display -right-1 w-fit px-1.5 h-4 rounded-full bg-badge text-on-badge font-medium"
|
class="absolute overflow-hidden grid grid-rows-1 grid-cols-1 -top-1 font-display -right-1 w-fit px-1.5 h-4 rounded-full bg-badge text-on-badge font-medium"
|
||||||
style="font-size: 0.7rem;"
|
style="font-size: 0.7rem;"
|
||||||
transition:fade={{
|
transition:fade={{
|
||||||
duration,
|
duration,
|
||||||
easing: quintOut,
|
easing: quintOut,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#key item.badge}
|
{#key item.badge}
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-center w-full h-full col-start-1 row-start-1"
|
class="flex items-center justify-center w-full h-full col-start-1 row-start-1"
|
||||||
in:fade={{
|
in:fade={{
|
||||||
duration,
|
duration,
|
||||||
easing: quintOut,
|
easing: quintOut,
|
||||||
}}
|
}}
|
||||||
out:fade={{
|
out:fade={{
|
||||||
duration,
|
duration,
|
||||||
easing: quintOut,
|
easing: quintOut,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.badge}
|
{item.badge}
|
||||||
</div>
|
</div>
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<p class="font-medium hidden md:flex">
|
<p class="font-medium hidden md:flex">
|
||||||
{item.name}
|
{item.name}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<div bind:this={container}>
|
<div bind:this={container}>
|
||||||
<Panel class="max-w-[778px] w-screen h-20 flex items-center gap-3 relative">
|
<Panel class="max-w-[778px] w-screen h-20 flex items-center gap-3 relative">
|
||||||
{@const linkRect = linkRects.at(selectedIndex) || linkRects[0]}
|
{@const linkRect = linkRects.at(selectedIndex) || linkRects[0]}
|
||||||
{#if linkRect}
|
{#if linkRect}
|
||||||
<div
|
<div
|
||||||
class="absolute bg-panel-highlight rounded-xl"
|
class="absolute bg-panel-highlight rounded-xl"
|
||||||
style="width: {linkRect.width}px; height: {linkRect.height}px; top: {linkRect.top -
|
style="width: {linkRect.width}px; height: {linkRect.height}px; top: {linkRect.top -
|
||||||
(containerRect?.top || 0)}px; left: {linkRect.left -
|
(containerRect?.top || 0)}px; left: {linkRect.left -
|
||||||
(containerRect?.left || 0)}px; opacity: {isSecretPage
|
(containerRect?.left || 0)}px; opacity: {isSecretPage
|
||||||
? 0
|
? 0
|
||||||
: 1}; {$effects
|
: 1}; {$effects
|
||||||
? `transition: left var(--transition) ${duration}ms, top var(--transition) ${duration}ms, opacity var(--transition) ${duration}ms;`
|
? `transition: left var(--transition) ${duration}ms, top var(--transition) ${duration}ms, opacity var(--transition) ${duration}ms;`
|
||||||
: ''}"
|
: ''}"
|
||||||
></div>
|
></div>
|
||||||
{/if}
|
{/if}
|
||||||
<a
|
<a
|
||||||
class="w-28 h-full bg-accent rounded-xl items-center justify-center hidden md:flex"
|
class="w-28 h-full bg-accent rounded-xl items-center justify-center hidden md:flex"
|
||||||
href="/"
|
href="/"
|
||||||
>
|
>
|
||||||
<div class="h-5 w-full">
|
<div class="h-5 w-full">
|
||||||
<Logo />
|
<Logo />
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
{#each items as item, i (item.url)}
|
{#each items as item, i (item.url)}
|
||||||
{@render link(item, i)}
|
{@render link(item, i)}
|
||||||
{/each}
|
{/each}
|
||||||
<div class="w-0.5 bg-separator h-full hidden md:flex"></div>
|
<div class="w-0.5 bg-separator h-full hidden md:flex"></div>
|
||||||
<button
|
<button
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
const isDark =
|
const isDark =
|
||||||
document.documentElement.classList.contains("dark");
|
document.documentElement.classList.contains("dark");
|
||||||
setTheme(isDark ? "light" : "dark");
|
setTheme(isDark ? "light" : "dark");
|
||||||
}}
|
}}
|
||||||
class="w-14 h-full items-center justify-center hidden md:flex"
|
class="w-14 h-full items-center justify-center hidden md:flex"
|
||||||
>
|
>
|
||||||
<SunIcon class="dynadark:hidden block" />
|
<SunIcon class="dynadark:hidden block" />
|
||||||
<MoonIcon class="dynadark:block hidden" />
|
<MoonIcon class="dynadark:block hidden" />
|
||||||
</button>
|
</button>
|
||||||
</Panel>
|
</Panel>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Converter, FormatInfo } from "./converter.svelte";
|
import type { Converter } from "./converter.svelte";
|
||||||
import { FFmpegConverter } from "./ffmpeg.svelte";
|
import { FFmpegConverter } from "./ffmpeg.svelte";
|
||||||
import { PandocConverter } from "./pandoc.svelte";
|
import { PandocConverter } from "./pandoc.svelte";
|
||||||
import { VertdConverter } from "./vertd.svelte";
|
import { VertdConverter } from "./vertd.svelte";
|
||||||
|
|
|
@ -1,150 +1,150 @@
|
||||||
// THIS CODE IS FROM https://github.com/captbaritone/webamp/blob/15b0312cb794973a0e615d894df942452e920c36/packages/ani-cursor/src/parser.ts
|
// 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
|
// LICENSED UNDER MIT. (c) Jordan Eldredge and Webamp contributors
|
||||||
|
|
||||||
// this code is ripped from their project because i didn't want to
|
// this code is ripped from their project because i didn't want to
|
||||||
// re-invent the wheel, BUT the library they provide (ani-cursor)
|
// re-invent the wheel, BUT the library they provide (ani-cursor)
|
||||||
// doesn't expose the internals.
|
// doesn't expose the internals.
|
||||||
|
|
||||||
import { RIFFFile } from "riff-file";
|
import { RIFFFile } from "riff-file";
|
||||||
import { unpackArray, unpackString } from "byte-data";
|
import { unpackArray, unpackString } from "byte-data";
|
||||||
|
|
||||||
type Chunk = {
|
type Chunk = {
|
||||||
format: string;
|
format: string;
|
||||||
chunkId: string;
|
chunkId: string;
|
||||||
chunkData: {
|
chunkData: {
|
||||||
start: number;
|
start: number;
|
||||||
end: number;
|
end: number;
|
||||||
};
|
};
|
||||||
subChunks: Chunk[];
|
subChunks: Chunk[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// https://www.informit.com/articles/article.aspx?p=1189080&seqNum=3
|
// https://www.informit.com/articles/article.aspx?p=1189080&seqNum=3
|
||||||
type AniMetadata = {
|
type AniMetadata = {
|
||||||
cbSize: number; // Data structure size (in bytes)
|
cbSize: number; // Data structure size (in bytes)
|
||||||
nFrames: number; // Number of images (also known as frames) stored in the file
|
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
|
nSteps: number; // Number of frames to be displayed before the animation repeats
|
||||||
iWidth: number; // Width of frame (in pixels)
|
iWidth: number; // Width of frame (in pixels)
|
||||||
iHeight: number; // Height of frame (in pixels)
|
iHeight: number; // Height of frame (in pixels)
|
||||||
iBitCount: number; // Number of bits per pixel
|
iBitCount: number; // Number of bits per pixel
|
||||||
nPlanes: number; // Number of color planes
|
nPlanes: number; // Number of color planes
|
||||||
iDispRate: number; // Default frame display rate (measured in 1/60th-of-a-second units)
|
iDispRate: number; // Default frame display rate (measured in 1/60th-of-a-second units)
|
||||||
bfAttributes: number; // ANI attribute bit flags
|
bfAttributes: number; // ANI attribute bit flags
|
||||||
};
|
};
|
||||||
|
|
||||||
type ParsedAni = {
|
type ParsedAni = {
|
||||||
rate: number[] | null;
|
rate: number[] | null;
|
||||||
seq: number[] | null;
|
seq: number[] | null;
|
||||||
images: Uint8Array[];
|
images: Uint8Array[];
|
||||||
metadata: AniMetadata;
|
metadata: AniMetadata;
|
||||||
artist: string | null;
|
artist: string | null;
|
||||||
title: string | null;
|
title: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DWORD = { bits: 32, be: false, signed: false, fp: false };
|
const DWORD = { bits: 32, be: false, signed: false, fp: false };
|
||||||
|
|
||||||
export function parseAni(arr: Uint8Array): ParsedAni {
|
export function parseAni(arr: Uint8Array): ParsedAni {
|
||||||
const riff = new RIFFFile();
|
const riff = new RIFFFile();
|
||||||
|
|
||||||
riff.setSignature(arr);
|
riff.setSignature(arr);
|
||||||
|
|
||||||
const signature = riff.signature as Chunk;
|
const signature = riff.signature as Chunk;
|
||||||
if (signature.format !== "ACON") {
|
if (signature.format !== "ACON") {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Expected format. Expected "ACON", got "${signature.format}"`,
|
`Expected format. Expected "ACON", got "${signature.format}"`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to get a chunk by chunkId and transform it if it's non-null.
|
// Helper function to get a chunk by chunkId and transform it if it's non-null.
|
||||||
function mapChunk<T>(
|
function mapChunk<T>(
|
||||||
chunkId: string,
|
chunkId: string,
|
||||||
mapper: (chunk: Chunk) => T,
|
mapper: (chunk: Chunk) => T,
|
||||||
): T | null {
|
): T | null {
|
||||||
const chunk = riff.findChunk(chunkId) as Chunk | null;
|
const chunk = riff.findChunk(chunkId) as Chunk | null;
|
||||||
return chunk == null ? null : mapper(chunk);
|
return chunk == null ? null : mapper(chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readImages(chunk: Chunk, frameCount: number): Uint8Array[] {
|
function readImages(chunk: Chunk, frameCount: number): Uint8Array[] {
|
||||||
return chunk.subChunks.slice(0, frameCount).map((c) => {
|
return chunk.subChunks.slice(0, frameCount).map((c) => {
|
||||||
if (c.chunkId !== "icon") {
|
if (c.chunkId !== "icon") {
|
||||||
throw new Error(`Unexpected chunk type in fram: ${c.chunkId}`);
|
throw new Error(`Unexpected chunk type in fram: ${c.chunkId}`);
|
||||||
}
|
}
|
||||||
return arr.slice(c.chunkData.start, c.chunkData.end);
|
return arr.slice(c.chunkData.start, c.chunkData.end);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const metadata = mapChunk("anih", (c) => {
|
const metadata = mapChunk("anih", (c) => {
|
||||||
const words = unpackArray(
|
const words = unpackArray(
|
||||||
arr,
|
arr,
|
||||||
DWORD,
|
DWORD,
|
||||||
c.chunkData.start,
|
c.chunkData.start,
|
||||||
c.chunkData.end,
|
c.chunkData.end,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
cbSize: words[0],
|
cbSize: words[0],
|
||||||
nFrames: words[1],
|
nFrames: words[1],
|
||||||
nSteps: words[2],
|
nSteps: words[2],
|
||||||
iWidth: words[3],
|
iWidth: words[3],
|
||||||
iHeight: words[4],
|
iHeight: words[4],
|
||||||
iBitCount: words[5],
|
iBitCount: words[5],
|
||||||
nPlanes: words[6],
|
nPlanes: words[6],
|
||||||
iDispRate: words[7],
|
iDispRate: words[7],
|
||||||
bfAttributes: words[8],
|
bfAttributes: words[8],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
if (metadata == null) {
|
if (metadata == null) {
|
||||||
throw new Error("Did not find anih");
|
throw new Error("Did not find anih");
|
||||||
}
|
}
|
||||||
|
|
||||||
const rate = mapChunk("rate", (c) => {
|
const rate = mapChunk("rate", (c) => {
|
||||||
return unpackArray(arr, DWORD, c.chunkData.start, c.chunkData.end);
|
return unpackArray(arr, DWORD, c.chunkData.start, c.chunkData.end);
|
||||||
});
|
});
|
||||||
// chunkIds are always four chars, hence the trailing space.
|
// chunkIds are always four chars, hence the trailing space.
|
||||||
const seq = mapChunk("seq ", (c) => {
|
const seq = mapChunk("seq ", (c) => {
|
||||||
return unpackArray(arr, DWORD, c.chunkData.start, c.chunkData.end);
|
return unpackArray(arr, DWORD, c.chunkData.start, c.chunkData.end);
|
||||||
});
|
});
|
||||||
|
|
||||||
const lists = riff.findChunk("LIST", true) as Chunk[] | null;
|
const lists = riff.findChunk("LIST", true) as Chunk[] | null;
|
||||||
const imageChunk = lists?.find((c) => c.format === "fram");
|
const imageChunk = lists?.find((c) => c.format === "fram");
|
||||||
if (imageChunk == null) {
|
if (imageChunk == null) {
|
||||||
throw new Error("Did not find fram LIST");
|
throw new Error("Did not find fram LIST");
|
||||||
}
|
}
|
||||||
|
|
||||||
let images = readImages(imageChunk, metadata.nFrames);
|
let images = readImages(imageChunk, metadata.nFrames);
|
||||||
|
|
||||||
let title = null;
|
let title = null;
|
||||||
let artist = null;
|
let artist = null;
|
||||||
|
|
||||||
const infoChunk = lists?.find((c) => c.format === "INFO");
|
const infoChunk = lists?.find((c) => c.format === "INFO");
|
||||||
if (infoChunk != null) {
|
if (infoChunk != null) {
|
||||||
infoChunk.subChunks.forEach((c) => {
|
infoChunk.subChunks.forEach((c) => {
|
||||||
switch (c.chunkId) {
|
switch (c.chunkId) {
|
||||||
case "INAM":
|
case "INAM":
|
||||||
title = unpackString(
|
title = unpackString(
|
||||||
arr,
|
arr,
|
||||||
c.chunkData.start,
|
c.chunkData.start,
|
||||||
c.chunkData.end,
|
c.chunkData.end,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "IART":
|
case "IART":
|
||||||
artist = unpackString(
|
artist = unpackString(
|
||||||
arr,
|
arr,
|
||||||
c.chunkData.start,
|
c.chunkData.start,
|
||||||
c.chunkData.end,
|
c.chunkData.end,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "LIST":
|
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?
|
// 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") {
|
if (c.format === "fram") {
|
||||||
images = readImages(c, metadata.nFrames);
|
images = readImages(c, metadata.nFrames);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Unexpected subchunk
|
// Unexpected subchunk
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { images, rate, seq, metadata, artist, title };
|
return { images, rate, seq, metadata, artist, title };
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +1,32 @@
|
||||||
import { VertFile } from "./file.svelte";
|
import { VertFile } from "./file.svelte";
|
||||||
|
|
||||||
interface ConvertMessage {
|
interface ConvertMessage {
|
||||||
type: "convert";
|
type: "convert";
|
||||||
input: VertFile;
|
input: VertFile;
|
||||||
to: string;
|
to: string;
|
||||||
compression: number | null;
|
compression: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FinishedMessage {
|
interface FinishedMessage {
|
||||||
type: "finished";
|
type: "finished";
|
||||||
output: ArrayBufferLike;
|
output: ArrayBufferLike;
|
||||||
zip?: boolean;
|
zip?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoadedMessage {
|
interface LoadedMessage {
|
||||||
type: "loaded";
|
type: "loaded";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ErrorMessage {
|
interface ErrorMessage {
|
||||||
type: "error";
|
type: "error";
|
||||||
error: string;
|
error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkerMessage = (
|
export type WorkerMessage = (
|
||||||
| ConvertMessage
|
| ConvertMessage
|
||||||
| FinishedMessage
|
| FinishedMessage
|
||||||
| LoadedMessage
|
| LoadedMessage
|
||||||
| ErrorMessage
|
| ErrorMessage
|
||||||
) & {
|
) & {
|
||||||
id: number;
|
id: number;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,278 +1,284 @@
|
||||||
import Vips from "wasm-vips";
|
import Vips from "wasm-vips";
|
||||||
import {
|
import {
|
||||||
initializeImageMagick,
|
initializeImageMagick,
|
||||||
MagickFormat,
|
MagickFormat,
|
||||||
MagickImage,
|
MagickImage,
|
||||||
MagickImageCollection,
|
MagickImageCollection,
|
||||||
MagickReadSettings,
|
MagickReadSettings,
|
||||||
type IMagickImage,
|
type IMagickImage,
|
||||||
} from "@imagemagick/magick-wasm";
|
} from "@imagemagick/magick-wasm";
|
||||||
import { makeZip } from "client-zip";
|
import { makeZip } from "client-zip";
|
||||||
import wasm from "@imagemagick/magick-wasm/magick.wasm?url";
|
import wasm from "@imagemagick/magick-wasm/magick.wasm?url";
|
||||||
import { parseAni } from "$lib/parse/ani";
|
import { parseAni } from "$lib/parse/ani";
|
||||||
|
import { Icns } from "@fiahfy/icns/dist";
|
||||||
const vipsPromise = Vips({
|
|
||||||
dynamicLibraries: [],
|
const vipsPromise = Vips({
|
||||||
});
|
dynamicLibraries: [],
|
||||||
|
});
|
||||||
const magickPromise = initializeImageMagick(new URL(wasm, import.meta.url));
|
|
||||||
|
const magickPromise = initializeImageMagick(new URL(wasm, import.meta.url));
|
||||||
const magickRequiredFormats = [
|
|
||||||
".dng",
|
const magickRequiredFormats = [
|
||||||
".heic",
|
".dng",
|
||||||
".ico",
|
".heic",
|
||||||
".cur",
|
".ico",
|
||||||
".ani",
|
".cur",
|
||||||
".cr2",
|
".ani",
|
||||||
".nef",
|
".cr2",
|
||||||
];
|
".nef",
|
||||||
const unsupportedFrom: string[] = [];
|
];
|
||||||
const unsupportedTo = [...magickRequiredFormats];
|
const unsupportedFrom: string[] = [];
|
||||||
|
const unsupportedTo = [...magickRequiredFormats];
|
||||||
vipsPromise
|
|
||||||
.then(() => {
|
vipsPromise
|
||||||
postMessage({ type: "loaded" });
|
.then(() => {
|
||||||
})
|
postMessage({ type: "loaded" });
|
||||||
.catch((error) => {
|
})
|
||||||
postMessage({ type: "error", error });
|
.catch((error) => {
|
||||||
});
|
postMessage({ type: "error", error });
|
||||||
|
});
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const handleMessage = async (message: any): Promise<any> => {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const vips = await vipsPromise;
|
const handleMessage = async (message: any): Promise<any> => {
|
||||||
switch (message.type) {
|
const vips = await vipsPromise;
|
||||||
case "convert": {
|
switch (message.type) {
|
||||||
if (!message.to.startsWith(".")) message.to = `.${message.to}`;
|
case "convert": {
|
||||||
console.log(message);
|
if (!message.to.startsWith(".")) message.to = `.${message.to}`;
|
||||||
if (unsupportedFrom.includes(message.input.from)) {
|
console.log(message);
|
||||||
return {
|
if (unsupportedFrom.includes(message.input.from)) {
|
||||||
type: "error",
|
return {
|
||||||
error: `Unsupported input format ${message.input.from}`,
|
type: "error",
|
||||||
};
|
error: `Unsupported input format ${message.input.from}`,
|
||||||
}
|
};
|
||||||
|
}
|
||||||
if (unsupportedTo.includes(message.to)) {
|
|
||||||
return {
|
if (unsupportedTo.includes(message.to)) {
|
||||||
type: "error",
|
return {
|
||||||
error: `Unsupported output format ${message.to}`,
|
type: "error",
|
||||||
};
|
error: `Unsupported output format ${message.to}`,
|
||||||
}
|
};
|
||||||
|
}
|
||||||
const buffer = await message.input.file.arrayBuffer();
|
|
||||||
if (
|
const buffer = await message.input.file.arrayBuffer();
|
||||||
magickRequiredFormats.includes(message.input.from) ||
|
if (
|
||||||
magickRequiredFormats.includes(message.to)
|
magickRequiredFormats.includes(message.input.from) ||
|
||||||
) {
|
magickRequiredFormats.includes(message.to)
|
||||||
// only wait when we need to
|
) {
|
||||||
await magickPromise;
|
// only wait when we need to
|
||||||
|
await magickPromise;
|
||||||
// special ico handling to split them all into separate images
|
|
||||||
if (message.input.from === ".ico") {
|
// special ico handling to split them all into separate images
|
||||||
const imgs = MagickImageCollection.create();
|
if (message.input.from === ".ico") {
|
||||||
|
const imgs = MagickImageCollection.create();
|
||||||
while (true) {
|
|
||||||
try {
|
while (true) {
|
||||||
const img = MagickImage.create(
|
try {
|
||||||
new Uint8Array(buffer),
|
const img = MagickImage.create(
|
||||||
new MagickReadSettings({
|
new Uint8Array(buffer),
|
||||||
format: MagickFormat.Ico,
|
new MagickReadSettings({
|
||||||
frameIndex: imgs.length,
|
format: MagickFormat.Ico,
|
||||||
}),
|
frameIndex: imgs.length,
|
||||||
);
|
}),
|
||||||
imgs.push(img);
|
);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
imgs.push(img);
|
||||||
} catch (_) {
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
break;
|
} catch (_) {
|
||||||
}
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (imgs.length === 0) {
|
|
||||||
return {
|
if (imgs.length === 0) {
|
||||||
type: "error",
|
return {
|
||||||
error: `Failed to read ICO -- no images found inside?`,
|
type: "error",
|
||||||
};
|
error: `Failed to read ICO -- no images found inside?`,
|
||||||
}
|
};
|
||||||
|
}
|
||||||
const convertedImgs: Uint8Array[] = [];
|
|
||||||
await Promise.all(
|
const convertedImgs: Uint8Array[] = [];
|
||||||
imgs.map(async (img, i) => {
|
await Promise.all(
|
||||||
const output = await magickConvert(img, message.to);
|
imgs.map(async (img, i) => {
|
||||||
convertedImgs[i] = output;
|
const output = await magickConvert(img, message.to);
|
||||||
}),
|
convertedImgs[i] = output;
|
||||||
);
|
}),
|
||||||
|
);
|
||||||
const zip = makeZip(
|
|
||||||
convertedImgs.map(
|
const zip = makeZip(
|
||||||
(img, i) =>
|
convertedImgs.map(
|
||||||
new File(
|
(img, i) =>
|
||||||
[img],
|
new File(
|
||||||
`image${i}.${message.to.slice(1)}`,
|
[img],
|
||||||
),
|
`image${i}.${message.to.slice(1)}`,
|
||||||
),
|
),
|
||||||
"images.zip",
|
),
|
||||||
);
|
"images.zip",
|
||||||
|
);
|
||||||
// read the ReadableStream to the end
|
|
||||||
const zipBytes = await readToEnd(zip.getReader());
|
// read the ReadableStream to the end
|
||||||
|
const zipBytes = await readToEnd(zip.getReader());
|
||||||
imgs.dispose();
|
|
||||||
|
imgs.dispose();
|
||||||
return {
|
|
||||||
type: "finished",
|
return {
|
||||||
output: zipBytes,
|
type: "finished",
|
||||||
zip: true,
|
output: zipBytes,
|
||||||
};
|
zip: true,
|
||||||
} else if (message.input.from === ".ani") {
|
};
|
||||||
console.log("Parsing ANI file");
|
} else if (message.input.from === ".ani") {
|
||||||
try {
|
console.log("Parsing ANI file");
|
||||||
const parsedAni = parseAni(new Uint8Array(buffer));
|
try {
|
||||||
const files: File[] = [];
|
const parsedAni = parseAni(new Uint8Array(buffer));
|
||||||
await Promise.all(
|
const files: File[] = [];
|
||||||
parsedAni.images.map(async (img, i) => {
|
await Promise.all(
|
||||||
const blob = await magickConvert(
|
parsedAni.images.map(async (img, i) => {
|
||||||
MagickImage.create(
|
const blob = await magickConvert(
|
||||||
img,
|
MagickImage.create(
|
||||||
new MagickReadSettings({
|
img,
|
||||||
format: MagickFormat.Ico,
|
new MagickReadSettings({
|
||||||
}),
|
format: MagickFormat.Ico,
|
||||||
),
|
}),
|
||||||
message.to,
|
),
|
||||||
);
|
message.to,
|
||||||
files.push(
|
);
|
||||||
new File([blob], `image${i}${message.to}`),
|
files.push(
|
||||||
);
|
new File([blob], `image${i}${message.to}`),
|
||||||
}),
|
);
|
||||||
);
|
}),
|
||||||
|
);
|
||||||
const zip = makeZip(files, "images.zip");
|
|
||||||
const zipBytes = await readToEnd(zip.getReader());
|
const zip = makeZip(files, "images.zip");
|
||||||
|
const zipBytes = await readToEnd(zip.getReader());
|
||||||
return {
|
|
||||||
type: "finished",
|
return {
|
||||||
output: zipBytes,
|
type: "finished",
|
||||||
zip: true,
|
output: zipBytes,
|
||||||
};
|
zip: true,
|
||||||
} catch (e) {
|
};
|
||||||
console.error(e);
|
} catch (e) {
|
||||||
}
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
console.log(message.input.from);
|
|
||||||
|
console.log(message.input.from);
|
||||||
const img = MagickImage.create(
|
|
||||||
new Uint8Array(buffer),
|
const img = MagickImage.create(
|
||||||
new MagickReadSettings({
|
new Uint8Array(buffer),
|
||||||
format: message.input.from
|
new MagickReadSettings({
|
||||||
.slice(1)
|
format: message.input.from
|
||||||
.toUpperCase() as MagickFormat,
|
.slice(1)
|
||||||
}),
|
.toUpperCase() as MagickFormat,
|
||||||
);
|
}),
|
||||||
|
);
|
||||||
const converted = await magickConvert(img, message.to);
|
|
||||||
|
const converted = await magickConvert(img, message.to);
|
||||||
return {
|
|
||||||
type: "finished",
|
return {
|
||||||
output: converted,
|
type: "finished",
|
||||||
};
|
output: converted,
|
||||||
}
|
};
|
||||||
|
}
|
||||||
let image = vips.Image.newFromBuffer(buffer, "");
|
|
||||||
|
if (message.input.from === ".icns") {
|
||||||
// check if animated image & keep it animated when converting
|
const icns = Icns.from(new Uint8Array(buffer));
|
||||||
if (image.getTypeof("n-pages") > 0) {
|
console.log(icns);
|
||||||
image = vips.Image.newFromBuffer(buffer, "[n=-1]");
|
}
|
||||||
}
|
|
||||||
|
let image = vips.Image.newFromBuffer(buffer, "");
|
||||||
const opts: { [key: string]: string } = {};
|
|
||||||
if (typeof message.compression !== "undefined") {
|
// check if animated image & keep it animated when converting
|
||||||
opts["Q"] = Math.min(100, message.compression + 1).toString();
|
if (image.getTypeof("n-pages") > 0) {
|
||||||
}
|
image = vips.Image.newFromBuffer(buffer, "[n=-1]");
|
||||||
|
}
|
||||||
const output = image.writeToBuffer(message.to, opts);
|
|
||||||
image.delete();
|
const opts: { [key: string]: string } = {};
|
||||||
return {
|
if (typeof message.compression !== "undefined") {
|
||||||
type: "finished",
|
opts["Q"] = Math.min(100, message.compression + 1).toString();
|
||||||
output: output.buffer,
|
}
|
||||||
};
|
|
||||||
}
|
const output = image.writeToBuffer(message.to, opts);
|
||||||
}
|
image.delete();
|
||||||
};
|
return {
|
||||||
|
type: "finished",
|
||||||
const readToEnd = async (reader: ReadableStreamDefaultReader<Uint8Array>) => {
|
output: output.buffer,
|
||||||
const chunks: Uint8Array[] = [];
|
};
|
||||||
let done = false;
|
}
|
||||||
while (!done) {
|
}
|
||||||
const { value, done: d } = await reader.read();
|
};
|
||||||
if (value) chunks.push(value);
|
|
||||||
done = d;
|
const readToEnd = async (reader: ReadableStreamDefaultReader<Uint8Array>) => {
|
||||||
}
|
const chunks: Uint8Array[] = [];
|
||||||
const blob = new Blob(chunks, { type: "application/zip" });
|
let done = false;
|
||||||
const arrayBuffer = await blob.arrayBuffer();
|
while (!done) {
|
||||||
return new Uint8Array(arrayBuffer);
|
const { value, done: d } = await reader.read();
|
||||||
};
|
if (value) chunks.push(value);
|
||||||
|
done = d;
|
||||||
const magickToBlob = async (img: IMagickImage): Promise<Blob> => {
|
}
|
||||||
const canvas = new OffscreenCanvas(img.width, img.height);
|
const blob = new Blob(chunks, { type: "application/zip" });
|
||||||
return new Promise<Blob>((resolve, reject) =>
|
const arrayBuffer = await blob.arrayBuffer();
|
||||||
img.getPixels(async (p) => {
|
return new Uint8Array(arrayBuffer);
|
||||||
const area = p.getArea(0, 0, img.width, img.height);
|
};
|
||||||
const chunkSize = img.hasAlpha ? 4 : 3;
|
|
||||||
const chunks = Math.ceil(area.length / chunkSize);
|
const magickToBlob = async (img: IMagickImage): Promise<Blob> => {
|
||||||
const data = new Uint8ClampedArray(chunks * 4);
|
const canvas = new OffscreenCanvas(img.width, img.height);
|
||||||
|
return new Promise<Blob>((resolve, reject) =>
|
||||||
for (let j = 0, k = 0; j < area.length; j += chunkSize, k += 4) {
|
img.getPixels(async (p) => {
|
||||||
data[k] = area[j];
|
const area = p.getArea(0, 0, img.width, img.height);
|
||||||
data[k + 1] = area[j + 1];
|
const chunkSize = img.hasAlpha ? 4 : 3;
|
||||||
data[k + 2] = area[j + 2];
|
const chunks = Math.ceil(area.length / chunkSize);
|
||||||
data[k + 3] = img.hasAlpha ? area[j + 3] : 255;
|
const data = new Uint8ClampedArray(chunks * 4);
|
||||||
}
|
|
||||||
|
for (let j = 0, k = 0; j < area.length; j += chunkSize, k += 4) {
|
||||||
const ctx = canvas.getContext("2d");
|
data[k] = area[j];
|
||||||
if (!ctx) {
|
data[k + 1] = area[j + 1];
|
||||||
reject(new Error("Failed to get canvas context"));
|
data[k + 2] = area[j + 2];
|
||||||
return;
|
data[k + 3] = img.hasAlpha ? area[j + 3] : 255;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.putImageData(new ImageData(data, img.width, img.height), 0, 0);
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) {
|
||||||
const blob = await canvas.convertToBlob({
|
reject(new Error("Failed to get canvas context"));
|
||||||
type: "image/png",
|
return;
|
||||||
});
|
}
|
||||||
|
|
||||||
resolve(blob);
|
ctx.putImageData(new ImageData(data, img.width, img.height), 0, 0);
|
||||||
}),
|
|
||||||
);
|
const blob = await canvas.convertToBlob({
|
||||||
};
|
type: "image/png",
|
||||||
|
});
|
||||||
const magickConvert = async (img: IMagickImage, to: string) => {
|
|
||||||
const vips = await vipsPromise;
|
resolve(blob);
|
||||||
|
}),
|
||||||
const intermediary = await magickToBlob(img);
|
);
|
||||||
const buf = await intermediary.arrayBuffer();
|
};
|
||||||
|
|
||||||
const imgVips = vips.Image.newFromBuffer(buf);
|
const magickConvert = async (img: IMagickImage, to: string) => {
|
||||||
const output = imgVips.writeToBuffer(to);
|
const vips = await vipsPromise;
|
||||||
|
|
||||||
imgVips.delete();
|
const intermediary = await magickToBlob(img);
|
||||||
img.dispose();
|
const buf = await intermediary.arrayBuffer();
|
||||||
|
|
||||||
return output;
|
const imgVips = vips.Image.newFromBuffer(buf);
|
||||||
};
|
const output = imgVips.writeToBuffer(to);
|
||||||
|
|
||||||
onmessage = async (e) => {
|
imgVips.delete();
|
||||||
const message = e.data;
|
img.dispose();
|
||||||
try {
|
|
||||||
const res = await handleMessage(message);
|
return output;
|
||||||
if (!res) return;
|
};
|
||||||
postMessage({
|
|
||||||
...res,
|
onmessage = async (e) => {
|
||||||
id: message.id,
|
const message = e.data;
|
||||||
});
|
try {
|
||||||
} catch (e) {
|
const res = await handleMessage(message);
|
||||||
postMessage({
|
if (!res) return;
|
||||||
type: "error",
|
postMessage({
|
||||||
error: e,
|
...res,
|
||||||
id: message.id,
|
id: message.id,
|
||||||
});
|
});
|
||||||
}
|
} catch (e) {
|
||||||
};
|
postMessage({
|
||||||
|
type: "error",
|
||||||
|
error: e,
|
||||||
|
id: message.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -1,113 +1,113 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { flip } from "$lib/animation";
|
import { flip } from "$lib/animation";
|
||||||
import Uploader from "$lib/components/functional/Uploader.svelte";
|
import Uploader from "$lib/components/functional/Uploader.svelte";
|
||||||
import Panel from "$lib/components/visual/Panel.svelte";
|
import Panel from "$lib/components/visual/Panel.svelte";
|
||||||
import { files } from "$lib/store/index.svelte";
|
import { files } from "$lib/store/index.svelte";
|
||||||
import { quintOut } from "svelte/easing";
|
import { quintOut } from "svelte/easing";
|
||||||
import { blur } from "svelte/transition";
|
import { blur } from "svelte/transition";
|
||||||
|
|
||||||
const images = $derived(
|
const images = $derived(
|
||||||
files.files.filter((f) =>
|
files.files.filter((f) =>
|
||||||
f.converters.map((c) => c.name).includes("libvips"),
|
f.converters.map((c) => c.name).includes("libvips"),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
let forcedBlobURLs = $state<Map<string, string>>(new Map());
|
let forcedBlobURLs = $state<Map<string, string>>(new Map());
|
||||||
|
|
||||||
const jpegify = () => {
|
const jpegify = () => {
|
||||||
const imgs = [...images];
|
const imgs = [...images];
|
||||||
imgs.map(async (f, i) => {
|
imgs.map(async (f, i) => {
|
||||||
f.to = ".jpeg";
|
f.to = ".jpeg";
|
||||||
const result = await f.convert(compression);
|
const result = await f.convert(compression);
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
forcedBlobURLs.set(f.id, URL.createObjectURL(result.file));
|
forcedBlobURLs.set(f.id, URL.createObjectURL(result.file));
|
||||||
forcedBlobURLs = new Map([...forcedBlobURLs]);
|
forcedBlobURLs = new Map([...forcedBlobURLs]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let compressionInverted = $state(10);
|
let compressionInverted = $state(10);
|
||||||
const compression = $derived(100 - compressionInverted);
|
const compression = $derived(100 - compressionInverted);
|
||||||
const processing = $derived(images.map((f) => f.processing).includes(true));
|
const processing = $derived(images.map((f) => f.processing).includes(true));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto w-full max-w-[778px] flex flex-col gap-8">
|
<div class="mx-auto w-full max-w-[778px] flex flex-col gap-8">
|
||||||
<h1 class="text-5xl text-center">SECRET JPEGIFY!!!</h1>
|
<h1 class="text-5xl text-center">SECRET JPEGIFY!!!</h1>
|
||||||
<p class="text-muted text-center -mt-4 font-normal italic">
|
<p class="text-muted text-center -mt-4 font-normal italic">
|
||||||
(shh... don't tell anyone!)
|
(shh... don't tell anyone!)
|
||||||
</p>
|
</p>
|
||||||
<Uploader class="w-full h-64" jpegify={true} />
|
<Uploader class="w-full h-64" jpegify={true} />
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min="1"
|
min="1"
|
||||||
max="100"
|
max="100"
|
||||||
step="1"
|
step="1"
|
||||||
class="w-full h-2 bg-panel rounded-lg appearance-none cursor-pointer"
|
class="w-full h-2 bg-panel rounded-lg appearance-none cursor-pointer"
|
||||||
bind:value={compressionInverted}
|
bind:value={compressionInverted}
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onclick={jpegify}
|
onclick={jpegify}
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
class="btn bg-accent text-black rounded-2xl text-2xl w-full mx-auto"
|
class="btn bg-accent text-black rounded-2xl text-2xl w-full mx-auto"
|
||||||
>JPEGIFY {compressionInverted}%!!!</button
|
>JPEGIFY {compressionInverted}%!!!</button
|
||||||
>
|
>
|
||||||
<div class="flex flex-wrap flex-row justify-center gap-4">
|
<div class="flex flex-wrap flex-row justify-center gap-4">
|
||||||
{#each images as file, i (file.id)}
|
{#each images as file, i (file.id)}
|
||||||
<div
|
<div
|
||||||
class="max-w-full w-full h-96"
|
class="max-w-full w-full h-96"
|
||||||
animate:flip={{ duration: 400, easing: quintOut }}
|
animate:flip={{ duration: 400, easing: quintOut }}
|
||||||
transition:blur={{
|
transition:blur={{
|
||||||
duration: 400,
|
duration: 400,
|
||||||
amount: 8,
|
amount: 8,
|
||||||
easing: quintOut,
|
easing: quintOut,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Panel class="w-full h-full flex flex-col gap-4 relative z-0">
|
<Panel class="w-full h-full flex flex-col gap-4 relative z-0">
|
||||||
<div
|
<div
|
||||||
class="relative rounded-xl flex-grow overflow-hidden flex items-center justify-center"
|
class="relative rounded-xl flex-grow overflow-hidden flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={forcedBlobURLs.get(file.id) ||
|
src={forcedBlobURLs.get(file.id) ||
|
||||||
file.result?.blobUrl ||
|
file.result?.blobUrl ||
|
||||||
file.blobUrl}
|
file.blobUrl}
|
||||||
alt={file.name}
|
alt={file.name}
|
||||||
class="h-full relative"
|
class="h-full relative"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
src={forcedBlobURLs.get(file.id) ||
|
src={forcedBlobURLs.get(file.id) ||
|
||||||
file.result?.blobUrl ||
|
file.result?.blobUrl ||
|
||||||
file.blobUrl}
|
file.blobUrl}
|
||||||
alt={file.name}
|
alt={file.name}
|
||||||
class="h-full absolute top-0 left-0 w-full object-cover blur-2xl -z-10"
|
class="h-full absolute top-0 left-0 w-full object-cover blur-2xl -z-10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-shrink-0 flex items-center gap-4 w-full">
|
<div class="flex-shrink-0 flex items-center gap-4 w-full">
|
||||||
<button
|
<button
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
file?.download();
|
file?.download();
|
||||||
}}
|
}}
|
||||||
disabled={!!!file.result}
|
disabled={!!!file.result}
|
||||||
class="btn bg-accent text-black rounded-2xl text-2xl w-full mx-auto"
|
class="btn bg-accent text-black rounded-2xl text-2xl w-full mx-auto"
|
||||||
>
|
>
|
||||||
Download
|
Download
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
URL.revokeObjectURL(
|
URL.revokeObjectURL(
|
||||||
forcedBlobURLs.get(file.id) || "",
|
forcedBlobURLs.get(file.id) || "",
|
||||||
);
|
);
|
||||||
forcedBlobURLs.delete(file.id);
|
forcedBlobURLs.delete(file.id);
|
||||||
files.files = files.files.filter(
|
files.files = files.files.filter(
|
||||||
(f) => f.id !== file.id,
|
(f) => f.id !== file.id,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
class="btn border-accent-red border-2 bg-transparent text-black dynadark:text-white rounded-2xl text-2xl w-full mx-auto"
|
class="btn border-accent-red border-2 bg-transparent text-black dynadark:text-white rounded-2xl text-2xl w-full mx-auto"
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue