# 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).