diff --git a/CHANGELOG.md b/CHANGELOG.md index d1d9629e..2832979e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,7 @@ (https://codeberg.org/dnkl/foot/issues/376). * The minimum version requirement for the libxkbcommon dependency is now 1.0.0. +* Empty pixel rows at the bottom of a sixel is now trimmed. ### Deprecated diff --git a/scripts/generate-alt-random-writes.py b/scripts/generate-alt-random-writes.py index 2e51082b..2a3b23a5 100755 --- a/scripts/generate-alt-random-writes.py +++ b/scripts/generate-alt-random-writes.py @@ -158,20 +158,37 @@ def main(): reset_actions = ['\033[m', '\033[39m', '\033[49m'] out.write(random.choice(reset_actions)) - # Leave alt screen - out.write('\033[m\033[r\033[?1049l') + # Reset colors + out.write('\033[m\033[r') if opts.sixel: # The sixel 'alphabet' sixels = '?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~' - for _ in range(200): - # Offset image - out.write(' ' * (random.randrange(cols // 2))) + last_pos = None + last_size = None + + for _ in range(100): + if last_pos is not None and random.randrange(2): + # Overwrite last sixel. I.e. use same position and + # size as last sixel + pass + else: + # Random origin in upper left quadrant + last_pos = random.randrange(lines // 2) + 1, random.randrange(cols // 2) + 1 + last_size = random.randrange(height // 2), random.randrange(width // 2) + + out.write(f'\033[{last_pos[0]};{last_pos[1]}H') + six_height, six_width = last_size + six_rows = (six_height + 5) // 6 # Round up; each sixel is 6 pixels # Begin sixel out.write('\033Pq') + # Sixel size. Without this, sixels will be + # auto-resized on cell-boundaries. + out.write(f'"1;1;{six_width};{six_height}') + # Set up 256 random colors for idx in range(256): # param 2: 1=HLS, 2=RGB. @@ -179,29 +196,36 @@ def main(): # (except 'hue' which is 0..360) out.write(f'#{idx};2;{random.randrange(101)};{random.randrange(101)};{random.randrange(101)}') - # Randomize image width/height - six_height = random.randrange(height // 2) - six_width = random.randrange(width // 2) + for row in range(six_rows): + band_count = random.randrange(4, 33) + for band in range(band_count): + # Choose a random color + out.write(f'#{random.randrange(256)}') - # Sixel size. Without this, sixels will be - # auto-resized on cell-boundaries. We expect programs - # to emit this sequence since otherwise you cannot get - # correctly sized images. - out.write(f'"0;0;{six_width};{six_height}') + if random.randrange(2): + for col in range(six_width): + out.write(f'{random.choice(sixels)}') + else: + pix_left = six_width + while pix_left > 0: + repeat_count = random.randrange(1, pix_left + 1) + out.write(f'!{repeat_count}{random.choice(sixels)}') + pix_left -= repeat_count - for row in range(six_height // 6): # Each sixel is 6 pixels - # Choose a random color - out.write(f'#{random.randrange(256)}') - - for col in range(six_width): - out.write(f'{random.choice(sixels)}') - - # Next line - out.write('-') + # Next line + if band + 1 < band_count: + # Move cursor to beginning of current row + out.write('$') + elif row + 1 < six_rows: + # Newline + out.write('-') # End sixel out.write('\033\\') + # Leave alt screen + out.write('\033[?1049l') + if __name__ == '__main__': sys.exit(main()) diff --git a/sixel.c b/sixel.c index 6d57b6c4..776750a2 100644 --- a/sixel.c +++ b/sixel.c @@ -37,8 +37,9 @@ sixel_init(struct terminal *term) term->sixel.state = SIXEL_DECSIXEL; term->sixel.pos = (struct coord){0, 0}; + term->sixel.max_non_empty_row_no = 0; + term->sixel.row_byte_ofs = 0; term->sixel.color_idx = 0; - term->sixel.max_col = 0; term->sixel.param = 0; term->sixel.param_idx = 0; memset(term->sixel.params, 0, sizeof(term->sixel.params)); @@ -695,6 +696,13 @@ sixel_reflow(struct terminal *term) void sixel_unhook(struct terminal *term) { + if (term->sixel.image.height > term->sixel.max_non_empty_row_no + 1) { + LOG_DBG( + "last row only partially filled, reducing image height: %d -> %d", + term->sixel.image.height, term->sixel.max_non_empty_row_no + 1); + term->sixel.image.height = term->sixel.max_non_empty_row_no + 1; + } + int pixel_row_idx = 0; int pixel_rows_left = term->sixel.image.height; const int stride = term->sixel.image.width * sizeof(uint32_t); @@ -825,7 +833,6 @@ sixel_unhook(struct terminal *term) term->sixel.image.data = NULL; term->sixel.image.width = 0; term->sixel.image.height = 0; - term->sixel.max_col = 0; term->sixel.pos = (struct coord){0, 0}; free(term->sixel.private_palette); @@ -837,6 +844,88 @@ sixel_unhook(struct terminal *term) render_refresh(term); } +static bool +resize_horizontally(struct terminal *term, int new_width) +{ + LOG_DBG("resizing image horizontally: %dx(%d) -> %dx(%d)", + term->sixel.image.width, term->sixel.image.height, + new_width, term->sixel.image.height); + + if (unlikely(new_width > term->sixel.max_width)) { + LOG_WARN("maximum image dimensions reached"); + return false; + } + + uint32_t *old_data = term->sixel.image.data; + const int old_width = term->sixel.image.width; + const int height = term->sixel.image.height; + + int alloc_height = (height + 6 - 1) / 6 * 6; + + xassert(new_width > 0); + xassert(alloc_height > 0); + + /* Width (and thus stride) change - need to allocate a new buffer */ + uint32_t *new_data = xmalloc(new_width * alloc_height * sizeof(uint32_t)); + + /* Copy old rows, and initialize new columns to background color */ + for (int r = 0; r < height; r++) { + memcpy(&new_data[r * new_width], + &old_data[r * old_width], + old_width * sizeof(uint32_t)); + + for (int c = old_width; c < new_width; c++) + new_data[r * new_width + c] = color_with_alpha(term, term->colors.bg); + } + + free(old_data); + + term->sixel.image.data = new_data; + term->sixel.image.width = new_width; + term->sixel.row_byte_ofs = term->sixel.pos.row * new_width; + return true; +} + +static bool +resize_vertically(struct terminal *term, int new_height) +{ + LOG_DBG("resizing image vertically: (%d)x%d -> (%d)x%d", + term->sixel.image.width, term->sixel.image.height, + term->sixel.image.width, new_height); + + if (unlikely(new_height > term->sixel.max_height)) { + LOG_WARN("maximum image dimensions reached"); + return false; + } + + uint32_t *old_data = term->sixel.image.data; + const int width = term->sixel.image.width; + const int old_height = term->sixel.image.height; + + int alloc_height = (new_height + 6 - 1) / 6 * 6; + + xassert(width > 0); + xassert(new_height > 0); + + uint32_t *new_data = realloc( + old_data, width * alloc_height * sizeof(uint32_t)); + + if (new_data == NULL) { + LOG_ERRNO("failed to reallocate sixel image buffer"); + return false; + } + + /* Initialize new rows to background color */ + for (int r = old_height; r < new_height; r++) { + for (int c = 0; c < width; c++) + new_data[r * width + c] = color_with_alpha(term, term->colors.bg); + } + + term->sixel.image.data = new_data; + term->sixel.image.height = new_height; + return true; +} + static bool resize(struct terminal *term, int new_width, int new_height) { @@ -844,6 +933,13 @@ resize(struct terminal *term, int new_width, int new_height) term->sixel.image.width, term->sixel.image.height, new_width, new_height); + if (new_width > term->sixel.max_width || + new_height > term->sixel.max_height) + { + LOG_WARN("maximum image dimensions reached"); + return false; + } + uint32_t *old_data = term->sixel.image.data; const int old_width = term->sixel.image.width; const int old_height = term->sixel.image.height; @@ -892,47 +988,55 @@ resize(struct terminal *term, int new_width, int new_height) term->sixel.image.data = new_data; term->sixel.image.width = new_width; term->sixel.image.height = new_height; + term->sixel.row_byte_ofs = term->sixel.pos.row * new_width; return true; } static void -sixel_add(struct terminal *term, uint32_t color, uint8_t sixel) +sixel_add(struct terminal *term, int col, int width, uint32_t color, uint8_t sixel) { - //LOG_DBG("adding sixel %02hhx using color 0x%06x", sixel, color); + xassert(term->sixel.pos.col < term->sixel.image.width); + xassert(term->sixel.pos.row < term->sixel.image.height); - if (term->sixel.pos.col >= term->sixel.max_width || - term->sixel.pos.row * 6 + 5 >= term->sixel.max_height) - { - return; - } + size_t ofs = term->sixel.row_byte_ofs + col; + uint32_t *data = &term->sixel.image.data[ofs]; - if (term->sixel.pos.col >= term->sixel.image.width || - term->sixel.pos.row * 6 + 5 >= (term->sixel.image.height + 6 - 1) / 6 * 6) - { - int width = max( - term->sixel.image.width, - max(term->sixel.max_col, term->sixel.pos.col + 1)); + int max_non_empty_row = 0; + int row = term->sixel.pos.row; - int height = max( - term->sixel.image.height, - (term->sixel.pos.row + 1) * 6); - - if (!resize(term, width, height)) - return; - } - - for (int i = 0; i < 6; i++, sixel >>= 1) { + for (int i = 0; i < 6; i++, sixel >>= 1, data += width) { if (sixel & 1) { - size_t pixel_row = term->sixel.pos.row * 6 + i; - size_t stride = term->sixel.image.width; - size_t idx = pixel_row * stride + term->sixel.pos.col; - term->sixel.image.data[idx] = color_with_alpha(term, color); + *data = color; + max_non_empty_row = row + i; } } xassert(sixel == 0); - term->sixel.pos.col++; + + term->sixel.max_non_empty_row_no = max( + term->sixel.max_non_empty_row_no, + max_non_empty_row); +} + +static void +sixel_add_many(struct terminal *term, uint8_t c, unsigned count) +{ + uint32_t color = term->sixel.palette[term->sixel.color_idx]; + + int col = term->sixel.pos.col; + int width = term->sixel.image.width; + + if (unlikely(col + count - 1 >= width)) { + width = col + count; + if (unlikely(!resize_horizontally(term, width))) + return; + } + + for (unsigned i = 0; i < count; i++, col++) + sixel_add(term, col, width, color, c); + + term->sixel.pos.col = col; } static void @@ -959,16 +1063,26 @@ decsixel(struct terminal *term, uint8_t c) break; case '$': - if (term->sixel.pos.col > term->sixel.max_col) - term->sixel.max_col = term->sixel.pos.col; - term->sixel.pos.col = 0; + if (likely(term->sixel.pos.col <= term->sixel.max_width)) { + /* + * We set, and keep, ‘col’ outside the image boundary when + * we’ve reached the maximum image height, to avoid also + * having to check the row vs image height in the common + * path in sixel_add(). + */ + term->sixel.pos.col = 0; + } break; case '-': - if (term->sixel.pos.col > term->sixel.max_col) - term->sixel.max_col = term->sixel.pos.col; - term->sixel.pos.row++; + term->sixel.pos.row += 6; term->sixel.pos.col = 0; + term->sixel.row_byte_ofs += term->sixel.image.width * 6; + + if (term->sixel.pos.row >= term->sixel.image.height) { + if (!resize_vertically(term, term->sixel.pos.row + 6)) + term->sixel.pos.col = term->sixel.max_width + 1; + } break; case '?': case '@': case 'A': case 'B': case 'C': case 'D': case 'E': @@ -981,7 +1095,7 @@ decsixel(struct terminal *term, uint8_t c) case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': case '{': case '|': case '}': case '~': - sixel_add(term, term->sixel.palette[term->sixel.color_idx], c - 63); + sixel_add_many(term, c - 63, 1); break; case ' ': @@ -1031,6 +1145,7 @@ decgra(struct terminal *term, uint8_t c) ph <= term->sixel.max_height && pv <= term->sixel.max_width) { resize(term, ph, pv); + term->sixel.max_non_empty_row_no = pv - 1; } term->sixel.state = SIXEL_DECSIXEL; @@ -1050,13 +1165,23 @@ decgri(struct terminal *term, uint8_t c) term->sixel.param += c - '0'; break; - default: - //LOG_DBG("repeating '%c' %u times", c, term->sixel.param); - for (unsigned i = 0; i < term->sixel.param; i++) - decsixel(term, c); + case '?': case '@': case 'A': case 'B': case 'C': case 'D': case 'E': + case 'F': case 'G': case 'H': case 'I': case 'J': case 'K': case 'L': + case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': + case 'T': case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': + case '[': case '\\': case ']': case '^': case '_': case '`': case 'a': + case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': + case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': + case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': + case 'w': case 'x': case 'y': case 'z': case '{': case '|': case '}': + case '~': { + unsigned count = term->sixel.param; + if (likely(count > 0)) + sixel_add_many(term, c - 63, count); term->sixel.state = SIXEL_DECSIXEL; break; } + } } static void @@ -1114,7 +1239,8 @@ decgci(struct terminal *term, uint8_t c) LOG_DBG("setting palette #%d = HLS %hhu/%hhu/%hhu (0x%06x)", term->sixel.color_idx, hue, lum, sat, rgb); - term->sixel.palette[term->sixel.color_idx] = rgb; + term->sixel.palette[term->sixel.color_idx] = + color_with_alpha(term, rgb); break; } @@ -1126,7 +1252,8 @@ decgci(struct terminal *term, uint8_t c) LOG_DBG("setting palette #%d = RGB %hhu/%hhu/%hhu", term->sixel.color_idx, r, g, b); - term->sixel.palette[term->sixel.color_idx] = r << 16 | g << 8 | b; + term->sixel.palette[term->sixel.color_idx] = + color_with_alpha(term, r << 16 | g << 8 | b); break; } } diff --git a/terminal.h b/terminal.h index e4e662fb..8be5dd74 100644 --- a/terminal.h +++ b/terminal.h @@ -521,8 +521,9 @@ struct terminal { } state; struct coord pos; /* Current sixel coordinate */ + int max_non_empty_row_no; + size_t row_byte_ofs; /* Byte position into image, for current row */ int color_idx; /* Current palette index */ - int max_col; /* Largest column index we've seen (aka the image width) */ uint32_t *private_palette; /* Private palette, used when private mode 1070 is enabled */ uint32_t *shared_palette; /* Shared palette, used when private mode 1070 is disabled */ uint32_t *palette; /* Points to either private_palette or shared_palette */