diff options
Diffstat (limited to 'src/routes/+page.svelte')
-rw-r--r-- | src/routes/+page.svelte | 202 |
1 files changed, 202 insertions, 0 deletions
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..7cc3eb7 --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,202 @@ +<!-- +MIT License + +Copyright (c) 2025 memdmp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + --> + +<script lang="ts"> + import Alerts, { alert, confirm } from '$lib/Alerts.svelte'; + import { onMount } from 'svelte'; + + let fileInput = $state(null as unknown as HTMLInputElement); + let fileName = $state(''); + let fileRaw = $state(null as null | File); + let canvas = $state(null as unknown as HTMLCanvasElement); + let imgUrl = $state(''); + let output = $state(''); + $effect(() => { + if (fileInput?.files?.length) { + const file = fileInput.files![0]; + fileName = file.name; + fileRaw = file ?? null; + } + }); + onMount(() => { + const el = document.createElement('canvas'); + const ctx = el.getContext('2d'); + if (!ctx) { + alert( + "Could not create 2d Canvas Context. This application will not work. Make sure any privacy-resistance features are disabled and your browser isn't ancient.", + { + title: 'Fatal Error', + } + ); + } else { + if ( + JSON.stringify(ctx.getImageData(0, 0, 32, 32).data) !== + JSON.stringify(ctx.getImageData(0, 0, 32, 32).data) + ) { + alert( + "Canvas readbacks not identical. This application may not work correctly. Make sure any privacy-resistance features are disabled and your browser isn't ancient.", + { + title: 'Error', + } + ); + } + } + }); + const update = async (fileRaw: File) => { + const blob = new Blob([await fileRaw.bytes()]); + const blobUrl = URL.createObjectURL(blob); + imgUrl = blobUrl; + }; + $effect(() => { + if (fileRaw) update(fileRaw); + else imgUrl = ''; + }); + const toHex = (v: number) => Math.floor(v).toString(16).padStart(2, '0'); + const save = ( + data: [r: number, g: number, b: number, a: number][], + width: number, + height: number + ) => { + let content = `<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="crispEdges" viewBox=${JSON.stringify(`0 0 ${width} ${height}`)} width=${JSON.stringify('' + width)} height=${JSON.stringify('' + height)}>`; + let item: (typeof data)[0]; + let x = 0; + let y = 0; + while ((item = data.shift()!)) { + if (++x >= width) (x = 0), y++; + if (item[3] !== 0) + content += `<rect fill="#${toHex(item[0])}${toHex(item[1])}${toHex(item[2])}${item[3] !== 255 ? toHex(item[3]) : ''}" x=${JSON.stringify('' + x)} y=${JSON.stringify('' + y)} width="1" height="1"/>`; + } + content += '</svg>'; + output = URL.createObjectURL( + new Blob([content], { + type: 'image/svg+xml', + }) + ); + }; +</script> + +<svelte:head> + <title>Bitmap to Pixelated SVG</title> + <link rel="stylesheet" href="/cs16.css" /> +</svelte:head> + +<Alerts /> +<div> + <h1>PixelSVG</h1> + <div> + <label style="display: flex; gap:4px;"> + <span class="cs-btn" + >{fileName ? `File: ${fileName}` : 'Upload a File'}</span + > + <img + src={imgUrl} + alt="" + style="height: 24px" + onload={async (e) => { + const imgEl = e.currentTarget as HTMLImageElement; + const rect = { + width: imgEl.naturalWidth, + height: imgEl.naturalHeight, + }; + if ( + rect.width * rect.height > 65536 && + !(await confirm( + `This image isn't small; it's made up of ${rect.width * rect.height} pixels. The outputted file could be up to ${((`<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="crispEdges" viewBox=${JSON.stringify(`0 0 ${rect.width} ${rect.height}`)} width=${JSON.stringify('' + rect.width)} height=${JSON.stringify('' + rect.height)}></svg>`.length + `<rect fill="#ffffffff" x="${rect.width}" y="${rect.height}" width="1" height="1"/>`.length * rect.width * rect.height) / 1024 / 1024).toFixed(1)} MB large. It may crash your browser. Are you sure you wish to continue?`, + { + title: 'Confirmation', + } + )) + ) + return (imgUrl = ''), (fileName = ''); + canvas.width = rect.width; + canvas.height = rect.height; + const ctx = canvas.getContext('2d', { + alpha: true, + colorSpace: 'display-p3', + }); + if (!ctx) throw new Error('no 2d context obtainable'); + try { + ctx.drawImage(e.currentTarget as HTMLImageElement, 0, 0); + } catch (error) { + await alert( + "Failed to draw image to canvas. The image's data may be invalid or the image may be too big. Check the Console.", + { + title: 'Failed to draw image', + } + ); + throw error; + } + const data = ctx.getImageData(0, 0, rect.width, rect.height, { + colorSpace: 'display-p3', + }); + let out = [] as [r: number, g: number, b: number, a: number][]; + for (let i = 0; i < data.data.length; i += 4) { + const r = data.data[i + 0]; + const g = data.data[i + 1]; + const b = data.data[i + 2]; + const a = data.data[i + 3]; + out.push([r, g, b, a]); + } + save(out, rect.width, rect.height); + }} + onerror={(e) => { + if (imgUrl !== '') + alert( + 'Failed to load the image into an HTML Image Element. Please make sure the image you provided is valid and in a format your browser understands.', + { + title: 'Failed to load image', + } + ); + }} + /> + <input + type="file" + bind:this={fileInput} + onchange={() => { + if (fileInput?.files?.length) { + const file = fileInput.files![0]; + fileName = file.name; + fileRaw = file ?? null; + } + }} + style="opacity:0;position:fixed;top:0;left:0;width:0;height:0;pointer-events:none;" + /> + </label> + </div> + {#if output} + <div style="margin-top:12px;margin-bottom:12px;"> + Output:<br /> + <a href={output} target="_blank" + ><img src={output} alt="could not render" /> (open)</a + > + </div> + {/if} + <div style="opacity:0.5"> + {imgUrl ? 'Canvas:' : ''}<br /><canvas + bind:this={canvas} + width="0" + height="0" + ></canvas> + </div> +</div> |