When a Sixel image is drawn with a vertical pixel-aspect scale (DECGRA raster
attribute Pan > 1) and the image's pixel height is not a multiple of 6, the
final partial band (the bottom 1–5 pixel rows) renders as garbled rows. With
Pan = 1, or with a height that is a whole multiple of 6 (no partial band), the
exact same image renders correctly.
The Sixel raster attributes are "Pan;Pad;Ph;Pv. cterm interprets Pan/Pad as
integer pixel-block dimensions (cterm_dec.c: sx_iv = Pan tall, sx_ih = Pad
wide), so Pan = 2 draws each source pixel as 2 rows. Sixel pixel rows are
grouped into 6-row bands; when the image height is not a multiple of 6 the last
band is partial (fewer than 6 active rows). Scaling that partial band vertically
(Pan > 1) is where the corruption appears — full bands above it are fine.
Each file is solid-color horizontal stripes, one color per 6-pixel band, so a
correct render is clean colored bars and any corruption is obvious at the bottom:
| file | raster attr | height | bands | result |
|---|---|---|---|---|
repro_garbled.six |
"2;1;120;50 |
50 | 8 + a 2-row partial | bottom band garbled |
repro_clean.six |
"2;1;120;48 |
48 | 8 exact | renders correctly |
repro_control.six |
"1;1;120;50 |
50 | 8 + a 2-row partial | renders correctly |
Display each (e.g. cat repro_garbled.six to the terminal):
Pan > 1 (same height 50, only Pan differs: 2 vs 1).The final partial band should scale exactly like the full bands above it — its
present rows scaled by Pan, its absent rows left blank.
SyncDuke (a Duke Nukem 3D BBS door): it emits a half-height Sixel with Pan=2,
Pad=2 so cterm upscales it 2× and the frame ships ~half the bytes. At an 80×25
terminal (640×400 → 200 encoded rows = 33 bands + 2) the bottom rows garble; at
80×24 (640×384 → 192 = 32 exact bands) it is clean. Worked around in the door by
clamping the encoded Sixel height to a whole number of bands.
Anonymous
Can you include screenshots? These all appear to render correctly for me.
Last edit: Stephen James Hurd 2026-06-28
It's possible you're misunderstanding what the raster height parameter actually does... the definitive reference is here: https://vt100.net/docs/vt3xx-gp/chapter14.html
Essentially, the raster height/width just specifies an area to be filled with the background colour. It is not a clipping rectangle.
Here some examples of corrupted Sixel images - they only corrupt at 640x400 (25 line) mode, with no corruption at 640x384 (24 line) mode.
Oh, and not corrupted when scaled to the 640x396 (evenly divisible by 6).
Last edit: Rob Swindell 2026-06-28
Claude's follow-up:
Ticket #258 follow-up — corrected repro (Ph;Pv removed)
You're right about the raster attributes, and thanks for the pointer — Ph/Pv is a
background-fill area, not a clip. The original three files muddied the report:
they set
"2;1;120;50, and that Pv (50) isn't a whole number of 6-row bands, sopart of what I flagged as corruption was the stray background remainder you
described. That one's on me.
There's a separate, real artifact underneath it that involves no Ph;Pv at
all. With the raster size stripped (header is just
"pan;pad), the trigger isa partial final band — image height not a multiple of 6 — rendered with
pan > 1 (vertical pixel-aspect scaling). With pan = 1, or with a height
that's a whole number of bands, the identical image renders clean.
Repro (3 files attached)
Each is
ESC P q "pan;pad+ palette + bands + ST — no Ph;Pv — withmulti-color bands (one palette color per pixel row, so every band is a 6-color
$-overlay, like real image data, not a solid fill). A correct render is Nhorizontal color lines; corruption shows as garbled rows in the bottom band.
258_pan2_partial.six"2;2258_pan1_partial.six"1;1258_pan2_aligned.six"2;2cateach to a sixel-capable SyncTERM:(2 vs 1): isolates it to the vertical scaling.
48): isolates it to the partial band.
Where it bit in practice
SyncDuke (a Duke Nukem 3D BBS door) renders Build's native 320×200 and ships it
as a half-size sixel with
"2;2, so SyncTERM upscales each pixel to a 2×2 block— ~1/4 the bytes of a pre-upscaled frame. On an 80×25 canvas the encoded height
is 200 rows = 33 bands + a 2-row partial, and that bottom band garbles; on 80×24
(192 = 32 exact bands) it's clean. Worked around in the door by clamping the
encoded height to a whole number of bands. Screenshots of both states attached.
SyncTERM build: <fill in="" —="" help="" →="" about="" build="" date="">.</fill>
I meant include screenshots of the alleged repro. Once more, these "repro" files all seem to render correctly. To be clear, there's no such thing as "a partial band". Every sixel defines the contents of six pixels to be the current foreground colour or either the current sixel background colour (if the DCS P2 is not specified or is 0 or 2, as in all of these examples), or the current pixel value (if P2 is explicitly 1 ie: not in any of these repro cases).
Ignoring everything Claude said and going just off what @rswindell said, the issue is likely that sixel can't express partial bands, and a 25-line screen contains a partial band at the end.
With scrolling disabled, the parser refuses the newline that would move the sixel Y position into that partial bottom area. So the partial final band cannot be placed there. Depending on width/X state, following data is ignored or applied to the prior band position.
With scrolling enabled, it does move down, sees the next full 12-pixel band would exceed the bottom, scrolls by a whole text row (16 pixels), then draws the next band higher on the screen. So it also does not update the last 4 physical rows as a partial sixel band.
And independently, bitmap_setpixels() does not clip off-screen rectangles; it rejects the whole rectangle. So if a partial off-screen sixel band ever did get submitted, the visible part would still not draw.
Latest repro/preport by Claude regarding this issue. I confirmed the results in SyncTERM myself.
Repro using the above .six files and SyncTERM
The same files render "fine" (or at least, not obviously corrupt) with Windows Terminal.
This file displays fine in other sixel terminals, but corrupted in SyncTERM
Example output in SyncTERM
Fix for the mask/background problem committed... it still won't clip sixels though, so the modulo six thing is likely still needed.