diff --git a/constixel.hpp b/constixel.hpp index 6cb6f5a..9ef587f 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,10 +6331,183 @@ class image { */ T format{}; + /** + * @private + */ + runtime_palette palette_{}; + + /** + * @private + */ + bool has_custom_palette_{false}; + /// @endcond // DOXYGEN_EXCLUDE #endif // #ifndef __INTELLISENSE__ }; +/** + * @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); + } + } + } + + template + static void sixel(const uint8_t *data, size_t w, size_t h, + const runtime_palette *rp, F &&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)(';'); + 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; + } + } + std::forward(char_out)('-'); + } + sixel_end(std::forward(char_out)); + } + /// @endcond +}; + +/// @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 + +/** + * @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) {} + + [[nodiscard]] size_t width() const noexcept { return w_; } + [[nodiscard]] size_t height() const noexcept { return h_; } + + 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 #endif // CONSTIXEL_HPP_