From 70478b799d22ab77af7c35463bbc52b3d27c62a6 Mon Sep 17 00:00:00 2001 From: Tinic Uro Date: Fri, 1 May 2026 15:57:25 -0700 Subject: [PATCH 1/3] Add runtime-sized rgba_to_sixel free functions. The image<> template requires compile-time W and H, which is awkward for callers that produce arbitrary-resolution frames at runtime (terminal previews, decoded video). Add two free functions in namespace constixel that take RGBA bytes plus runtime width/height and emit a sixel stream via a char-callback: rgba_to_sixel(rgba, w, h, char_out) Quantises to constixel's built-in 256-color palette (format_8bit::quant). Use when input has more than 256 distinct colors or when the caller has no preferred palette. rgba_to_sixel(rgba, w, h, palette, char_out) Uses a caller-supplied palette of up to 256 sRGB byte triples. The built-in quantiser is bypassed; each pixel maps to its squared-sRGB-distance nearest entry. Use when the caller already knows the exact palette of the source data. Implementation reuses the existing sixel header/raster/end helpers in spirit but inlines a runtime-sized encoder loop with run-length compression matching sixel_image. No changes to the compile-time-sized image<> path. --- constixel.hpp | 177 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) diff --git a/constixel.hpp b/constixel.hpp index 6cb6f5a..95bb3fa 100644 --- a/constixel.hpp +++ b/constixel.hpp @@ -6259,6 +6259,183 @@ class image { #endif // #ifndef __INTELLISENSE__ }; +// --------------------------------------------------------------------------- +// Runtime-sized sixel output +// --------------------------------------------------------------------------- +// +// The image<> template requires compile-time W and H, which is awkward for +// callers that produce arbitrary-resolution frames at runtime (terminal +// previews, decoded video, etc.). The free functions below take RGBA bytes +// plus runtime width/height and emit a sixel stream via a char-callback, +// mirroring the per-format image::sixel(F&&) entry point but without the +// compile-time-size constraint. +// +// Two overloads: +// * rgba_to_sixel(rgba, w, h, char_out) +// Quantises to constixel's built-in 256-color palette +// (format_8bit::quant). Use when input has more than 256 distinct +// colors or when the caller has no preferred palette. +// * rgba_to_sixel(rgba, w, h, palette, char_out) +// Uses a caller-supplied palette of <= 256 sRGB byte triples. The +// built-in quantiser is bypassed; each pixel maps to its +// squared-sRGB-distance nearest entry. Use when the caller already +// knows the exact palette of the source data. + +namespace runtime_sixel_detail { + +inline void emit_decimal(auto &&char_out, std::uint16_t u) { + if (u < 10) { + char_out(static_cast('0' + u)); + return; + } + char buf[6]; + int n = 0; + while (u > 0) { + buf[n++] = static_cast('0' + (u % 10)); + u = static_cast(u / 10); + } + while (n--) char_out(buf[n]); +} + +inline std::uint8_t nearest_in_palette(std::uint8_t r, std::uint8_t g, std::uint8_t b, + std::span> palette) { + int best_d = std::numeric_limits::max(); + std::uint8_t best_i = 0; + for (std::size_t i = 0; i < palette.size(); ++i) { + const int dr = static_cast(r) - static_cast(palette[i][0]); + const int dg = static_cast(g) - static_cast(palette[i][1]); + const int db = static_cast(b) - static_cast(palette[i][2]); + const int d = dr * dr + dg * dg + db * db; + if (d < best_d) { + best_d = d; + best_i = static_cast(i); + if (d == 0) break; + } + } + return best_i; +} + +template +inline void encode_indexed(const std::uint8_t *indexed, std::size_t w, std::size_t h, + std::span> palette, + F &&char_out) { + char_out(static_cast(0x1b)); + char_out('P'); + char_out('q'); + + char_out('"'); + emit_decimal(char_out, 1); + char_out(';'); + emit_decimal(char_out, 1); + char_out(';'); + emit_decimal(char_out, static_cast(w)); + char_out(';'); + emit_decimal(char_out, static_cast(h)); + + for (std::size_t c = 0; c < palette.size(); ++c) { + char_out('#'); + emit_decimal(char_out, static_cast(c)); + char_out(';'); + char_out('2'); + char_out(';'); + emit_decimal(char_out, static_cast((static_cast(palette[c][0]) * 100) / 255)); + char_out(';'); + emit_decimal(char_out, static_cast((static_cast(palette[c][1]) * 100) / 255)); + char_out(';'); + emit_decimal(char_out, static_cast((static_cast(palette[c][2]) * 100) / 255)); + } + + std::array used{}; + for (std::size_t y = 0; y < h; y += 6) { + used.fill(false); + const std::size_t y_end = std::min(y + 6, h); + for (std::size_t yy = y; yy < y_end; ++yy) { + for (std::size_t x = 0; x < w; ++x) { + used[indexed[yy * w + x]] = true; + } + } + + bool first_color = true; + for (std::size_t c = 0; c < palette.size(); ++c) { + if (!used[c]) continue; + if (!first_color) char_out('$'); + first_color = false; + char_out('#'); + emit_decimal(char_out, static_cast(c)); + + auto bits_at = [&](std::size_t x) -> std::uint8_t { + std::uint8_t bits = 0; + for (std::size_t y6 = 0; y6 < 6; ++y6) { + const std::size_t yy = y + y6; + if (yy >= h) break; + if (indexed[yy * w + x] == static_cast(c)) { + bits = static_cast(bits | (1u << y6)); + } + } + return bits; + }; + + std::size_t x = 0; + while (x < w) { + const std::uint8_t bits6 = bits_at(x); + std::size_t run = 0; + while (x + 1 + run < w && bits_at(x + 1 + run) == bits6 && run < 254) ++run; + if (run > 3) { + char_out('!'); + emit_decimal(char_out, static_cast(run + 1)); + x += run; + } + char_out(static_cast('?' + bits6)); + ++x; + } + } + char_out('-'); + } + + char_out(static_cast(0x1b)); + char_out('\\'); +} + +} // namespace runtime_sixel_detail + +template +inline void rgba_to_sixel(std::span rgba, + std::size_t w, std::size_t h, + std::span> palette, + F &&char_out) { + if (palette.empty() || palette.size() > 256) return; + if (rgba.size() < w * h * 4) return; + std::vector indexed(w * h); + for (std::size_t y = 0; y < h; ++y) { + for (std::size_t x = 0; x < w; ++x) { + const std::uint8_t *px = &rgba[(y * w + x) * 4]; + indexed[y * w + x] = runtime_sixel_detail::nearest_in_palette(px[0], px[1], px[2], palette); + } + } + runtime_sixel_detail::encode_indexed(indexed.data(), w, h, palette, std::forward(char_out)); +} + +template +inline void rgba_to_sixel(std::span rgba, + std::size_t w, std::size_t h, + F &&char_out) { + // Pull constixel's built-in 256-color palette via any format_8bit + // instantiation — quant is static constexpr and W/H-independent. + // Internal pal[] storage is byte-0=R, byte-1=G, byte-2=B (post-swap + // in hidden::quantize ctor), so we read it directly. + using pal_t = format_8bit<1, 1, false, false>; + constexpr const auto &raw = pal_t::quant.palette(); + std::array, 256> pal{}; + for (std::size_t i = 0; i < 256; ++i) { + pal[i][0] = static_cast((raw[i] >> 0) & 0xFF); + pal[i][1] = static_cast((raw[i] >> 8) & 0xFF); + pal[i][2] = static_cast((raw[i] >> 16) & 0xFF); + } + rgba_to_sixel(rgba, w, h, + std::span>(pal), + std::forward(char_out)); +} + } // namespace constixel #endif // CONSTIXEL_HPP_ From 397659d9bf10061d408cdc25114c275b7f7ed434 Mon Sep 17 00:00:00 2001 From: Tinic Uro Date: Fri, 1 May 2026 16:13:16 -0700 Subject: [PATCH 2/3] Rework runtime sixel API to fit constixel architecture. Replaces the freestanding rgba_to_sixel free functions with a proper sibling of image: - runtime_palette: shared <=256-entry palette type (BGR-packed). - format_8bit_dyn: 8-bit indexed format with runtime W/H carried by parameter (vs format_8bit's compile-time W/H). Default palette delegates to format_8bit::quant; custom palette uses runtime_palette. - dynamic_image: runtime-sized image, mirrors image surface (blit_RGBA, sixel, sixel_to_cout under CONSTIXEL_ENABLE_COUT). Custom palette also wired into the existing static image via new constructor overloads that take std::span>, gated on the format exposing a runtime_palette typedef. blit_RGBA and sixel members dispatch through if-constexpr to *_with_palette helpers when has_custom_palette_ is set; the default code path is unchanged. format_8bit gains sixel_with_palette and blit_RGBA_with_palette static methods that delegate to format_8bit_dyn (defined out-of-line to break the circular header dependency). No duplication of the sixel encoder between compile-time and runtime image types. --- constixel.hpp | 391 +++++++++++++++++++++++++++++--------------------- 1 file changed, 228 insertions(+), 163 deletions(-) diff --git a/constixel.hpp b/constixel.hpp index 95bb3fa..fe04a06 100644 --- a/constixel.hpp +++ b/constixel.hpp @@ -1877,6 +1877,37 @@ class format_4bit : public format { /// @endcond }; +/// @cond DOXYGEN_EXCLUDE +class format_8bit_dyn; +/// Runtime palette (<=256 sRGB entries, BGR-packed). Shared by +/// image and dynamic_image. +struct runtime_palette { + std::array pal{}; + size_t count_{0}; + constexpr runtime_palette() = default; + explicit runtime_palette(std::span> rgb) { + count_ = std::min(rgb.size(), size_t{256}); + for (size_t i = 0; i < count_; ++i) { + pal[i] = uint32_t(rgb[i][0]) | (uint32_t(rgb[i][1]) << 8) | (uint32_t(rgb[i][2]) << 16); + } + } + [[nodiscard]] size_t size() const noexcept { return count_; } + [[nodiscard]] uint32_t at(size_t i) const noexcept { return pal[i]; } + [[nodiscard]] uint8_t nearest(int32_t r, int32_t g, int32_t b) const { + int32_t best_d = std::numeric_limits::max(); + uint8_t best_i = 0; + for (size_t i = 0; i < count_; ++i) { + const int32_t dr = r - int32_t((pal[i] >> 0) & 0xFF); + const int32_t dg = g - int32_t((pal[i] >> 8) & 0xFF); + const int32_t db = b - int32_t((pal[i] >> 16) & 0xFF); + const int32_t d = dr*dr + dg*dg + db*db; + if (d < best_d) { best_d = d; best_i = uint8_t(i); if (d == 0) break; } + } + return best_i; + } +}; +/// @endcond + /** * @brief 8-bit format, 256 colors total. Use as template parameter for image. Example: * @@ -1892,6 +1923,8 @@ template class format_8bit : public format { public: /// @cond DOXYGEN_EXCLUDE + using runtime_palette = ::constixel::runtime_palette; + static constexpr size_t sixel_bitset_size = 256; static constexpr size_t bits_per_pixel = 8; static constexpr size_t bytes_per_line = W; @@ -2181,6 +2214,14 @@ class format_8bit : public format { } }); } + + template + static void sixel_with_palette(std::span data, + const runtime_palette &rp, F &&char_out); + static void blit_RGBA_with_palette(std::span data, + const rect &r, + const uint8_t *ptr, int32_t stride, + const runtime_palette &rp); /// @endcond }; @@ -3569,6 +3610,23 @@ class image { : data(other) { } + /** + * \brief Creates an image that uses a caller-supplied runtime palette + * instead of the format's compile-time-baked palette. + * \param palette Up to 256 sRGB byte triples. + */ + explicit image(std::span> palette) + requires(!USE_SPAN && requires { typename T::runtime_palette; }) + : palette_(palette), has_custom_palette_(true) {} + + /** + * \brief USE_SPAN + custom-palette variant. + */ + image(const std::span::image_size> &other, + std::span> palette) + requires(USE_SPAN && requires { typename T::runtime_palette; }) + : data(other), palette_(palette), has_custom_palette_(true) {} + /** * \brief Boolean indicating that the palette is grayscale instead of color. * \return If true, the palette is grayscale. If false a colored palette is used. @@ -4987,6 +5045,12 @@ class image { constixel::rect blitrect{.x = x, .y = y, .w = w, .h = h}; blitrect &= {.x = 0, .y = 0, .w = W, .h = H}; blitrect &= {.x = x, .y = y, .w = iw, .h = ih}; + if constexpr (requires { typename T::runtime_palette; }) { + if (has_custom_palette_) { + T::blit_RGBA_with_palette(data, blitrect, ptr, stride, palette_); + return; + } + } T::blit_RGBA(data, blitrect, ptr, stride); } @@ -5114,6 +5178,12 @@ class image { */ template constexpr void sixel(F &&char_out) const { + if constexpr (requires { typename T::runtime_palette; }) { + if (has_custom_palette_) { + T::sixel_with_palette(data, palette_, std::forward(char_out)); + return; + } + } T::template sixel(data, std::forward(char_out), {0, 0, W, H}); } @@ -5133,6 +5203,12 @@ class image { */ template constexpr void sixel(F &&char_out, const rect &rect) const { + if constexpr (requires { typename T::runtime_palette; }) { + if (has_custom_palette_) { + T::sixel_with_palette(data, palette_, std::forward(char_out)); + return; + } + } T::template sixel(data, std::forward(char_out), rect); } @@ -6255,186 +6331,175 @@ class image { */ T format{}; + /** + * @private + */ + runtime_palette palette_{}; + + /** + * @private + */ + bool has_custom_palette_{false}; + /// @endcond // DOXYGEN_EXCLUDE #endif // #ifndef __INTELLISENSE__ }; -// --------------------------------------------------------------------------- -// Runtime-sized sixel output -// --------------------------------------------------------------------------- -// -// The image<> template requires compile-time W and H, which is awkward for -// callers that produce arbitrary-resolution frames at runtime (terminal -// previews, decoded video, etc.). The free functions below take RGBA bytes -// plus runtime width/height and emit a sixel stream via a char-callback, -// mirroring the per-format image::sixel(F&&) entry point but without the -// compile-time-size constraint. -// -// Two overloads: -// * rgba_to_sixel(rgba, w, h, char_out) -// Quantises to constixel's built-in 256-color palette -// (format_8bit::quant). Use when input has more than 256 distinct -// colors or when the caller has no preferred palette. -// * rgba_to_sixel(rgba, w, h, palette, char_out) -// Uses a caller-supplied palette of <= 256 sRGB byte triples. The -// built-in quantiser is bypassed; each pixel maps to its -// squared-sRGB-distance nearest entry. Use when the caller already -// knows the exact palette of the source data. - -namespace runtime_sixel_detail { - -inline void emit_decimal(auto &&char_out, std::uint16_t u) { - if (u < 10) { - char_out(static_cast('0' + u)); - return; - } - char buf[6]; - int n = 0; - while (u > 0) { - buf[n++] = static_cast('0' + (u % 10)); - u = static_cast(u / 10); - } - while (n--) char_out(buf[n]); -} - -inline std::uint8_t nearest_in_palette(std::uint8_t r, std::uint8_t g, std::uint8_t b, - std::span> palette) { - int best_d = std::numeric_limits::max(); - std::uint8_t best_i = 0; - for (std::size_t i = 0; i < palette.size(); ++i) { - const int dr = static_cast(r) - static_cast(palette[i][0]); - const int dg = static_cast(g) - static_cast(palette[i][1]); - const int db = static_cast(b) - static_cast(palette[i][2]); - const int d = dr * dr + dg * dg + db * db; - if (d < best_d) { - best_d = d; - best_i = static_cast(i); - if (d == 0) break; - } - } - return best_i; -} - -template -inline void encode_indexed(const std::uint8_t *indexed, std::size_t w, std::size_t h, - std::span> palette, - F &&char_out) { - char_out(static_cast(0x1b)); - char_out('P'); - char_out('q'); - - char_out('"'); - emit_decimal(char_out, 1); - char_out(';'); - emit_decimal(char_out, 1); - char_out(';'); - emit_decimal(char_out, static_cast(w)); - char_out(';'); - emit_decimal(char_out, static_cast(h)); - - for (std::size_t c = 0; c < palette.size(); ++c) { - char_out('#'); - emit_decimal(char_out, static_cast(c)); - char_out(';'); - char_out('2'); - char_out(';'); - emit_decimal(char_out, static_cast((static_cast(palette[c][0]) * 100) / 255)); - char_out(';'); - emit_decimal(char_out, static_cast((static_cast(palette[c][1]) * 100) / 255)); - char_out(';'); - emit_decimal(char_out, static_cast((static_cast(palette[c][2]) * 100) / 255)); - } - - std::array used{}; - for (std::size_t y = 0; y < h; y += 6) { - used.fill(false); - const std::size_t y_end = std::min(y + 6, h); - for (std::size_t yy = y; yy < y_end; ++yy) { - for (std::size_t x = 0; x < w; ++x) { - used[indexed[yy * w + x]] = true; +/** + * @brief 8-bit indexed format for runtime-sized images. Use as Ops parameter + * for dynamic_image. Same palette / sixel semantics as format_8bit + * but with W and H carried at runtime. + */ +class format_8bit_dyn : public format { + public: + /// @cond DOXYGEN_EXCLUDE + static constexpr size_t bits_per_pixel = 8; + static constexpr size_t sixel_bitset_size = 256; + static constexpr size_t bytes_per_line(size_t w) { return w; } + static constexpr size_t image_size(size_t w, size_t h) { return bytes_per_line(w) * h; } + using runtime_palette = ::constixel::runtime_palette; + + static uint8_t nearest(int32_t r, int32_t g, int32_t b, const runtime_palette *rp) { + return rp ? rp->nearest(r, g, b) + : format_8bit<1, 1, false, false>::quant.nearest(r, g, b); + } + static size_t palette_size(const runtime_palette *rp) { return rp ? rp->size() : 256; } + static uint32_t palette_at(size_t c, const runtime_palette *rp) { + return rp ? rp->at(c) : format_8bit<1, 1, false, false>::quant.palette()[c]; + } + + static void blit_RGBA(uint8_t *dst, size_t dst_w, size_t dst_h, + const rect &r, const uint8_t *src, int32_t stride, + const runtime_palette *rp) { + rect ir{.x = 0, .y = 0, .w = int32_t(dst_w), .h = int32_t(dst_h)}; + ir &= r; + if (ir.w <= 0 || ir.h <= 0) return; + const auto rx = size_t(ir.x), ry = size_t(ir.y); + const auto rw = size_t(ir.w), rh = size_t(ir.h); + for (size_t y = 0; y < rh; ++y) { + for (size_t x = 0; x < rw; ++x) { + const uint8_t *px = &src[y * size_t(stride) + x * 4]; + dst[(ry + y) * dst_w + (rx + x)] = nearest(px[0], px[1], px[2], rp); } } + } - bool first_color = true; - for (std::size_t c = 0; c < palette.size(); ++c) { - if (!used[c]) continue; - if (!first_color) char_out('$'); - first_color = false; - char_out('#'); - emit_decimal(char_out, static_cast(c)); - - auto bits_at = [&](std::size_t x) -> std::uint8_t { - std::uint8_t bits = 0; - for (std::size_t y6 = 0; y6 < 6; ++y6) { - const std::size_t yy = y + y6; - if (yy >= h) break; - if (indexed[yy * w + x] == static_cast(c)) { - bits = static_cast(bits | (1u << y6)); + template + static void sixel(const uint8_t *data, size_t w, size_t h, + const runtime_palette *rp, F &&char_out) { + sixel_header(std::forward(char_out)); + std::forward(char_out)('"'); + sixel_number(std::forward(char_out), 1); std::forward(char_out)(';'); + sixel_number(std::forward(char_out), 1); std::forward(char_out)(';'); + sixel_number(std::forward(char_out), uint16_t(w)); std::forward(char_out)(';'); + sixel_number(std::forward(char_out), uint16_t(h)); + const size_t pal_n = palette_size(rp); + for (size_t c = 0; c < pal_n; ++c) { + sixel_color(std::forward(char_out), uint16_t(c), palette_at(c, rp)); + } + palette_bitset pset{}; + std::array stack{}; + for (size_t y = 0; y < h; y += 6) { + pset.clear(); + const size_t y_end = std::min(y + 6, h); + for (size_t yy = y; yy < y_end; ++yy) + for (size_t x = 0; x < w; ++x) pset.mark(data[yy * w + x]); + const size_t stack_count = pset.genstack(stack); + for (size_t s = 0; s < stack_count; ++s) { + const uint8_t col = stack[s]; + if (col != 0) std::forward(char_out)('$'); + std::forward(char_out)('#'); + sixel_number(std::forward(char_out), uint16_t(col)); + auto bits_at = [&](size_t x) -> uint8_t { + uint8_t bits = 0; + for (size_t y6 = 0; y6 < 6; ++y6) { + const size_t yy = y + y6; + if (yy >= h) break; + if (data[yy * w + x] == col) bits = uint8_t(bits | (1u << y6)); } + return bits; + }; + size_t x = 0; + while (x < w) { + const uint8_t bits6 = bits_at(x); + size_t run = 0; + while (x + 1 + run < w && bits_at(x + 1 + run) == bits6 && run < 254) ++run; + if (run > 3) { + std::forward(char_out)('!'); + sixel_number(std::forward(char_out), uint16_t(run + 1)); + x += run; + } + std::forward(char_out)(char('?' + bits6)); + ++x; } - return bits; - }; - - std::size_t x = 0; - while (x < w) { - const std::uint8_t bits6 = bits_at(x); - std::size_t run = 0; - while (x + 1 + run < w && bits_at(x + 1 + run) == bits6 && run < 254) ++run; - if (run > 3) { - char_out('!'); - emit_decimal(char_out, static_cast(run + 1)); - x += run; - } - char_out(static_cast('?' + bits6)); - ++x; } + std::forward(char_out)('-'); } - char_out('-'); + sixel_end(std::forward(char_out)); } + /// @endcond +}; - char_out(static_cast(0x1b)); - char_out('\\'); +/// @cond DOXYGEN_EXCLUDE +template +template +void format_8bit::sixel_with_palette( + std::span::image_size> data, + const runtime_palette &rp, F &&char_out) { + format_8bit_dyn::sixel(data.data(), W, H, &rp, std::forward(char_out)); +} +template +void format_8bit::blit_RGBA_with_palette( + std::span::image_size> data, + const rect &r, const uint8_t *ptr, int32_t stride, + const runtime_palette &rp) { + format_8bit_dyn::blit_RGBA(data.data(), W, H, r, ptr, stride, &rp); } +/// @endcond -} // namespace runtime_sixel_detail +/** + * @brief Runtime-sized indexed image. Sibling of image for callers + * that don't know dimensions at compile time. Same operation surface. + * @tparam Ops Runtime-format ops class (e.g. format_8bit_dyn). + */ +template +class dynamic_image { + public: + dynamic_image(size_t w, size_t h) + : data_(Ops::image_size(w, h), uint8_t{0}), w_(w), h_(h) {} + dynamic_image(size_t w, size_t h, std::span> palette) + : data_(Ops::image_size(w, h), uint8_t{0}), w_(w), h_(h), + palette_(palette), has_custom_palette_(true) {} -template -inline void rgba_to_sixel(std::span rgba, - std::size_t w, std::size_t h, - std::span> palette, - F &&char_out) { - if (palette.empty() || palette.size() > 256) return; - if (rgba.size() < w * h * 4) return; - std::vector indexed(w * h); - for (std::size_t y = 0; y < h; ++y) { - for (std::size_t x = 0; x < w; ++x) { - const std::uint8_t *px = &rgba[(y * w + x) * 4]; - indexed[y * w + x] = runtime_sixel_detail::nearest_in_palette(px[0], px[1], px[2], palette); - } - } - runtime_sixel_detail::encode_indexed(indexed.data(), w, h, palette, std::forward(char_out)); -} + [[nodiscard]] size_t width() const noexcept { return w_; } + [[nodiscard]] size_t height() const noexcept { return h_; } -template -inline void rgba_to_sixel(std::span rgba, - std::size_t w, std::size_t h, - F &&char_out) { - // Pull constixel's built-in 256-color palette via any format_8bit - // instantiation — quant is static constexpr and W/H-independent. - // Internal pal[] storage is byte-0=R, byte-1=G, byte-2=B (post-swap - // in hidden::quantize ctor), so we read it directly. - using pal_t = format_8bit<1, 1, false, false>; - constexpr const auto &raw = pal_t::quant.palette(); - std::array, 256> pal{}; - for (std::size_t i = 0; i < 256; ++i) { - pal[i][0] = static_cast((raw[i] >> 0) & 0xFF); - pal[i][1] = static_cast((raw[i] >> 8) & 0xFF); - pal[i][2] = static_cast((raw[i] >> 16) & 0xFF); - } - rgba_to_sixel(rgba, w, h, - std::span>(pal), - std::forward(char_out)); -} + void blit_RGBA(int32_t x, int32_t y, int32_t w, int32_t h, + const uint8_t *ptr, int32_t /*iw*/, int32_t /*ih*/, int32_t stride) { + Ops::blit_RGBA(data_.data(), w_, h_, {x, y, w, h}, ptr, stride, + has_custom_palette_ ? &palette_ : nullptr); + } + + template + void sixel(F &&char_out) const { + Ops::sixel(data_.data(), w_, h_, + has_custom_palette_ ? &palette_ : nullptr, + std::forward(char_out)); + } + +#ifdef CONSTIXEL_ENABLE_COUT + void sixel_to_cout() const { + sixel([](char ch) { std::cout.put(ch); }); + std::cout << '\n'; + } +#endif + + private: + std::vector data_; + size_t w_, h_; + typename Ops::runtime_palette palette_{}; + bool has_custom_palette_{false}; +}; } // namespace constixel From aba610b65690fd0949761731e1ca88ab568f17b4 Mon Sep 17 00:00:00 2001 From: Tinic Uro Date: Fri, 1 May 2026 16:28:35 -0700 Subject: [PATCH 3/3] Sixel: emit \033P;1q (P2=1) to avoid Windows Terminal #17887. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Microsoft Terminal #17887: when P2 is 0 or omitted in the sixel introducer, an arbitrary number of cell-height rows past the image get filled with an arbitrary color (uninitialized memory). Setting P2=1 marks unset pixels as transparent and sidesteps the bug. For fully-painted images (every pixel covered, as dynamic_image produces) P2=0 vs P2=1 are visually identical — but only P2=1 renders correctly under WT >=1.22 sixel. --- constixel.hpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/constixel.hpp b/constixel.hpp index fe04a06..9ef587f 100644 --- a/constixel.hpp +++ b/constixel.hpp @@ -6387,7 +6387,14 @@ class format_8bit_dyn : public format { template static void sixel(const uint8_t *data, size_t w, size_t h, const runtime_palette *rp, F &&char_out) { - sixel_header(std::forward(char_out)); + // \033P;1q — P2=1 (unset pixels transparent). Avoids Microsoft + // Terminal #17887 where omitted P2 fills cell-padding rows with + // arbitrary color. Visually equivalent for fully-painted images. + std::forward(char_out)(char(0x1b)); + std::forward(char_out)('P'); + std::forward(char_out)(';'); + std::forward(char_out)('1'); + std::forward(char_out)('q'); std::forward(char_out)('"'); sixel_number(std::forward(char_out), 1); std::forward(char_out)(';'); sixel_number(std::forward(char_out), 1); std::forward(char_out)(';');