From 67c3f8f24f3f18a63e830e22e6958e852c3c80e3 Mon Sep 17 00:00:00 2001 From: bey0ne <195894611+bey0ne@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:20:33 +0000 Subject: [PATCH] Image Prep: add a Custom size profile (#49) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tags missing from the profile table (type 1386 in #49, 1358 in #36) had no way to get a BMP of their real size. The profile list now always ends with "Custom size…": width and height (8-800 px, like the Flipper's Custom Size screen) and Mono / Red / Yellow. The values are remembered in localStorage and out-of-range sizes are clamped when the field is left. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QYkmJ3RnCgGZEBKnfRJt5x --- README.md | 2 +- web-image-prep/index.html | 58 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 76ea17d..3921f5e 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Only test tags you own or are allowed to test. ## Features - **TagTinker Flipper App:** High-performance RLE streaming IR engine. -- **TagTinker Image Prep (web):** Single-file, dependency-free HTML page that lists every supported tag profile, runs a full image pipeline (tone, contrast, detail, sharpen, dither, photo-grade Oklab 3-colour quantisation) and exports a Flipper-ready BMP. Hosted at **[i12bp8.github.io/TagTinker](https://i12bp8.github.io/TagTinker/)** (source: `web-image-prep/`). +- **TagTinker Image Prep (web):** Single-file, dependency-free HTML page that lists every supported tag profile (plus a **Custom size** entry for tags that are not listed), runs a full image pipeline (tone, contrast, detail, sharpen, dither, photo-grade Oklab 3-colour quantisation) and exports a Flipper-ready BMP. Hosted at **[i12bp8.github.io/TagTinker](https://i12bp8.github.io/TagTinker/)** (source: `web-image-prep/`). - **Drop-folder image flow:** Drop a prepared BMP into `apps_data/tagtinker/dropped/` on the Flipper SD card, then open `Targeted Payloads → → Set Image` and pick it. The Flipper rescales the 1-bit and two-plane BMPs that the image preparer exports, so one file can be sent to graphics tags of other sizes and to any page. Type 1626 (SmartTAG Color 2.6) targets only accept files up to 24 KB. - **NFC Tag Scan:** Add a target by scanning the tag's NFC chip instead of typing its barcode. This works when the tag's NFC data carries an ID TagTinker can decode; otherwise use `+ Type Barcode`. - **WiFi Plugins (optional):** Plug a Flipper WiFi Dev Board (ESP32-S2) into the GPIO header to unlock live, network-rendered tag designs — crypto price cards, weather tiles, identicons, and more — auto-discovered by the FAP. New plugins live entirely on the cloud worker; the Flipper firmware never has to be re-flashed to add one. The dev board firmware talks to the worker named by `CONFIG_TT_CLOUD_URL`, so you can point it at your own deployment of `cloud-plugins/` instead. diff --git a/web-image-prep/index.html b/web-image-prep/index.html index 269c278..7586c4d 100644 --- a/web-image-prep/index.html +++ b/web-image-prep/index.html @@ -268,6 +268,20 @@

1 · Source

2 · Tag profile

+
Resolution—
Color mode—
@@ -415,13 +429,49 @@

Drop · Pick · Send

opt.textContent = `${p.code} · ${p.w}×${p.h} · ${p.color.toUpperCase()} · ${p.name}`; profileSel.appendChild(opt); }); + /* Always offered, whatever the search: tags missing from the table + * (e.g. issue #49) still need a BMP of the right size. */ + const custom = document.createElement("option"); + custom.value = "custom"; + custom.textContent = "Custom size… · any W×H · MONO / RED / YELLOW"; + profileSel.appendChild(custom); if (profileSel.options.length && !profileSel.value) profileSel.selectedIndex = 0; applyProfile(); } -const currentProfile = () => PROFILES[parseInt(profileSel.value, 10)] ?? PROFILES[0]; + +/* Same limits as the Flipper's Custom Size screen (TAGTINKER_CUSTOM_SIZE_MIN/MAX). */ +const CUSTOM_MIN = 8, CUSTOM_MAX = 800; +const CUSTOM_KEY = "tagtinker.customProfile"; +function clampSize(v, fallback) { + const n = Math.round(Number(v)); + if (!Number.isFinite(n)) return fallback; + return Math.max(CUSTOM_MIN, Math.min(CUSTOM_MAX, n)); +} +function customProfile() { + return { + code: "custom", + w: clampSize($("c-w").value, 296), + h: clampSize($("c-h").value, 128), + color: ["mono", "red", "yellow"].includes($("c-color").value) ? $("c-color").value : "mono", + name: "Custom", + }; +} +function saveCustom() { + try { localStorage.setItem(CUSTOM_KEY, JSON.stringify(customProfile())); } catch {} +} +function loadCustom() { + try { + const c = JSON.parse(localStorage.getItem(CUSTOM_KEY) || "null"); + if (c) { $("c-w").value = c.w; $("c-h").value = c.h; $("c-color").value = c.color; } + } catch {} +} +const currentProfile = () => profileSel.value === "custom" + ? customProfile() + : (PROFILES[parseInt(profileSel.value, 10)] ?? PROFILES[0]); function applyProfile() { const p = currentProfile(); + $("custom").hidden = p.code !== "custom"; $("pf-size").textContent = `${p.w} × ${p.h}`; $("pf-color").textContent = p.color.toUpperCase(); $("pf-type").textContent = p.code; @@ -435,6 +485,11 @@

Drop · Pick · Send

} filter.addEventListener("input", rebuildProfileList); profileSel.addEventListener("change", applyProfile); +["c-w", "c-h", "c-color"].forEach(id => $(id).addEventListener("input", () => { saveCustom(); applyProfile(); })); +/* Snap out-of-range sizes back into the field once the user leaves it. */ +["c-w", "c-h"].forEach(id => $(id).addEventListener("change", () => { + $(id).value = clampSize($(id).value, id === "c-w" ? 296 : 128); saveCustom(); applyProfile(); +})); /* ─── Bindings ────────────────────────────────────────────────────── */ function bindRange(id, key, fmt = v => v) { @@ -907,6 +962,7 @@

Drop · Pick · Send

}); /* ─── Boot ────────────────────────────────────────────────────────── */ +loadCustom(); rebuildProfileList(); window.addEventListener("resize", scheduleRender);