diff options
Diffstat (limited to 'dev-docs/salvador.md')
| -rw-r--r-- | dev-docs/salvador.md | 143 |
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). |
