mirror of
https://github.com/i12bp8/ESLPwn.git
synced 2026-07-03 04:41:50 +00:00
287481453e
Worker (cloud-plugins/): - New plugin 'GitHub Profile': avatar dithered upper-left, display name + @handle (with accent star) + word-wrapped bio in the right column, followers/repos line, and a 53x7 contribution heatmap with an accent palette (red dots for everyday activity, black for the rare hottest days). Tuned for 208x112 ESLs and adapts to 296x128 with a bigger avatar and roomier heatmap. - Identicon plugin gains DiceBear style selector (pixel-art / bottts / lorelei / adventurer / micah / fun-emoji / shapes) on top of the original symmetric grid 'local' mode. Both are rendered through the new shared image_util pipeline. - Crypto plugin redesigned: editorial layout with corner brackets, bold price headline, white-knockout delta badge with a real triangle glyph, currency symbol prefix using the canvas font's new euro / pound / yen glyphs, and a stippled accent fill under the sparkline. Canvas: - setPixel is now last-write-wins across planes, so drawing black on top of an accent-filled area produces a clean black pixel instead of the muddy plane-conflict the e-paper driver was showing previously. Every pixel is therefore exactly one of white / black / accent which is what plugin authors intuit. - New whitePixel + drawTextWhite primitives for knockout text (e.g. white labels punched through a coloured badge). - FONT_EXTRA shipped with the canvas for currency / arrow / shape / star / bracket glyphs. Plugins can now write '\u20AC1234' and have the euro symbol Just Work. Image pipeline (new image_util.ts): - One fetchImageGray() that sniffs PNG vs JPEG, decodes via upng-js / jpeg-js, composites alpha over white, and returns a tightly-packed grayscale buffer. - One blitGrayDither() that nearest-neighbour scales the gray buffer to a destination rect and serpentine Floyd-Steinberg dithers it on the way in. Used by identicon and github. FAP: - WiFi BMP writer is now plane-aware: writes the same biPlanes=1, biBitCount=2, 3-entry palette layout the rest of the IR TX pipeline already understood for web-image-prep BMPs. Tri-colour tags now actually receive accent ink from cloud-rendered plugins. - WiFi run scene un-forces accent: plugins are invoked with the tag's actual accent capability and the BMP is opened with the matching plane count. - Run scene now calls bmp_abort on exit so an aborted transfer doesn't leak the ~10 KB pixel buffer. - Plugin manifest cache capped at TT_WIFI_MAX_FAP_PLUGINS=8 (down from 16) and the UART RX stream buffer shrunk to 8 KB. Together these cut WiFi-flow heap pressure by ~23 KB and fix the OOM crashes that occasionally happened when opening WiFi Plugins. - Broadcast scene auto-transmit fix: enter_callback now filters by the >> Transmit << row index, and the cursor defaults to row 0 instead of Transmit. Previously a stale OK key-release from the prior submenu would land on the Transmit row and blast the radio without any user action.
114 lines
4.3 KiB
TypeScript
114 lines
4.3 KiB
TypeScript
/*
|
|
* Shared image-fetch / decode / dither helpers used by every plugin
|
|
* that wants to render a downloaded raster onto the e-paper canvas.
|
|
*
|
|
* - fetchImageGray(url): downloads, sniffs PNG vs JPEG, decodes, then
|
|
* composites alpha over white and converts to perceptual luma. The
|
|
* result is always a tightly packed Uint8Array of grayscale 0..255.
|
|
* - blitGrayDither(canvas, ...): nearest-neighbour scales a grayscale
|
|
* buffer into a destination rect on a Canvas and serpentine
|
|
* Floyd-Steinberg dithers it to monochrome ink in the process.
|
|
*
|
|
* Keeping these in one file means the worker bundle ships UPNG / jpeg-js
|
|
* once, not once per plugin, and any pixel-level improvements (e.g.
|
|
* a different dither) propagate everywhere automatically.
|
|
*/
|
|
|
|
import * as UPNG from "upng-js";
|
|
// jpeg-js is CommonJS without proper types, so import as any.
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
import jpegJs from "jpeg-js";
|
|
import { Canvas, Ink } from "./canvas";
|
|
|
|
export interface GrayImage { gray: Uint8Array; w: number; h: number; }
|
|
|
|
/** Fetch a remote image and reduce it to a grayscale luma buffer. PNG
|
|
* and JPEG are both supported - we sniff the magic bytes rather than
|
|
* trusting the URL extension or the Content-Type header (which CDNs
|
|
* frequently get wrong). */
|
|
export async function fetchImageGray(url: string): Promise<GrayImage | null> {
|
|
let raw: ArrayBuffer;
|
|
try {
|
|
const r = await fetch(url, { headers: { "User-Agent": "TagTinker/1.0" } });
|
|
if (!r.ok) return null;
|
|
raw = await r.arrayBuffer();
|
|
} catch {
|
|
return null;
|
|
}
|
|
return decodeImageGray(raw);
|
|
}
|
|
|
|
/** Decode a buffer into grayscale, sniffing PNG vs JPEG on first bytes. */
|
|
export function decodeImageGray(buf: ArrayBuffer): GrayImage | null {
|
|
const u8 = new Uint8Array(buf);
|
|
if (u8.length < 4) return null;
|
|
let rgba: Uint8Array;
|
|
let w: number, h: number;
|
|
/* PNG magic: 89 50 4E 47 */
|
|
if (u8[0] === 0x89 && u8[1] === 0x50 && u8[2] === 0x4E && u8[3] === 0x47) {
|
|
let dec;
|
|
try { dec = UPNG.decode(buf); } catch { return null; }
|
|
const arr = UPNG.toRGBA8(dec);
|
|
if (!arr.length) return null;
|
|
rgba = new Uint8Array(arr[0]);
|
|
w = dec.width; h = dec.height;
|
|
/* JPEG magic: FF D8 FF */
|
|
} else if (u8[0] === 0xFF && u8[1] === 0xD8 && u8[2] === 0xFF) {
|
|
let dec;
|
|
try { dec = jpegJs.decode(u8, { useTArray: true }); } catch { return null; }
|
|
rgba = dec.data as Uint8Array;
|
|
w = dec.width; h = dec.height;
|
|
} else {
|
|
return null;
|
|
}
|
|
|
|
const gray = new Uint8Array(w * h);
|
|
for (let i = 0, j = 0; i < gray.length; i++, j += 4) {
|
|
const a = rgba[j + 3] / 255;
|
|
const r = rgba[j] * a + 255 * (1 - a);
|
|
const g = rgba[j + 1] * a + 255 * (1 - a);
|
|
const b = rgba[j + 2] * a + 255 * (1 - a);
|
|
gray[i] = (0.2126 * r + 0.7152 * g + 0.0722 * b) | 0;
|
|
}
|
|
return { gray, w, h };
|
|
}
|
|
|
|
/** Serpentine Floyd-Steinberg dither into a destination rect. The
|
|
* source is nearest-neighbour scaled into a working float buffer so
|
|
* the dither runs at output resolution (where artefacts actually
|
|
* matter to the user). */
|
|
export function blitGrayDither(
|
|
c: Canvas, dx: number, dy: number, dstW: number, dstH: number,
|
|
img: GrayImage, ink: Ink = 0,
|
|
): void {
|
|
const { gray, w: srcW, h: srcH } = img;
|
|
const buf = new Float32Array(dstW * dstH);
|
|
for (let y = 0; y < dstH; y++) {
|
|
const sy = Math.min(srcH - 1, Math.floor((y * srcH) / dstH));
|
|
for (let x = 0; x < dstW; x++) {
|
|
const sx = Math.min(srcW - 1, Math.floor((x * srcW) / dstW));
|
|
buf[y * dstW + x] = gray[sy * srcW + sx];
|
|
}
|
|
}
|
|
for (let y = 0; y < dstH; y++) {
|
|
const rev = (y & 1) === 1;
|
|
const xStart = rev ? dstW - 1 : 0;
|
|
const xEnd = rev ? -1 : dstW;
|
|
const xStep = rev ? -1 : 1;
|
|
for (let x = xStart; x !== xEnd; x += xStep) {
|
|
const i = y * dstW + x;
|
|
const old = buf[i];
|
|
const np = old < 128 ? 0 : 255;
|
|
if (np === 0) c.setPixel(dx + x, dy + y, ink);
|
|
const err = old - np;
|
|
const sx = rev ? -1 : 1;
|
|
if (x + sx >= 0 && x + sx < dstW) buf[i + sx] += err * 7 / 16;
|
|
if (y + 1 < dstH) {
|
|
if (x - sx >= 0 && x - sx < dstW) buf[i + dstW - sx] += err * 3 / 16;
|
|
buf[i + dstW] += err * 5 / 16;
|
|
if (x + sx >= 0 && x + sx < dstW) buf[i + dstW + sx] += err * 1 / 16;
|
|
}
|
|
}
|
|
}
|
|
}
|