1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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).
|