Color problem on DEC LJ-250 printer
Printer self test: OK
$ img2sixel images/snake.jpg -w200 -8 | tee snake-printtest.six > /dev/cu.usbserial-2
welp, that’s a hot mess...
$ chafa images/snake.jpg -f sixel --dither=diffusion -s 25x | tee snake-chafa.six.txt > /dev/cu.usbserial-2
$ jpegtopnm images/snake.jpg | pnmscale -quiet -xyfit 200 200 | pnmquant 256 | ppmtosixel | tee snake-ppmtosixel.six.txt > /dev/cu.usbserial-2
We need to re-examine ppmtosixel.
Its output looks slightly dark, likely because it emits sRGB as-is; now it makes sense that ppmtosixel provides a -g <gamma> option.
Are you using 180 dpi by any chance? Because the LJ-250 manual says you only get 8 colors at 180 dpi, and it seems more likely that these images are 8 colors than 256 colors.
The other possibility is that you have access to a palette of 256, but can only use a subset of that palette in one image, similar to the way the VT240 and VT340 work. Because the manual also mentions a 64-color palette for compatibility with the VT240, but the VT240 certainly doesn't allow you to use all 64 of those colors in one image - you've got a maximum of 4.
@j4james
Are you using 180 dpi by any chance?
Yes, I should have read the manual more carefully -- 256 colors aren’t available in the default 180 dpi mode. need to switch to 90 dpi.
That means setting P3 (the third DCS parameter) to 8 or 9.
Applying gamma correction (γ=2.2) noticeably improved the tones; we should not emit raw sRGB.
The quick hack applied gamma to the palette after the fact, but we should really apply it before palette construction and dither in the correct color space; also, gamma is only an approximation -- we should use the proper sRGB -> Linear RGB transfer functions.
As for the LJ250 “256-color” mode, it’s essentially a halftone: It sprays the three primaries to form the 2^3 = 8 base colors on a 180 dpi grid, then composes a 2×2 cell at 90 dpi, yielding 8^4 = 256 combinations that read as 256 colors.
As for the LJ250 “256-color” mode, it’s essentially a halftone: It sprays the three primaries to form the 2^3 = 8 base colors on a 180 dpi grid, then composes a 2×2 cell at 90 dpi, yielding 8^4 = 256 combinations that read as 256 colors.
If the above is correct, the “adaptive 256 colors” produced by img2sixel are likely remapped to a fixed 256-color set; the unexpected banding supports this interpretation.
Given that, using libsixel in fixed 8-color mode with dithering and rendering at 180 dpi may actually produce cleaner output.
I tried printing at 180dpi 8color.
$ converters/img2sixel images/snake.jpg -m images/map8.six -R | tee snake-map8-testprint.six.txt > /dev/cu.usbserial-2
The 8 colors in images/map8.six are likely suboptimal; the inter-color distances aren’t ideal for dithering, so we probably need to derive a new set of 8 colors from a color chart.
In case it helps, they provide RGB definitions for the 8 default colors on page 7-31 of the manual.
| Row/Col | Color | RGB |
|---|---|---|
| 0/1 | black | 4;4;6 |
| 8/7 | red | 53;8;14 |
| 18/1 | green | 3;26;22 |
| 12/3 | yellow | 89;83;13 |
| 0/9 | blue | 4;4;29 |
| 6/7 | magenta | 53;5;25 |
| 22/7 | cyan | 2;22;64 |
| 25/6 | white | 90;88;85 |
Although white is technically transparent, so I suspect you may need to actually treat any white pixels in the image as transparent, instead of mapping them to a color index.
@j4james Thank you. I’m not a color expert, but here’s my current thinking -- there are four color spaces involved:
(1) A base space for palette mapping and diffusion To optimize inter-color distances, assume a distortion-free linear RGB space for now.
(2) sRGB RGB values read by libsixel (via stb, etc.) are in sRGB; convert these to (1).
(3) Device-specific colors This is what I’m trying to measure now -- the behavior of the paper / ink combination (now I'm using genuine cartridges LJ25X-AA / LJ25X-AB) If we can characterize six primaries (C, M, Y plus their ~1:1 mixes R, G, B), plus white and black -- eight colors total -- we can map them into (1) using a color chart and apply that palette to the image data.
(4) NTSC RGB (as you pointed out) I suspect this is the interchange space between the host and the printer. When writing the quantized 8-color rgb values into SIXEL, apply (1)→(4) if needed; with only eight colors, the device will likely do an acceptable mapping even without an explicit transform.
I printed the LJ250’s 256-color chart; the red box marks the base 8 colors.
I didn’t know about "colorimeters", but chatGPT suggested that’s the proper tool -- since I don’t have one on hand, I’ll proceed with approximate values.
| row/col | name | munsell | sRGB(gamma) |
|---|---|---|---|
| 0/1 | black | N4 | 86 89 90 |
| 0/9 | blue | 5PB4/10 | 0 102 159 |
| 6/7 | magenta | 5RP5/12 | 192 72 127 |
| 8/7 | red | 10RP6/10 | 219 110 130 |
| 12/3 | yellow | 7.5Y9/8 | 254 225 103 |
| 18/1 | green | 10G6/8 | 27 165 132 |
| 22/7 | cyan | 5B6/8 | 0 162 186 |
| 25/6 | white | N9.5 | 245 247 242 |