NNicheStackTutorials
← All tutorials
Frontend Tools

b64tool: Encode Images to Data URLs Without Uploading to Random Sites

Needed Base64 for a static site's inline SVG fallback. Built encode/decode with file picker, UTF-8 safety, and the atob Unicode crash fix.

·10 min read·Frontend Tools
b64tool: Encode Images to Data URLs Without Uploading to Random Sites cover

Scenario: Client refused third-party encoder sites for contract PDF thumbnails. Everything had to run client-side.

Full tool

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Base64 Encode / Decode</title>
  <style>
    body { font-family: system-ui; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
    textarea { width: 100%; min-height: 120px; font-family: monospace; font-size: 12px; }
    .row { margin: .75rem 0; display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; }
    #preview { max-width: 200px; max-height: 200px; margin-top: 1rem; }
    .err { color: #b91c1c; }
  </style>
</head>
<body>
  <h1>Base64 Tool</h1>
  <div class="row">
    <input type="file" id="file" accept="image/*,.txt,.json">
    <button id="enc-text">Encode text</button>
    <button id="dec-text">Decode to text</button>
    <button id="copy">Copy output</button>
  </div>
  <textarea id="in" placeholder="Text or paste base64"></textarea>
  <textarea id="out" readonly placeholder="Result"></textarea>
  <p id="msg"></p>
  <img id="preview" alt="" hidden>
  <script>
    const input = document.getElementById('in');
    const output = document.getElementById('out');
    const msg = document.getElementById('msg');
    const preview = document.getElementById('preview');

    function utf8ToB64(str) {
      const bytes = new TextEncoder().encode(str);
      let bin = '';
      bytes.forEach(b => bin += String.fromCharCode(b));
      return btoa(bin);
    }

    function b64ToUtf8(b64) {
      const bin = atob(b64.replace(/\s/g, ''));
      const bytes = Uint8Array.from(bin, c => c.charCodeAt(0));
      return new TextDecoder().decode(bytes);
    }

    document.getElementById('enc-text').onclick = () => {
      msg.textContent = '';
      try {
        output.value = utf8ToB64(input.value);
      } catch (e) { msg.textContent = e.message; msg.className = 'err'; }
    };

    document.getElementById('dec-text').onclick = () => {
      msg.textContent = '';
      try {
        output.value = b64ToUtf8(input.value);
      } catch (e) { msg.textContent = 'Invalid Base64: ' + e.message; msg.className = 'err'; }
    };

    document.getElementById('file').onchange = async (e) => {
      const file = e.target.files?.[0];
      if (!file) return;
      const buf = await file.arrayBuffer();
      const bytes = new Uint8Array(buf);
      let bin = '';
      bytes.forEach(b => bin += String.fromCharCode(b));
      const b64 = btoa(bin);
      const mime = file.type || 'application/octet-stream';
      output.value = 'data:' + mime + ';base64,' + b64;
      if (mime.startsWith('image/')) {
        preview.src = output.value;
        preview.hidden = false;
      } else {
        preview.hidden = true;
      }
      msg.textContent = file.name + ' (' + (file.size / 1024).toFixed(1) + ' KB)';
      msg.className = '';
    };

    document.getElementById('copy').onclick = () => {
      navigator.clipboard.writeText(output.value);
      msg.textContent = 'Copied.';
    };
  </script>
</body>
</html>

Classic Unicode crash

btoa('café') // InvalidCharacterError

Never use raw btoa on user strings. TextEncoder → binary string → btoa is the fix above.

Data URL size warning

A 400KB PNG becomes ~533KB Base64. I warn above 100KB:

if (file.size > 100_000) msg.textContent += ' — large for inline CSS';

Verify

  1. Encode Hello 世界 → decode → same string
  2. Upload PNG → data URL + preview shows
  3. Paste invalid !!! base64 → clear error

Tags: Base64EncodingVanilla JSPrivacy