Skip to content

Instantly share code, notes, and snippets.

@j6k4m8
Last active March 4, 2026 05:23
Show Gist options
  • Select an option

  • Save j6k4m8/c831e496f3223ea9890ee8d188cd1390 to your computer and use it in GitHub Desktop.

Select an option

Save j6k4m8/c831e496f3223ea9890ee8d188cd1390 to your computer and use it in GitHub Desktop.
"""BMP-only image-to-base64 art encoder."""
# https://blog.jordan.matelsky.com/bb64/
from __future__ import annotations
import base64
import math
import struct
import subprocess
import textwrap
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable
import numpy as np
BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
# Dark -> light order measured from rendered glyph luminance.
DARK_TO_LIGHT_BASE64 = "N0BMW8QRDgOH96KUqGEdpbAmPSa5X4Z3hkeV2woFIyCnu1JTYsftj7xLzvcil+/r"
_B64_VALUE = {ch: idx for idx, ch in enumerate(BASE64_ALPHABET)}
BMP_PIXEL_OFFSET = 54
BMP_HEADER_B64_CHARS = 72 # 54 bytes -> 72 base64 chars.
@dataclass(frozen=True)
class EncodeResult:
"""Result of one BMP base64-art encoding run.
Attributes:
data_uri: Full `data:image/bmp;base64,...` string.
base64_text: Raw base64 payload without wrapping.
wrapped_base64: Base64 payload wrapped to `line_length`.
art_block: Extracted `art_rows x art_cols` block from base64 payload.
width: Encoded BMP width in pixels.
height: Encoded BMP height in pixels.
art_cols: Base64-art block columns.
art_rows: Base64-art block rows.
line_length: Wrap width used for `wrapped_base64`.
skip_chars: Base64 offset where `art_block` starts.
mutable_char_run: Number of consecutively mutable chars from `skip_chars`.
"""
data_uri: str
base64_text: str
wrapped_base64: str
art_block: str
width: int
height: int
art_cols: int
art_rows: int
line_length: int
skip_chars: int
mutable_char_run: int
def encode_image_to_bmp_art_base64(
image_path: str | Path,
width: int = 64,
height: int = 64,
*,
art_cols: int | None = None,
art_rows: int | None = None,
line_length: int | None = None,
skip_chars: int | None = None,
passes: int = 2,
neighbor_radius: int = 3,
char_weight: float = 1.0,
pixel_weight: float = 0.0,
thumbnail_size: int | None = None,
char_order: str = DARK_TO_LIGHT_BASE64,
) -> EncodeResult:
"""Encode an image so a base64 window resembles source luminance.
Args:
image_path: Input image path readable by `ffmpeg`.
width: Output BMP width in pixels.
height: Output BMP height in pixels.
art_cols: Base64-art columns. If omitted, inferred from mutable run.
art_rows: Base64-art rows. If omitted, inferred from mutable run.
line_length: Wrap width for `wrapped_base64`. Defaults to `art_cols`.
skip_chars: Base64 offset where art starts. Defaults to 72.
passes: Greedy optimization passes over the art window.
neighbor_radius: Rank search radius for candidate characters.
char_weight: Weight for character-rank mismatch.
pixel_weight: Weight for squared byte drift. Use 0.0 for fully lossy fitting.
thumbnail_size: Optional thumbnail edge length in art cells. If set,
inserts a full-image thumbnail into the top-left `N x N` region
of the target base64-art map before optimization.
char_order: 64-char dark-to-light permutation over base64 alphabet.
Returns:
EncodeResult with data URI and text artifacts.
Raises:
RuntimeError: If ffmpeg cannot decode the input.
ValueError: If settings or derived geometry are invalid.
"""
_validate_inputs(
width=width,
height=height,
passes=passes,
neighbor_radius=neighbor_radius,
thumbnail_size=thumbnail_size,
char_order=char_order,
)
src_rgb = _read_raw_frame(image_path, width, height, pix_fmt="rgb24").reshape(height, width, 3)
blob, pixel_offset, row_stride = _rgb_to_bmp(src_rgb)
# Keep geometry simple: disallow BMP row padding.
if row_stride != width * 3:
raise ValueError("width must be divisible by 4 in BMP mode (no row padding)")
mutable_bytes = np.zeros(blob.size, dtype=bool)
mutable_bytes[pixel_offset:] = True
char_mutable = _char_mutability_mask(blob.size, mutable_bytes)
skip_chars = BMP_HEADER_B64_CHARS if skip_chars is None else skip_chars
if skip_chars < 0:
raise ValueError("skip_chars must be >= 0")
if skip_chars >= char_mutable.size:
raise ValueError("skip_chars is past end of base64 payload")
run_len = _run_length_from(char_mutable, skip_chars)
if run_len <= 0:
raise ValueError("skip_chars does not start in a mutable char run")
if art_cols is None and art_rows is None:
art_cols, art_rows = _best_factor_pair(run_len)
elif art_cols is None or art_rows is None:
raise ValueError("set both art_cols and art_rows, or neither")
assert art_cols is not None and art_rows is not None
if art_cols <= 0 or art_rows <= 0:
raise ValueError("art_cols and art_rows must be > 0")
needed = art_cols * art_rows
if needed > run_len:
raise ValueError(
f"requested art block ({needed} chars) exceeds mutable run ({run_len} chars)"
)
target_gray = _read_raw_frame(image_path, art_cols, art_rows, pix_fmt="gray").reshape(
art_rows, art_cols
).copy()
if thumbnail_size is not None:
if thumbnail_size > art_cols or thumbnail_size > art_rows:
raise ValueError("thumbnail_size exceeds art block dimensions")
thumb_gray = _read_raw_frame(
image_path, thumbnail_size, thumbnail_size, pix_fmt="gray"
).reshape(thumbnail_size, thumbnail_size)
target_gray[:thumbnail_size, :thumbnail_size] = thumb_gray
target_rank = np.rint(
(target_gray.astype(np.float32) / 255.0) * (len(char_order) - 1)
).astype(np.int16).reshape(-1)
_optimize_window_masked(
blob=blob,
mutable=mutable_bytes,
target_rank=target_rank,
skip_chars=skip_chars,
passes=passes,
neighbor_radius=neighbor_radius,
char_weight=char_weight,
pixel_weight=pixel_weight,
char_order=char_order,
)
b64 = base64.b64encode(blob.tobytes()).decode("ascii")
if skip_chars + needed > len(b64):
raise ValueError("requested art window exceeds base64 payload")
line_length = art_cols if line_length is None else line_length
if line_length <= 0:
raise ValueError("line_length must be > 0")
wrapped = "\n".join(textwrap.wrap(b64, line_length))
art = _extract_art_block(b64, cols=art_cols, rows=art_rows, skip_chars=skip_chars)
return EncodeResult(
data_uri=f"data:image/bmp;base64,{b64}",
base64_text=b64,
wrapped_base64=wrapped,
art_block=art,
width=width,
height=height,
art_cols=art_cols,
art_rows=art_rows,
line_length=line_length,
skip_chars=skip_chars,
mutable_char_run=run_len,
)
def _validate_inputs(
*,
width: int,
height: int,
passes: int,
neighbor_radius: int,
thumbnail_size: int | None,
char_order: str,
) -> None:
"""Validate shared encoder settings."""
if width <= 0 or height <= 0:
raise ValueError("width and height must be > 0")
if passes <= 0:
raise ValueError("passes must be > 0")
if neighbor_radius < 0:
raise ValueError("neighbor_radius must be >= 0")
if thumbnail_size is not None and thumbnail_size <= 0:
raise ValueError("thumbnail_size must be > 0 when set")
if len(char_order) != 64 or set(char_order) != set(BASE64_ALPHABET):
raise ValueError("char_order must be a permutation of the 64 base64 chars")
def _read_raw_frame(
image_path: str | Path, width: int, height: int, *, pix_fmt: str
) -> np.ndarray:
"""Load and resize one frame from an image via ffmpeg."""
channels = 1 if pix_fmt == "gray" else 3
cmd = [
"ffmpeg",
"-v",
"error",
"-i",
str(image_path),
"-vf",
f"scale={width}:{height}:flags=lanczos,format={pix_fmt}",
"-f",
"rawvideo",
"-pix_fmt",
pix_fmt,
"-",
]
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)
if proc.returncode != 0:
raise RuntimeError(
"ffmpeg failed while reading image: "
f"{proc.stderr.decode('utf-8', errors='replace').strip()}"
)
arr = np.frombuffer(proc.stdout, dtype=np.uint8)
expected = width * height * channels
if arr.size != expected:
raise RuntimeError(f"ffmpeg produced {arr.size} bytes, expected {expected}")
return arr
def _rgb_to_bmp(rgb: np.ndarray) -> tuple[np.ndarray, int, int]:
"""Convert RGB uint8 image to contiguous 24-bit BMP bytes."""
height, width, _ = rgb.shape
row_bytes = width * 3
row_stride = (row_bytes + 3) & ~3
bgr_rows = rgb[::-1, :, ::-1].reshape(height, row_bytes)
pixel = np.zeros((height, row_stride), dtype=np.uint8)
pixel[:, :row_bytes] = bgr_rows
pixel_blob = pixel.reshape(-1)
file_size = BMP_PIXEL_OFFSET + pixel_blob.size
header = struct.pack("<2sIHHI", b"BM", file_size, 0, 0, BMP_PIXEL_OFFSET)
dib = struct.pack(
"<IIIHHIIIIII",
40,
width,
height,
1,
24,
0,
pixel_blob.size,
2835,
2835,
0,
0,
)
blob = np.concatenate((np.frombuffer(header + dib, dtype=np.uint8), pixel_blob))
return blob, BMP_PIXEL_OFFSET, row_stride
def _char_mutability_mask(blob_size: int, mutable_bytes: np.ndarray) -> np.ndarray:
"""Build bool mask for base64 chars fully controlled by mutable bytes."""
if mutable_bytes.size != blob_size:
raise ValueError("mutable byte mask has wrong size")
max_chars = 4 * ((blob_size + 2) // 3)
out = np.zeros(max_chars, dtype=bool)
for pos in range(max_chars):
idxs = _affected_byte_indices(pos, blob_size)
if idxs and all(mutable_bytes[i] for i in idxs):
out[pos] = True
return out
def _affected_byte_indices(pos: int, blob_size: int) -> tuple[int, ...]:
"""Return affected byte indices for one base64 character position."""
group = pos // 4
slot = pos % 4
b0 = group * 3
b1 = b0 + 1
b2 = b0 + 2
if b0 >= blob_size:
return ()
if slot == 0:
return (b0,)
if slot == 1:
return (b0, b1) if b1 < blob_size else ()
if slot == 2:
return (b1, b2) if b2 < blob_size else ()
return (b2,) if b2 < blob_size else ()
def _run_length_from(mask: np.ndarray, start: int) -> int:
"""Count contiguous True values starting at index `start`."""
n = mask.size
i = start
while i < n and bool(mask[i]):
i += 1
return i - start
def _best_factor_pair(n: int) -> tuple[int, int]:
"""Pick a near-square factor pair that uses all chars in `n`."""
if n <= 0:
raise ValueError("n must be > 0")
root = int(math.isqrt(n))
for a in range(root, 0, -1):
if n % a == 0:
b = n // a
return b, a
return n, 1
def _optimize_window_masked(
*,
blob: np.ndarray,
mutable: np.ndarray,
target_rank: np.ndarray,
skip_chars: int,
passes: int,
neighbor_radius: int,
char_weight: float,
pixel_weight: float,
char_order: str,
) -> None:
"""Greedy search to align base64 character ranks with target luminance."""
original = blob.copy()
target_len = int(target_rank.size)
for _ in range(passes):
for i in range(target_len):
pos = skip_chars + i
t_rank = int(target_rank[i])
best_score = float("inf")
best_changes: list[tuple[int, int]] = []
for rank in _candidate_ranks(t_rank, neighbor_radius, len(char_order)):
ch = char_order[rank]
changes = _propose_char_mutation(blob, pos, _B64_VALUE[ch])
if not changes or any(not mutable[idx] for idx, _ in changes):
continue
char_cost = abs(rank - t_rank)
score = char_weight * char_cost
if pixel_weight > 0.0:
byte_cost = 0.0
for idx, new_val in changes:
d = float(new_val) - float(original[idx])
byte_cost += d * d
score += pixel_weight * byte_cost
if score < best_score:
best_score = score
best_changes = changes
if score == 0.0:
break
for idx, new_val in best_changes:
blob[idx] = new_val
def _candidate_ranks(center: int, radius: int, n: int) -> Iterable[int]:
"""Yield candidate rank indices around a center rank."""
yield center
for step in range(1, radius + 1):
if center - step >= 0:
yield center - step
if center + step < n:
yield center + step
def _propose_char_mutation(
blob: np.ndarray, pos: int, value: int
) -> list[tuple[int, int]]:
"""Return byte edits needed to force one base64 sextet value."""
group = pos // 4
slot = pos % 4
b0 = group * 3
b1 = b0 + 1
b2 = b0 + 2
if b0 >= blob.size:
return []
changes: dict[int, int] = {}
if slot == 0:
changes[b0] = (value << 2) | (int(blob[b0]) & 0x03)
elif slot == 1:
if b1 >= blob.size:
return []
changes[b0] = (int(blob[b0]) & 0xFC) | ((value >> 4) & 0x03)
changes[b1] = (int(blob[b1]) & 0x0F) | ((value & 0x0F) << 4)
elif slot == 2:
if b2 >= blob.size:
return []
changes[b1] = (int(blob[b1]) & 0xF0) | ((value >> 2) & 0x0F)
changes[b2] = (int(blob[b2]) & 0x3F) | ((value & 0x03) << 6)
else:
if b2 >= blob.size:
return []
changes[b2] = (int(blob[b2]) & 0xC0) | (value & 0x3F)
return [(idx, new_val) for idx, new_val in changes.items()]
def _extract_art_block(base64_text: str, *, cols: int, rows: int, skip_chars: int) -> str:
"""Extract and render a fixed-size block from base64 text."""
needed = cols * rows
chunk = base64_text[skip_chars : skip_chars + needed]
if len(chunk) < needed:
raise ValueError("not enough base64 chars for requested block")
return "\n".join(chunk[i : i + cols] for i in range(0, needed, cols))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment