Files
Tools/base64-decoder/base64-decoder.html
T
LyAhn 3ee7d7a3ac feat(base64-decoder): add base64 to file decoder web tool
Single-file browser tool that decodes a base64 string and saves the
result back out as a file. No server, no dependencies, nothing uploaded.

Input cleanup covers data URIs, URL-safe alphabet, missing padding,
line wrapping, percent-encoding and wrapping quotes. Type detection
runs magic bytes (~30 signatures), then the data URI's declared MIME,
then UTF-8 content sniffing for SVG/HTML/XML/JSON/PEM; unrecognised
payloads fall back to .bin with a hex dump. Images, audio, video, PDF
and text are previewed inline before download.
2026-07-27 21:08:09 +01:00

573 lines
21 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Base64 &rarr; File</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
font-family:"Segoe UI",Calibri,Arial,sans-serif;
background:#0f1316;color:#c8d8ea;display:flex;flex-direction:column;overflow:hidden
}
/* ── Header ── */
.header{
flex-shrink:0;height:54px;display:flex;align-items:center;gap:14px;
padding:0 22px;
background:linear-gradient(135deg,#14191b 0%,#0e1e28 60%,#0b2020 100%);
border-bottom:2px solid transparent;
border-image:linear-gradient(to right,#3eafff,#2acc94) 1;
}
.brand-name{font-size:18px;font-weight:800;letter-spacing:.3px;color:#fff}
.brand-name em{
font-style:normal;
background:linear-gradient(90deg,#3eafff,#2acc94);
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text
}
.brand-pipe{color:#2a3a3a;font-size:13px}
.brand-sub{font-size:12px;color:#4a6878}
.header-right{margin-left:auto;display:flex;align-items:center;gap:10px}
.stat-chip{
background:rgba(62,175,255,.08);border:1px solid rgba(62,175,255,.18);
border-radius:20px;padding:3px 12px;font-size:11px;color:#5a8898;white-space:nowrap
}
.stat-chip strong{color:#2acc94}
/* ── Layout ── */
.layout{display:flex;flex:1;min-height:0}
.pane{display:flex;flex-direction:column;min-width:0;min-height:0}
.pane-in{flex:1 1 50%;border-right:1px solid #1a2428}
.pane-out{flex:1 1 50%;background:#0c1014}
.pane-head{
flex-shrink:0;display:flex;align-items:center;gap:10px;
padding:9px 16px;border-bottom:1px solid #1a2428;background:#0c1014
}
.pane-out .pane-head{background:#0a0e11}
.pane-title{
font-size:9px;font-weight:700;letter-spacing:2px;
text-transform:uppercase;color:#2a4048
}
.pane-tools{margin-left:auto;display:flex;align-items:center;gap:6px}
/* ── Input ── */
.drop{position:relative;flex:1;display:flex;min-height:0}
textarea{
flex:1;resize:none;border:0;outline:0;padding:14px 16px;
background:#0f1316;color:#a8c4d8;
font-family:"Cascadia Mono",Consolas,"Courier New",monospace;
font-size:12px;line-height:1.55;word-break:break-all
}
textarea::placeholder{color:#2f4450}
.drop.over::after{
content:"Drop file to load its contents";
position:absolute;inset:8px;border:2px dashed #3eafff;border-radius:8px;
background:rgba(62,175,255,.07);display:flex;align-items:center;justify-content:center;
font-size:13px;color:#3eafff;pointer-events:none
}
/* ── Buttons ── */
button{
font-family:inherit;font-size:11px;cursor:pointer;
background:#141e24;color:#6a8898;border:1px solid #1e2c34;
border-radius:5px;padding:5px 12px;transition:background .1s,color .1s,border-color .1s
}
button:hover:not(:disabled){background:#18242c;color:#a0c0d0;border-color:#2a3c46}
button:disabled{opacity:.35;cursor:not-allowed}
button.primary{
background:linear-gradient(90deg,#3eafff,#2acc94);color:#08191f;
border:0;font-weight:700;padding:7px 18px
}
button.primary:hover:not(:disabled){filter:brightness(1.12)}
/* ── Output ── */
.out-body{flex:1;overflow-y:auto;min-height:0;padding:16px}
.empty{
height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;
gap:8px;color:#2f4450;font-size:13px;text-align:center;padding:20px
}
.empty .big{font-size:30px;opacity:.5}
.card{
background:#0f151a;border:1px solid #1a2428;border-radius:8px;
padding:14px 16px;margin-bottom:14px
}
.card.err{border-color:#5a2430;background:#1a1013}
.card.err .card-label{color:#e06a7a}
.card-label{
font-size:9px;font-weight:700;letter-spacing:2px;text-transform:uppercase;
color:#2a4048;margin-bottom:10px
}
.err-msg{font-size:13px;color:#e8a0aa;line-height:1.5}
.err-hint{font-size:11px;color:#8a5a64;margin-top:8px;line-height:1.5}
.meta{display:grid;grid-template-columns:auto 1fr;gap:7px 16px;font-size:12px}
.meta dt{color:#4a6878;white-space:nowrap}
.meta dd{color:#c8d8ea;word-break:break-all}
.meta dd code{
font-family:"Cascadia Mono",Consolas,monospace;font-size:11px;
color:#2acc94;background:#0a1418;border-radius:3px;padding:1px 5px
}
.save-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
input[type=text]{
flex:1;min-width:150px;font-family:"Cascadia Mono",Consolas,monospace;font-size:12px;
background:#0a0e11;color:#c8d8ea;border:1px solid #1e2c34;border-radius:5px;
padding:7px 10px;outline:0
}
input[type=text]:focus{border-color:#3eafff}
.preview img,.preview video{
max-width:100%;max-height:340px;display:block;margin:0 auto;
border-radius:5px;background:#0a0e11
}
.preview audio{width:100%}
.preview iframe{width:100%;height:420px;border:0;border-radius:5px;background:#fff}
.preview pre{
max-height:340px;overflow:auto;
font-family:"Cascadia Mono",Consolas,monospace;font-size:11.5px;line-height:1.55;
color:#a8c4d8;white-space:pre-wrap;word-break:break-word
}
.hex{
font-family:"Cascadia Mono",Consolas,monospace;font-size:11.5px;line-height:1.6;
color:#5a8898;white-space:pre;overflow-x:auto
}
.hex b{color:#2acc94;font-weight:400}
.hex i{color:#2a4048;font-style:normal}
@media (max-width:820px){
body{overflow:auto}
.layout{flex-direction:column}
.pane-in{border-right:0;border-bottom:1px solid #1a2428}
.pane-in .drop{min-height:200px}
}
</style>
</head>
<body>
<div class="header">
<span class="brand-name">Base<em>64</em> &rarr; File</span>
<span class="brand-pipe">|</span>
<span class="brand-sub">decode base64 and save it back out as a file</span>
<div class="header-right">
<span class="stat-chip">In <strong id="statIn">0</strong></span>
<span class="stat-chip">Out <strong id="statOut">0 B</strong></span>
</div>
</div>
<div class="layout">
<div class="pane pane-in">
<div class="pane-head">
<span class="pane-title">Base64 input</span>
<div class="pane-tools">
<button id="btnPaste">Paste</button>
<button id="btnLoad">Load file</button>
<button id="btnClear">Clear</button>
</div>
</div>
<div class="drop" id="drop">
<textarea id="input" spellcheck="false" autocomplete="off"
placeholder="Paste base64 here.&#10;&#10;Data URIs (data:image/png;base64,...), URL-safe base64, missing padding and&#10;wrapped lines are all handled automatically."></textarea>
</div>
</div>
<div class="pane pane-out">
<div class="pane-head">
<span class="pane-title">Decoded file</span>
<div class="pane-tools">
<button id="btnCopy" disabled>Copy text</button>
<button class="primary" id="btnSave" disabled>Download</button>
</div>
</div>
<div class="out-body" id="out">
<div class="empty">
<div class="big">&#8681;</div>
<div>Paste or drop some base64 to get started</div>
</div>
</div>
</div>
</div>
<input type="file" id="filePicker" hidden>
<script>
(function(){
"use strict";
var input = document.getElementById('input');
var out = document.getElementById('out');
var drop = document.getElementById('drop');
var filePicker = document.getElementById('filePicker');
var statIn = document.getElementById('statIn');
var statOut = document.getElementById('statOut');
var btnSave = document.getElementById('btnSave');
var btnCopy = document.getElementById('btnCopy');
var state = { bytes:null, name:'', mime:'', text:null, url:null };
/* ── Magic-byte signatures ──────────────────────────────────────────
Checked in order; first match wins. `at` is the byte offset of the
signature, `sig` is a byte array (null = wildcard for that byte). */
var SIGS = [
{ ext:'png', mime:'image/png', at:0, sig:[0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A] },
{ ext:'jpg', mime:'image/jpeg', at:0, sig:[0xFF,0xD8,0xFF] },
{ ext:'gif', mime:'image/gif', at:0, sig:[0x47,0x49,0x46,0x38] },
{ ext:'webp', mime:'image/webp', at:8, sig:[0x57,0x45,0x42,0x50] },
{ ext:'bmp', mime:'image/bmp', at:0, sig:[0x42,0x4D] },
{ ext:'ico', mime:'image/x-icon', at:0, sig:[0x00,0x00,0x01,0x00] },
{ ext:'pdf', mime:'application/pdf', at:0, sig:[0x25,0x50,0x44,0x46] },
{ ext:'wav', mime:'audio/wav', at:8, sig:[0x57,0x41,0x56,0x45] },
{ ext:'avi', mime:'video/x-msvideo', at:8, sig:[0x41,0x56,0x49,0x20] },
{ ext:'mp4', mime:'video/mp4', at:4, sig:[0x66,0x74,0x79,0x70] },
{ ext:'mkv', mime:'video/x-matroska', at:0, sig:[0x1A,0x45,0xDF,0xA3] },
{ ext:'ogg', mime:'audio/ogg', at:0, sig:[0x4F,0x67,0x67,0x53] },
{ ext:'flac', mime:'audio/flac', at:0, sig:[0x66,0x4C,0x61,0x43] },
{ ext:'mp3', mime:'audio/mpeg', at:0, sig:[0x49,0x44,0x33] },
{ ext:'mp3', mime:'audio/mpeg', at:0, sig:[0xFF,0xFB] },
{ ext:'zip', mime:'application/zip', at:0, sig:[0x50,0x4B,0x03,0x04] },
{ ext:'gz', mime:'application/gzip', at:0, sig:[0x1F,0x8B] },
{ ext:'bz2', mime:'application/x-bzip2', at:0, sig:[0x42,0x5A,0x68] },
{ ext:'xz', mime:'application/x-xz', at:0, sig:[0xFD,0x37,0x7A,0x58,0x5A,0x00] },
{ ext:'7z', mime:'application/x-7z-compressed', at:0, sig:[0x37,0x7A,0xBC,0xAF,0x27,0x1C] },
{ ext:'rar', mime:'application/vnd.rar', at:0, sig:[0x52,0x61,0x72,0x21,0x1A,0x07] },
{ ext:'tar', mime:'application/x-tar', at:257, sig:[0x75,0x73,0x74,0x61,0x72] },
{ ext:'sqlite', mime:'application/vnd.sqlite3', at:0,
sig:[0x53,0x51,0x4C,0x69,0x74,0x65,0x20,0x66,0x6F,0x72,0x6D,0x61,0x74,0x20,0x33,0x00] },
{ ext:'ttf', mime:'font/ttf', at:0, sig:[0x00,0x01,0x00,0x00,0x00] },
{ ext:'otf', mime:'font/otf', at:0, sig:[0x4F,0x54,0x54,0x4F] },
{ ext:'woff', mime:'font/woff', at:0, sig:[0x77,0x4F,0x46,0x46] },
{ ext:'woff2',mime:'font/woff2', at:0, sig:[0x77,0x4F,0x46,0x32] },
{ ext:'exe', mime:'application/vnd.microsoft.portable-executable', at:0, sig:[0x4D,0x5A] },
{ ext:'elf', mime:'application/x-elf', at:0, sig:[0x7F,0x45,0x4C,0x46] },
{ ext:'class',mime:'application/java-vm', at:0, sig:[0xCA,0xFE,0xBA,0xBE] }
];
function matches(bytes, s){
if (bytes.length < s.at + s.sig.length) return false;
for (var i = 0; i < s.sig.length; i++){
if (s.sig[i] !== null && bytes[s.at + i] !== s.sig[i]) return false;
}
return true;
}
function sniff(bytes){
for (var i = 0; i < SIGS.length; i++){
if (matches(bytes, SIGS[i])) return { ext:SIGS[i].ext, mime:SIGS[i].mime, sure:true };
}
return null;
}
/* Text-based formats have no magic bytes, so classify by content once
we know the payload decodes as valid UTF-8. */
function sniffText(txt){
var t = txt.replace(/^/, '').trimStart();
var head = t.slice(0, 400).toLowerCase();
if (/^<svg[\s>]/.test(head) || (head.indexOf('<?xml') === 0 && head.indexOf('<svg') > -1))
return { ext:'svg', mime:'image/svg+xml' };
if (head.indexOf('<!doctype html') === 0 || head.indexOf('<html') === 0)
return { ext:'html', mime:'text/html' };
if (head.indexOf('<?xml') === 0)
return { ext:'xml', mime:'application/xml' };
if (/^[{\[]/.test(t)){
try { JSON.parse(t); return { ext:'json', mime:'application/json' }; } catch(e){}
}
if (head.indexOf('-----begin ') === 0)
return { ext:'pem', mime:'application/x-pem-file' };
if (head.indexOf('#!') === 0)
return { ext:'sh', mime:'text/x-shellscript' };
if (/^%pdf/.test(head))
return { ext:'pdf', mime:'application/pdf' };
return { ext:'txt', mime:'text/plain' };
}
/* Last resort for a data URI whose payload has no magic bytes — the
declared MIME type usually names the extension we want. */
var MIME_EXT = {
'plain':'txt', 'jpeg':'jpg', 'javascript':'js', 'markdown':'md',
'octet-stream':'bin', 'msword':'doc', 'mpeg':'mp3', 'quicktime':'mov'
};
function extFromMime(mime){
var sub = (mime.split('/')[1] || '').split(';')[0].split('+')[0].trim().toLowerCase();
sub = sub.replace(/^(x-|vnd\.)/, '');
if (MIME_EXT[sub]) return MIME_EXT[sub];
return /^[a-z0-9]{1,8}$/.test(sub) ? sub : null;
}
function asUtf8(bytes){
try { return new TextDecoder('utf-8', {fatal:true}).decode(bytes); }
catch(e){ return null; }
}
function humanSize(n){
if (n < 1024) return n + ' B';
var u = ['KB','MB','GB'], i = -1, v = n;
do { v /= 1024; i++; } while (v >= 1024 && i < u.length - 1);
return v.toFixed(v < 10 ? 1 : 0) + ' ' + u[i];
}
function esc(s){
return String(s).replace(/[&<>"]/g, function(c){
return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];
});
}
/* ── Decode ──────────────────────────────────────────────────────── */
// Strips a data URI prefix, whitespace and quotes; converts URL-safe
// base64 to standard and restores missing '=' padding.
function normalise(raw){
var mime = '';
var s = raw.trim().replace(/^["'`]|["'`]$/g, '');
var m = /^data:([^;,]*)(;[^,]*)?,/i.exec(s);
if (m){
if (!/;base64/i.test(m[2] || '')) throw new Error('That data URI is not base64-encoded.');
mime = m[1] || '';
s = s.slice(m[0].length);
}
s = s.replace(/\s+/g, '');
if (/%[0-9a-f]{2}/i.test(s)){
try { s = decodeURIComponent(s); } catch(e){}
}
s = s.replace(/-/g, '+').replace(/_/g, '/');
s = s.replace(/=+$/, '');
var bad = s.match(/[^A-Za-z0-9+/]/);
if (bad) throw new Error('Not valid base64 — unexpected character "' + bad[0] + '".');
if (s.length % 4 === 1) throw new Error('Not valid base64 — the input length is truncated.');
while (s.length % 4) s += '=';
return { data:s, mime:mime };
}
function toBytes(b64){
var bin = atob(b64);
var bytes = new Uint8Array(bin.length);
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
return bytes;
}
function hexDump(bytes, rows){
var lines = [], limit = Math.min(bytes.length, rows * 16);
for (var o = 0; o < limit; o += 16){
var hex = '', txt = '';
for (var i = 0; i < 16; i++){
if (o + i < limit){
var b = bytes[o + i];
hex += (b < 16 ? '0' : '') + b.toString(16) + ' ';
txt += (b >= 32 && b < 127) ? esc(String.fromCharCode(b)) : '.';
} else {
hex += ' '; txt += ' ';
}
if (i === 7) hex += ' ';
}
lines.push('<i>' + ('0000000' + o.toString(16)).slice(-8) + '</i> ' + hex + ' <b>' + txt + '</b>');
}
if (bytes.length > limit) lines.push('<i>' + humanSize(bytes.length - limit) + ' more…</i>');
return lines.join('\n');
}
function reset(){
if (state.url) URL.revokeObjectURL(state.url);
state = { bytes:null, name:'', mime:'', text:null, url:null };
btnSave.disabled = true;
btnCopy.disabled = true;
statOut.textContent = '0 B';
}
function showEmpty(){
reset();
out.innerHTML = '<div class="empty"><div class="big">&#8681;</div>' +
'<div>Paste or drop some base64 to get started</div></div>';
}
function showError(msg, hint){
reset();
out.innerHTML = '<div class="card err"><div class="card-label">Could not decode</div>' +
'<div class="err-msg">' + esc(msg) + '</div>' +
(hint ? '<div class="err-hint">' + esc(hint) + '</div>' : '') + '</div>';
}
function decode(){
var raw = input.value;
statIn.textContent = raw.replace(/\s+/g, '').length.toLocaleString();
if (!raw.trim()){ showEmpty(); return; }
var norm, bytes;
try {
norm = normalise(raw);
bytes = toBytes(norm.data);
} catch(e){
showError(e.message, 'Check that the whole string was copied and that nothing ' +
'was truncated or line-wrapped with stray characters.');
return;
}
if (!bytes.length){ showError('The input decoded to zero bytes.'); return; }
var text = asUtf8(bytes);
var hit = sniff(bytes);
var kind;
if (hit) kind = hit;
else if (text !== null) kind = sniffText(text);
else kind = { ext:'bin', mime:'application/octet-stream' };
var mime = norm.mime || kind.mime;
if (!hit && norm.mime) kind.ext = extFromMime(norm.mime) || kind.ext;
reset();
state.bytes = bytes;
state.mime = mime;
state.text = (kind.ext === 'txt' || /^text\/|json|xml|svg|javascript|pem|shellscript/.test(kind.mime)) ? text : null;
state.name = 'decoded.' + kind.ext;
state.url = URL.createObjectURL(new Blob([bytes], {type:mime}));
statOut.textContent = humanSize(bytes.length);
btnSave.disabled = false;
btnCopy.disabled = state.text === null;
render(bytes, mime, kind, hit, norm.mime);
}
function render(bytes, mime, kind, hit, uriMime){
var detected = hit ? 'magic bytes'
: uriMime ? 'data URI header'
: kind.ext === 'bin' ? 'unrecognised — treated as binary'
: 'content sniffing';
var html =
'<div class="card"><div class="card-label">Save as</div>' +
'<div class="save-row">' +
'<input type="text" id="nameField" value="' + esc(state.name) + '" spellcheck="false">' +
'<button class="primary" id="btnSave2">Download</button>' +
'</div>' +
'</div>' +
'<div class="card"><div class="card-label">File</div>' +
'<dl class="meta">' +
'<dt>Type</dt><dd><code>' + esc(mime) + '</code></dd>' +
'<dt>Size</dt><dd>' + humanSize(bytes.length) +
' <span style="color:#4a6878">(' + bytes.length.toLocaleString() + ' bytes)</span></dd>' +
'<dt>Detected via</dt><dd style="color:#4a6878">' + detected + '</dd>' +
'</dl>' +
'</div>';
var p = '';
if (/^image\//.test(mime)){
p = '<img src="' + state.url + '" alt="decoded image">';
} else if (/^video\//.test(mime)){
p = '<video src="' + state.url + '" controls></video>';
} else if (/^audio\//.test(mime)){
p = '<audio src="' + state.url + '" controls></audio>';
} else if (mime === 'application/pdf'){
p = '<iframe src="' + state.url + '"></iframe>';
} else if (state.text !== null){
p = '<pre>' + esc(state.text.slice(0, 20000)) +
(state.text.length > 20000 ? '\n…' : '') + '</pre>';
} else {
p = '<div class="hex">' + hexDump(bytes, 12) + '</div>';
}
html += '<div class="card preview"><div class="card-label">Preview</div>' + p + '</div>';
out.innerHTML = html;
var nameField = document.getElementById('nameField');
nameField.addEventListener('input', function(){ state.name = nameField.value; });
nameField.addEventListener('keydown', function(e){ if (e.key === 'Enter') save(); });
document.getElementById('btnSave2').addEventListener('click', save);
}
function save(){
if (!state.bytes) return;
var a = document.createElement('a');
a.href = state.url;
a.download = (state.name || 'decoded.bin').trim() || 'decoded.bin';
document.body.appendChild(a);
a.click();
a.remove();
}
/* ── Events ──────────────────────────────────────────────────────── */
var timer;
input.addEventListener('input', function(){
clearTimeout(timer);
timer = setTimeout(decode, 120);
});
btnSave.addEventListener('click', save);
btnCopy.addEventListener('click', function(){
if (state.text === null) return;
navigator.clipboard.writeText(state.text).then(function(){
btnCopy.textContent = 'Copied';
setTimeout(function(){ btnCopy.textContent = 'Copy text'; }, 1200);
});
});
document.getElementById('btnClear').addEventListener('click', function(){
input.value = '';
input.focus();
decode();
});
document.getElementById('btnPaste').addEventListener('click', function(){
navigator.clipboard.readText().then(function(t){
input.value = t;
decode();
}).catch(function(){
input.focus();
});
});
document.getElementById('btnLoad').addEventListener('click', function(){
filePicker.click();
});
filePicker.addEventListener('change', function(){
if (filePicker.files[0]) readAsText(filePicker.files[0]);
filePicker.value = '';
});
function readAsText(file){
var r = new FileReader();
r.onload = function(){ input.value = r.result; decode(); };
r.readAsText(file);
}
['dragenter','dragover'].forEach(function(ev){
drop.addEventListener(ev, function(e){
e.preventDefault();
drop.classList.add('over');
});
});
['dragleave','drop'].forEach(function(ev){
drop.addEventListener(ev, function(e){
e.preventDefault();
if (ev === 'dragleave' && drop.contains(e.relatedTarget)) return;
drop.classList.remove('over');
});
});
drop.addEventListener('drop', function(e){
var f = e.dataTransfer.files[0];
if (f) readAsText(f);
});
window.addEventListener('keydown', function(e){
if ((e.ctrlKey || e.metaKey) && e.key === 's' && state.bytes){
e.preventDefault();
save();
}
});
input.focus();
})();
</script>
</body>
</html>