From dbba248d1013089140b4ff244a5ed435772970f8 Mon Sep 17 00:00:00 2001 From: Fabricio Silva Date: Thu, 12 Feb 2026 00:14:59 +0000 Subject: [PATCH] add support for binaries, renders image --- src/views/home.html | 4 ++-- static/home.mjs | 10 ++++++---- static/styles.css | 36 +++++++++++++++++++++++++++++------ static/view.mjs | 46 +++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 82 insertions(+), 14 deletions(-) diff --git a/src/views/home.html b/src/views/home.html index 5615503..a50d7cc 100644 --- a/src/views/home.html +++ b/src/views/home.html @@ -8,11 +8,11 @@ SYNOPSIS you can also paste or drag files to this page DESCRIPTION - works better with text + works better with text and images limited to 20MiB everything are encrypted decryption only on browser - no data or logs are collected + no data or logs never collected EXAMPLES $ curl <%- BASE_URL %> --data-binary @debug.log diff --git a/static/home.mjs b/static/home.mjs index 428c753..d26c82c 100644 --- a/static/home.mjs +++ b/static/home.mjs @@ -34,12 +34,14 @@ const create = async (data) => { codeElement.classList.add("loading"); try { + const body = + data instanceof File + ? await data.arrayBuffer() + : new TextEncoder().encode(data); + const response = await fetch("/", { method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: data, + body, }); const url = await response.text(); diff --git a/static/styles.css b/static/styles.css index 7f84811..d77134b 100644 --- a/static/styles.css +++ b/static/styles.css @@ -29,7 +29,9 @@ body, code, pre, samp { - font: 100 0.8125rem/1.25rem "JetBrains Mono", monospace; + font: + 100 0.8125rem/1.25rem "JetBrains Mono", + monospace; } code, @@ -44,6 +46,14 @@ pre { position: relative; } +pre.center { + display: grid; + place-content: center; + min-height: 100vh; + min-height: 100dvh; + padding: 1em; +} + samp { display: inline-block; white-space: pre-line; @@ -55,17 +65,31 @@ a { text-decoration: none; } -img, -video { +img { display: block; margin: auto; width: 100%; max-width: fit-content; + object-fit: contain; + max-height: calc(100vh - 2em); + max-height: calc(100dvh - 2em); +} + +/* Image zoom on click */ +pre.can-zoom img { + cursor: zoom-in; +} + +pre.zoomed img { + cursor: zoom-out; + width: auto; + max-width: none; + max-height: none; } -video { - max-height: 100vh; - max-height: 100dvh; +pre.zoomed { + overflow: auto; + place-content: safe center; } /* noscript hides loading */ diff --git a/static/view.mjs b/static/view.mjs index 7c8c77f..8d93a3a 100644 --- a/static/view.mjs +++ b/static/view.mjs @@ -61,6 +61,24 @@ const selectLines = (preElement) => { setTimeout(() => markElement.scrollIntoView(), 200); }; +const detectContentType = (buffer) => { + // Magic bytes for common image formats (https://en.wikipedia.org/wiki/Magic_number_(programming)) + const signatures = [ + { mime: "image/png", bytes: [0x89, 0x50, 0x4e, 0x47] }, + { mime: "image/jpeg", bytes: [0xff, 0xd8, 0xff] }, + { mime: "image/gif", bytes: [0x47, 0x49, 0x46, 0x38] }, + { mime: "image/webp", bytes: [0x52, 0x49, 0x46, 0x46] }, + ]; + + const view = new Uint8Array(buffer); + for (const { mime, bytes } of signatures) { + if (view.length < bytes.length) continue; + if (bytes.every((b, i) => view[i] === b)) return mime; + } + + return "text/plain"; +}; + addEventListener("DOMContentLoaded", async () => { // delete noscript tag document.querySelector("noscript").remove(); @@ -78,8 +96,29 @@ addEventListener("DOMContentLoaded", async () => { const iv = new Uint8Array(response.headers.get("X-IV")?.split(",")); const cipherText = await response.arrayBuffer(); const buffer = await decrypt(iv, k, cipherText); - - childElement.textContent = new TextDecoder().decode(buffer); + const type = detectContentType(buffer); + + if (type.startsWith("image/")) { + preElement.classList.add("center"); + const blob = new Blob([buffer], { type }); + const img = document.createElement("img"); + img.src = URL.createObjectURL(blob); + img.addEventListener("load", () => { + // Check if image is larger than its displayed size (needs zoom) + if ( + img.naturalWidth > img.clientWidth || + img.naturalHeight > img.clientHeight + ) { + preElement.classList.add("can-zoom"); + img.addEventListener("click", () => + preElement.classList.toggle("zoomed"), + ); + } + }); + childElement = img; + } else { + childElement.textContent = new TextDecoder().decode(buffer); + } } catch (err) { console.error(err); const mappedMessages = new Map([[404, "not found 🙈"]]); @@ -91,6 +130,9 @@ addEventListener("DOMContentLoaded", async () => { document.body.appendChild(preElement); } + // Skip highlighting for images + if (childElement.tagName === "IMG") return; + // force highlight.js instead of detect (default) if (ext) { hljs.configure({ languages: [ext] }); -- 2.51.2