function initBase64Tool() { const CHUNK_BYTES = 32768; const CHUNK_BASE64 = 65536; // must be divisible by 4 for decoding chunks const inputEl = document.getElementById("base64Input"); const outputEl = document.getElementById("base64Output"); const statusEl = document.getElementById("base64Status"); const inputMetaEl = document.getElementById("base64InputMeta"); const outputMetaEl = document.getElementById("base64OutputMeta"); const encodeBtn = document.getElementById("base64EncodeBtn"); const decodeBtn = document.getElementById("base64DecodeBtn"); const swapBtn = document.getElementById("base64SwapBtn"); const copyBtn = document.getElementById("base64CopyBtn"); const clearBtn = document.getElementById("base64ClearBtn"); const downloadBtn = document.getElementById("base64DownloadBtn"); const autoModeCheckbox = document.getElementById("base64AutoMode"); const fileInput = document.getElementById("base64FileInput"); const mimeInput = document.getElementById("base64MimeType"); if (!inputEl || !outputEl) return; function tick() { return new Promise((resolve) => setTimeout(resolve, 0)); } function setStatus(text, isError) { if (!statusEl) return; statusEl.textContent = text; statusEl.classList.toggle("status-error", !!isError); statusEl.classList.toggle("status-success", !isError); } function updateMeta() { const encoder = new TextEncoder(); const inputChars = inputEl.value.length; const outputChars = outputEl.value.length; const inputBytes = encoder.encode(inputEl.value).length; const outputBytes = encoder.encode(outputEl.value).length; if (inputMetaEl) inputMetaEl.textContent = `Chars: ${inputChars} | Bytes: ${inputBytes}`; if (outputMetaEl) outputMetaEl.textContent = `Chars: ${outputChars} | Bytes: ${outputBytes}`; } function updateButtonStates() { const hasInput = !!inputEl.value.trim(); const hasOutput = !!outputEl.value.trim(); if (encodeBtn) encodeBtn.disabled = !hasInput; if (decodeBtn) decodeBtn.disabled = !hasInput; if (normalizeBtn) normalizeBtn.disabled = !hasInput; if (swapBtn) swapBtn.disabled = !hasOutput; if (copyBtn) copyBtn.disabled = !hasOutput; if (downloadBtn) downloadBtn.disabled = !hasOutput; if (clearBtn) clearBtn.disabled = !hasInput && !hasOutput; } function parseDataUri(input) { const match = input.match(/^data:([^;,]+)?(?:;charset=[^;,]+)?(;base64)?,(.*)$/s); if (!match) return null; return { mime: match[1] || "text/plain", isBase64: !!match[2], payload: match[3] || "", }; } function normalizeBase64(input) { const trimmed = input.trim(); const dataUri = parseDataUri(trimmed); const source = dataUri && dataUri.isBase64 ? dataUri.payload : trimmed; let normalized = source.replace(/\s+/g, ""); normalized = normalized.replace(/-/g, "+").replace(/_/g, "/"); normalized = normalized.replace(/=+$/, ""); const mod = normalized.length % 4; if (mod) { normalized += "=".repeat(4 - mod); } return normalized; } function validateBase64Detailed(input) { const raw = input.trim(); if (!raw) return { ok: false, error: "Input is empty." }; const dataUri = parseDataUri(raw); const source = dataUri && dataUri.isBase64 ? dataUri.payload : raw; const value = source.replace(/\s+/g, ""); if (!value) return { ok: false, error: "Base64 payload is empty." }; const allowed = /^[A-Za-z0-9+/]+=*$/; if (!allowed.test(value)) { const invalidIndex = value.search(/[^A-Za-z0-9+/=\s]/); return { ok: false, error: `Invalid character at position ${invalidIndex + 1}. Allowed: A-Z, a-z, 0-9, +, /, =.`, }; } const firstPad = value.indexOf("="); if (firstPad !== -1 && /[^=]/.test(value.slice(firstPad))) { return { ok: false, error: "Padding '=' is only allowed at the end." }; } if (value.length % 4 === 1) { return { ok: false, error: "Invalid Base64 length." }; } return { ok: true }; } function looksLikeBase64(input) { const strict = validateBase64Detailed(input); return strict.ok; } async function encodeBytesToBase64Chunked(bytes) { const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; let output = ""; for (let i = 0; i < bytes.length; i += 3) { const a = bytes[i]; const b = i + 1 < bytes.length ? bytes[i + 1] : 0; const c = i + 2 < bytes.length ? bytes[i + 2] : 0; const triple = (a << 16) | (b << 8) | c; output += alphabet[(triple >> 18) & 63]; output += alphabet[(triple >> 12) & 63]; output += i + 1 < bytes.length ? alphabet[(triple >> 6) & 63] : "="; output += i + 2 < bytes.length ? alphabet[triple & 63] : "="; if (i % (CHUNK_BYTES * 3) === 0) await tick(); } return output; } async function encodeUtf8ToBase64Chunked(text) { const bytes = new TextEncoder().encode(text); return encodeBytesToBase64Chunked(bytes); } async function decodeBase64ToBytesChunked(base64) { const normalized = normalizeBase64(base64); const strict = validateBase64Detailed(normalized); if (!strict.ok) { throw new Error(strict.error); } const arrays = []; let total = 0; for (let i = 0; i < normalized.length; i += CHUNK_BASE64) { const chunk = normalized.slice(i, i + CHUNK_BASE64); const decoded = atob(chunk); const bytes = new Uint8Array(decoded.length); for (let j = 0; j < decoded.length; j++) { bytes[j] = decoded.charCodeAt(j); } arrays.push(bytes); total += bytes.length; if ((i / CHUNK_BASE64) % 8 === 0) await tick(); } const merged = new Uint8Array(total); let offset = 0; for (const arr of arrays) { merged.set(arr, offset); offset += arr.length; } return merged; } async function decodeBase64ToUtf8Chunked(base64) { const bytes = await decodeBase64ToBytesChunked(base64); return new TextDecoder().decode(bytes); } async function encodeAction() { const source = inputEl.value; if (!source.trim()) return; setStatus("Encoding...", false); outputEl.value = await encodeUtf8ToBase64Chunked(source); setStatus("Text encoded to Base64."); updateMeta(); updateButtonStates(); } async function decodeAction() { const source = inputEl.value; if (!source.trim()) return; setStatus("Decoding...", false); try { const bytes = await decodeBase64ToBytesChunked(source); const decodedText = new TextDecoder().decode(bytes); outputEl.value = decodedText; const dataUri = parseDataUri(source.trim()); if (dataUri?.mime && mimeInput && !mimeInput.value.trim()) { mimeInput.value = dataUri.mime; } setStatus("Base64 decoded successfully."); } catch (err) { outputEl.value = ""; setStatus(err.message || "Invalid Base64 input.", true); } updateMeta(); updateButtonStates(); } async function processAutoMode() { const source = inputEl.value; if (!source.trim()) return; if (looksLikeBase64(source) || /^data:.*;base64,/i.test(source.trim())) { await decodeAction(); } else { await encodeAction(); } } function handleInputChange() { if (!inputEl.value.trim()) { outputEl.value = ""; setStatus("Enter text or Base64 to get started."); } updateMeta(); updateButtonStates(); } async function handleFileUpload(file) { if (!file) return; const mime = file.type || "application/octet-stream"; if (mimeInput) mimeInput.value = mime; if (mime.startsWith("text/")) { const text = await file.text(); inputEl.value = text; outputEl.value = ""; setStatus(`Loaded text file into input: ${file.name}`); updateMeta(); updateButtonStates(); return; } const arrayBuffer = await file.arrayBuffer(); const bytes = new Uint8Array(arrayBuffer); inputEl.value = await encodeBytesToBase64Chunked(bytes); outputEl.value = ""; setStatus(`Loaded Base64 into input from file: ${file.name}`); updateMeta(); updateButtonStates(); } async function downloadDecodedFile() { if (!outputEl.value.trim()) return; try { const bytes = await decodeBase64ToBytesChunked(outputEl.value); const mime = mimeInput?.value.trim() || "application/octet-stream"; const blob = new Blob([bytes], { type: mime }); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = `decoded.${mime.split("/")[1] || "bin"}`; link.click(); URL.revokeObjectURL(link.href); setStatus("Decoded file downloaded."); } catch (err) { setStatus(err.message || "Output is not valid Base64 for download.", true); } } inputEl.addEventListener("input", handleInputChange); encodeBtn && encodeBtn.addEventListener("click", async () => { if (autoModeCheckbox?.checked) { await processAutoMode(); return; } await encodeAction(); }); decodeBtn && decodeBtn.addEventListener("click", async () => { if (autoModeCheckbox?.checked) { await processAutoMode(); return; } await decodeAction(); }); swapBtn && swapBtn.addEventListener("click", () => { if (!outputEl.value.trim()) return; inputEl.value = outputEl.value; outputEl.value = ""; setStatus("Moved output to input."); updateMeta(); updateButtonStates(); }); copyBtn && copyBtn.addEventListener("click", async () => { if (!outputEl.value.trim()) return; await navigator.clipboard.writeText(outputEl.value); const original = copyBtn.textContent; copyBtn.textContent = "Copied"; setStatus("Output copied."); setTimeout(() => { copyBtn.textContent = original; }, 1200); }); downloadBtn && downloadBtn.addEventListener("click", downloadDecodedFile); clearBtn && clearBtn.addEventListener("click", () => { inputEl.value = ""; outputEl.value = ""; if (mimeInput) mimeInput.value = ""; setStatus("Enter text or Base64 to get started."); updateMeta(); updateButtonStates(); }); fileInput && fileInput.addEventListener("change", async () => { const file = fileInput.files && fileInput.files[0]; await handleFileUpload(file); fileInput.value = ""; }); updateMeta(); updateButtonStates(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initBase64Tool); } else { initBase64Tool(); }