aboutsummaryrefslogtreecommitdiffstats
path: root/dev-docs/salvador.md
diff options
context:
space:
mode:
authorAhmed <git@gumx.cc>2026-06-14 01:46:29 +0300
committerAhmed <git@gumx.cc>2026-06-14 01:46:29 +0300
commit5a8d568931d9b23ce0df1265d05259a7012081c9 (patch)
tree4ea19b8bb1763a38246f342f172c285f6579e09b /dev-docs/salvador.md
init: mostly vibed
Diffstat (limited to 'dev-docs/salvador.md')
-rw-r--r--dev-docs/salvador.md143
1 files changed, 143 insertions, 0 deletions
diff --git a/dev-docs/salvador.md b/dev-docs/salvador.md
new file mode 100644
index 0000000..fa6e75f
--- /dev/null
+++ b/dev-docs/salvador.md
@@ -0,0 +1,143 @@
+# salvador - developer documentation
+
+## Overview
+
+Salvador converts an image URL to mIRC colour art using half-block characters
+and Floyd-Steinberg dithering. It uses Pillow for image loading and resizing
+and implements its own color matching and dithering against the 99-color mIRC
+palette.
+
+---
+
+## Module structure
+
+```
+salvador/
+ salvador.py - image fetching, rendering, !draw command
+ commands.py - admin commands (!join, !part, !chans, !help)
+ __init__.py - empty
+```
+
+---
+
+## Half-block rendering technique
+
+Each IRC character represents two vertical pixels. The character used is
+U+2584 (LOWER HALF BLOCK). The foreground color represents the lower pixel
+and the background color represents the upper pixel. IRC color codes are
+written as `\x03fg,bg`.
+
+To render an image at N character lines, the image is resized to N*2 pixels
+tall. Pairs of rows are then combined: row 0 and row 1 become character line 0,
+row 2 and row 3 become character line 1, and so on.
+
+This doubles the effective vertical resolution compared to full-block
+approaches where each character is one pixel.
+
+---
+
+## Color matching
+
+`_nearest(r, g, b)` finds the closest mIRC palette color using a
+perceptually-weighted RGB distance formula:
+
+```python
+rmean = (r + 128) / 2
+dist = (2 + rmean/256) * dr*dr + 4*dg*dg + (2 + (255-rmean)/256) * db*db
+```
+
+This is a standard approximation of human color perception. Green is weighted
+highest (coefficient 4). Red and blue are weighted by the red channel mean.
+Pure Euclidean distance produces noticeably worse results especially in
+saturated reds and blues.
+
+The full 99-color mIRC palette is hardcoded in `MIRC`. The first 16 are the
+original mIRC colors. Colors 16-98 are the extended palette added in later
+mIRC versions. Colors 88-98 are grayscale.
+
+---
+
+## Floyd-Steinberg dithering
+
+`_dither()` is a manual implementation of Floyd-Steinberg dithering.
+PIL's built-in dithering only targets the web palette or a custom palette
+using PIL's quantize methods, which do not map cleanly to the mIRC palette
+index scheme. Implementing it directly gives full control over the palette
+and error diffusion.
+
+The algorithm quantizes each pixel to the nearest palette color, computes
+the quantization error (difference between original and quantized RGB), and
+distributes that error to neighboring pixels:
+
+```
+(x+1, y ): 7/16 of error
+(x-1, y+1): 3/16 of error
+(x , y+1): 5/16 of error
+(x+1, y+1): 1/16 of error
+```
+
+The buffer is floats to accumulate fractional errors. Final values are clamped
+to 0-255 before color matching.
+
+---
+
+## Fetch and size limits
+
+`_fetch()` sends a request with a custom User-Agent and reads up to `MAX_BYTES`
+(5MB). It checks the Content-Type header before reading the body. If the
+response is not an image type, it raises a ValueError immediately rather than
+reading the full body.
+
+The size limit prevents memory exhaustion from large images. 5MB is generous
+for web images and strict enough to catch accidental links to large files.
+
+---
+
+## Aspect ratio fitting
+
+`_fit()` computes the output dimensions to fit within `MAX_WIDTH x height`
+while preserving the aspect ratio. IRC monospace character cells are
+approximately twice as tall as they are wide, so a naïve pixel aspect ratio
+would produce output that is 2× stretched vertically.
+
+`CHAR_RATIO = 2.0` corrects for this: the image's pixel aspect ratio is
+multiplied by `CHAR_RATIO` before the fit comparison, so the renderer allocates
+twice as many character columns as the raw pixel ratio would suggest, cancelling
+the cell height distortion.
+
+---
+
+## Flood protection
+
+A `FLOOD_DELAY` of 0.4 seconds is inserted between output lines via
+`time.sleep()`. Without this, sending 6-12 lines in rapid succession triggers
+flood protection on most IRC servers and the lines get dropped.
+
+`MAX_HEIGHT = 12` lines. Sending more than 12 lines would be disruptive in
+any channel.
+
+---
+
+## URL handling
+
+The command strips trailing punctuation from the URL (`.`, `,`, `)`, `>`).
+This handles the common case where someone pastes a URL at the end of a
+sentence and the punctuation gets included in the copied text.
+
+Only `http://` and `https://` URLs are accepted. This blocks `file://` and
+other schemes that could access local resources.
+
+---
+
+## Known issues and tradeoffs
+
+**Color accuracy degrades on complex gradients.** The 99-color mIRC palette
+is coarse. Dithering helps significantly but cannot fully compensate for the
+palette limitation.
+
+**No image caching.** Every `!draw` fetches the image fresh. Repeated calls
+with the same URL re-download every time.
+
+**Blocking fetch.** `_fetch()` runs in the command handler (main Sopel thread)
+and blocks while downloading. Large images or slow servers will block the bot
+for the duration of the download (up to 10 seconds timeout).