index.html (4738B)
1 <!doctype html> 2 <html lang="en"> 3 <head> 4 <meta charset="utf-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1"> 6 <title>Icon converter</title> 7 <style> 8 :root { color-scheme: light dark; --edge: #8883; --accent: #2060c0; } 9 body { margin: 0; font: 15px/1.5 system-ui, sans-serif; } 10 main { max-width: 42rem; margin: 0 auto; padding: 2rem 1rem; } 11 h1 { margin: 0 0 .25rem; font-size: 1.6rem; } 12 .lede { margin: 0 0 1.5rem; opacity: .75; } 13 #drop { border: 2px dashed var(--edge); border-radius: .5rem; padding: 3rem 1rem; text-align: center; } 14 #drop.over { border-color: var(--accent); } 15 fieldset { border: 1px solid var(--edge); border-radius: .5rem; margin: 1.5rem 0; } 16 label { margin-right: 1rem; } 17 #status { min-height: 1.5rem; } 18 #status.bad { color: #c02020; } 19 ul { padding-left: 1.25rem; } 20 li { font-family: ui-monospace, monospace; font-size: .85rem; } 21 a#download { display: inline-block; margin-top: .5rem; padding: .5rem 1rem; 22 background: var(--accent); color: #fff; border-radius: .35rem; text-decoration: none; } 23 </style> 24 </head> 25 <body> 26 <main> 27 <h1>Icon converter</h1> 28 <p class="lede">Convert app icons between <code>.icns</code>, <code>.ico</code> and ordinary images. 29 Everything runs in your browser — your image is never uploaded.</p> 30 <p class="lede">A demonstration of <code>cmd/wasm</code>, the browser build of this library.</p> 31 32 <div id="drop"> 33 Drop an image here, or <label><a href="#" id="pick">choose a file</a><input type="file" id="file" hidden></label> 34 </div> 35 36 <fieldset> 37 <legend>Convert to</legend> 38 <label><input type="radio" name="format" value="icns" checked> .icns (macOS)</label> 39 <label><input type="radio" name="format" value="ico"> .ico (Windows)</label> 40 <label><input type="radio" name="format" value="png"> .png</label> 41 <label><input type="radio" name="format" value="jpg"> .jpg</label> 42 </fieldset> 43 44 <p id="status"></p> 45 <ul id="icons"></ul> 46 <a id="download" hidden>Download</a> 47 </main> 48 49 <script src="wasm_exec.js"></script> 50 <script> 51 const $ = (id) => document.getElementById(id); 52 53 // The engine is only fetched once someone actually has a file, so the page 54 // itself stays small. 55 let starting; 56 function engine() { 57 if (!starting) { 58 starting = (async () => { 59 const go = new Go(); 60 let wasm; 61 try { 62 wasm = await WebAssembly.instantiateStreaming(fetch('main.wasm'), go.importObject); 63 } catch { 64 // Some static hosts serve .wasm with the wrong content type, which 65 // streaming instantiation refuses. 66 const bytes = await (await fetch('main.wasm')).arrayBuffer(); 67 wasm = await WebAssembly.instantiate(bytes, go.importObject); 68 } 69 // Not awaited: the program installs its API and then parks forever. 70 go.run(wasm.instance); 71 if (!window.icns) throw new Error('the converter did not start'); 72 })(); 73 } 74 return starting; 75 } 76 77 function say(text, bad) { 78 $('status').textContent = text; 79 $('status').className = bad ? 'bad' : ''; 80 } 81 82 async function handle(file) { 83 $('download').hidden = true; 84 $('icons').replaceChildren(); 85 say('Loading the converter…'); 86 try { 87 await engine(); 88 } catch (err) { 89 say(err.message, true); 90 return; 91 } 92 const bytes = new Uint8Array(await file.arrayBuffer()); 93 const found = icns.inspect(bytes); 94 if (found.ok) { 95 for (const icon of found.icons) { 96 const li = document.createElement('li'); 97 li.textContent = icon; 98 $('icons').appendChild(li); 99 } 100 } 101 const format = document.querySelector('input[name=format]:checked').value; 102 say('Converting…'); 103 const out = icns.convert(bytes, format); 104 if (!out.ok) { 105 say(out.error, true); 106 return; 107 } 108 const name = file.name.replace(/\.[^.]*$/, '') + '.' + format; 109 const link = $('download'); 110 link.href = URL.createObjectURL(new Blob([out.data], { type: 'application/octet-stream' })); 111 link.download = name; 112 link.textContent = 'Download ' + name; 113 link.hidden = false; 114 say(`${file.name} → ${name} (${out.data.length.toLocaleString()} bytes)`); 115 } 116 117 const drop = $('drop'); 118 for (const name of ['dragenter', 'dragover']) { 119 drop.addEventListener(name, (e) => { e.preventDefault(); drop.classList.add('over'); }); 120 } 121 for (const name of ['dragleave', 'drop']) { 122 drop.addEventListener(name, (e) => { e.preventDefault(); drop.classList.remove('over'); }); 123 } 124 drop.addEventListener('drop', (e) => { 125 if (e.dataTransfer.files.length) handle(e.dataTransfer.files[0]); 126 }); 127 $('pick').addEventListener('click', (e) => { e.preventDefault(); $('file').click(); }); 128 $('file').addEventListener('change', (e) => { 129 if (e.target.files.length) handle(e.target.files[0]); 130 }); 131 </script> 132 </body> 133 </html>