Image_ansi_colors: dithering to the 256-colour terminal palette, for a terminal without true colour (from dev 1ce461b4)
This commit is contained in:
1 parent
62f08401aa
commit
6e9889f7ae
3 files changed
+71
-1
No files matched your search
@@ -122,6 +122,15 @@ class Image(klammer_base.Klammer_base):
|
||||
Image_quality, whose shifted colours and edge ringing are invisible
|
||||
on a page and plain to see in eighty characters. A PDF source has
|
||||
to go through the cache's rasterising."""
|
||||
samples = self.txt_samples_raw(width, height)
|
||||
colors = str(getattr(self, "Image_ansi_colors", "true")).strip().lower()
|
||||
if colors not in ("true", "256"):
|
||||
raise Exception(f'Image_ansi_colors "{colors}" is not one of true, 256')
|
||||
if colors == "256":
|
||||
samples = Image.txt_palette_dither(samples)
|
||||
return samples
|
||||
|
||||
def txt_samples_raw(self, width, height):
|
||||
import OpenImageIO as oiio
|
||||
source = self.cache.find_file(self.cache_target, self.basename) or self.source
|
||||
if os.path.splitext(source)[1][1:].lower() in ("pdf", "eps"):
|
||||
@@ -141,6 +150,48 @@ class Image(klammer_base.Klammer_base):
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
# The xterm 256-colour palette (the 6x6x6 cube and the 24 greys; the 16
|
||||
# system colours are left out, a terminal redefines them), for a terminal
|
||||
# that shows no 24-bit colour and maps each colour to its nearest entry
|
||||
# (Apple's Terminal, 2026-10-01): dithering the picture to the palette
|
||||
# FIRST spreads each cell's error over its neighbours instead of letting
|
||||
# the terminal round whole runs of a sky to one entry.
|
||||
TXT_LEVELS = (0, 95, 135, 175, 215, 255) # the cube's axis; the greys are 8, 18, ... 238
|
||||
|
||||
@staticmethod
|
||||
def txt_nearest_palette(rgb):
|
||||
r, g, b = rgb
|
||||
cube = tuple(min(Image.TXT_LEVELS, key=lambda l: abs(l - v)) for v in (r, g, b))
|
||||
grey = max(8, min(238, 8 + 10 * round(((r + g + b) / 3 - 8) / 10)))
|
||||
# Weighted as the eye weighs the channels, so a grey-blue sky does
|
||||
# not fall to a green or a yellow entry when its error moves it.
|
||||
d = lambda c: 2 * (c[0] - r) ** 2 + 4 * (c[1] - g) ** 2 + 3 * (c[2] - b) ** 2
|
||||
return cube if d(cube) <= d((grey, grey, grey)) else (grey, grey, grey)
|
||||
|
||||
@staticmethod
|
||||
def txt_palette_dither(samples):
|
||||
"""samples (rows of (luminance, rgb)) with every rgb replaced by a
|
||||
palette colour, the error diffused Floyd-Steinberg fashion in RGB."""
|
||||
h, w = len(samples), len(samples[0]) if samples else 0
|
||||
acc = [[list(map(float, rgb)) for lum, rgb in row] for row in samples]
|
||||
out = []
|
||||
for y in range(h):
|
||||
row = []
|
||||
for x in range(w):
|
||||
want = tuple(min(255.0, max(0.0, v)) for v in acc[y][x])
|
||||
got = Image.txt_nearest_palette(tuple(int(round(v)) for v in want))
|
||||
# Three quarters of the error travel on: full diffusion
|
||||
# over a 256-entry palette speckles a smooth sky with
|
||||
# far-off entries, none of it looks posterised.
|
||||
err = [0.75 * (a - b) for a, b in zip(want, got)]
|
||||
for dx, dy, wgt in ((1, 0, 7 / 16), (-1, 1, 3 / 16), (0, 1, 5 / 16), (1, 1, 1 / 16)):
|
||||
xx, yy = x + dx, y + dy
|
||||
if 0 <= xx < w and yy < h:
|
||||
acc[yy][xx] = [a + e * wgt for a, e in zip(acc[yy][xx], err)]
|
||||
row.append((samples[y][x][0], got))
|
||||
out.append(row)
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def txt_dither(samples, dark_background):
|
||||
"""Floyd-Steinberg error diffusion of the luminance to one bit: True
|
||||
@@ -193,6 +244,12 @@ class Image(klammer_base.Klammer_base):
|
||||
for row in self.txt_samples(cols, rows):
|
||||
cells.append([(ramp[min(len(ramp) - 1, int(lum * len(ramp)))], rgb, Image.txt_toned(rgb, dark)) for lum, rgb in row])
|
||||
elif style == "halfblock":
|
||||
# Every cell is "▀" with the pair's two EXACT colours as foreground
|
||||
# and background. Choosing the character by the pair's dithered
|
||||
# brightness (tried 2026-10-01, to give txt a monochrome picture)
|
||||
# merges every pair of one brightness class into one colour, and
|
||||
# Andy saw the merged pairs at once on Linux; the txt twin stays a
|
||||
# solid field, as Image_txt_style's description says.
|
||||
samples = self.txt_samples(cols, rows * 2)
|
||||
for y in range(rows):
|
||||
cells.append([("▀", samples[2 * y][x][1], samples[2 * y + 1][x][1]) for x in range(cols)])
|
||||
|
||||
Reference in new issue
Block a user