libsixel icon indicating copy to clipboard operation
libsixel copied to clipboard

Color problem on DEC LJ-250 printer

Open saitoha opened this issue 2 months ago • 8 comments

Printer self test: OK Image

$ img2sixel images/snake.jpg -w200 -8 | tee snake-printtest.six > /dev/cu.usbserial-2
Image

welp, that’s a hot mess...

snake-printtest.six.txt

$ chafa images/snake.jpg -f sixel --dither=diffusion -s 25x | tee snake-chafa.six.txt > /dev/cu.usbserial-2
Image

snake-chafa.six.txt

$ jpegtopnm images/snake.jpg | pnmscale -quiet -xyfit 200 200 | pnmquant 256 | ppmtosixel | tee snake-ppmtosixel.six.txt > /dev/cu.usbserial-2
Image

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.

snake-ppmtosixel.six.txt

saitoha avatar Oct 04 '25 13:10 saitoha

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 avatar Oct 04 '25 18:10 j4james

@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. Image

That means setting P3 (the third DCS parameter) to 8 or 9. Image

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. Image

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. Image

saitoha avatar Oct 04 '25 18:10 saitoha

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.

Image Image

Given that, using libsixel in fixed 8-color mode with dithering and rendering at 180 dpi may actually produce cleaner output.

saitoha avatar Oct 04 '25 19:10 saitoha

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
Image

snake-map8-testprint.six.txt

saitoha avatar Oct 04 '25 19:10 saitoha

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. Image

saitoha avatar Oct 04 '25 19:10 saitoha

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 avatar Oct 04 '25 21:10 j4james

@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.

saitoha avatar Oct 05 '25 09:10 saitoha

I printed the LJ250’s 256-color chart; the red box marks the base 8 colors.

Image

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

saitoha avatar Oct 05 '25 15:10 saitoha