diff --git a/regress/image-sixel-parse.sh b/regress/image-sixel-parse.sh index 7e8eab35e..dc63b17a4 100755 --- a/regress/image-sixel-parse.sh +++ b/regress/image-sixel-parse.sh @@ -31,6 +31,14 @@ with image_test(sys.argv[1]) as test: assert len(client.images()) == 1 check_image(client.images()[0], 8, 16, [rgb] * (8 * 16)) + # Undefined registers are black in the shared image model. + test.emit(b'\033[2J\033[H\033P9;1q"1;1;8;16' + b'#5!8~-!8~-!8N\033\\') + assert len(client.images()) == 1 + check_image(client.images()[0], 8, 16, [(0, 0, 0)] * (8 * 16)) + test.redraw() + check_image(client.images()[0], 8, 16, [(0, 0, 0)] * (8 * 16)) + # Deliver the terminator in two pieces. No image is placed before it. test.emit(b'\033[2J\033[H') for byte in valid[:-1]: diff --git a/regress/image_terminal.py b/regress/image_terminal.py index 493c6ade5..c5ef50462 100644 --- a/regress/image_terminal.py +++ b/regress/image_terminal.py @@ -125,9 +125,11 @@ class Terminal: if match is None: break if match[1] == b'P': - image = re.match(rb'[0-9;]*q(.*)', match[2], re.S) + image = re.match(rb'([0-9;]*)q(.*)', match[2], re.S) if image is not None: - w, h, pixels = decode_sixel(image[1]) + params = image[1].split(b';') + p2 = int(params[1] or b'0') if len(params) > 1 else 0 + w, h, pixels = decode_sixel(image[2], p2) for i, colour in enumerate(pixels): if colour is not None: self.pixels[self.x * self.cw + i % w, diff --git a/regress/image_test.py b/regress/image_test.py index 46890eb8f..c04470ce3 100644 --- a/regress/image_test.py +++ b/regress/image_test.py @@ -1,6 +1,7 @@ """Helpers for image tests using an attached client on a PTY.""" import contextlib +import colorsys import errno import fcntl import itertools @@ -46,8 +47,13 @@ class Client: def images(self): """Decode complete SIXEL sequences from the terminal output.""" - return [decode_sixel(body) for body in re.findall( - rb"\033P[0-9;]*q(.*?)\033\\", self.data, re.S)] + images = [] + for params, body in re.findall( + rb"\033P([0-9;]*)q(.*?)\033\\", self.data, re.S): + params = params.split(b";") + p2 = int(params[1] or b"0") if len(params) > 1 else 0 + images.append(decode_sixel(body, p2)) + return images class ImageTest: @@ -234,12 +240,12 @@ def sixel(w, h, palette, pixels, p2=1): return "".join(out).encode() -def decode_sixel(body): +def decode_sixel(body, p2=1): """Read RGB output pixels without using tmux's SIXEL decoder.""" attributes = re.match(rb'"1;1;(\d+);(\d+)', body) assert attributes is not None, ("missing raster attributes", body[:80]) w, h = map(int, attributes.groups()) - pixels = [None] * (w * h) + pixels = [None if p2 == 1 else (0, 0, 0)] * (w * h) palette = {} c = x = y = 0 pos = attributes.end() @@ -247,13 +253,19 @@ def decode_sixel(body): ch = body[pos] pos += 1 if ch == ord("#"): - match = re.match(rb"(\d+)(?:;2;(\d+);(\d+);(\d+))?", body[pos:]) + match = re.match(rb"(\d+)(?:;([12]);(\d+);(\d+);(\d+))?", body[pos:]) assert match is not None, body[pos:pos + 80] c = int(match[1]) if match[2] is not None: - rgb = tuple(int(match[i]) for i in range(2, 5)) - assert all(v <= 100 for v in rgb), rgb - palette[c] = tuple((v * 255 + 50) // 100 for v in rgb) + values = tuple(int(match[i]) for i in range(3, 6)) + if match[2] == b"1": + hue, lightness, saturation = values + rgb = colorsys.hls_to_rgb(((hue + 240) % 360) / 360, + lightness / 100, saturation / 100) + palette[c] = tuple(int(v * 255 + .5) for v in rgb) + else: + assert all(v <= 100 for v in values), values + palette[c] = tuple((v * 255 + 50) // 100 for v in values) pos += match.end() continue if ch == ord("$"):