icns

Easily create .icns files (Mac Icons) with this Go library or the included CLI.
Log | Files | Refs | LICENSE

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>