Skip to content

Commit 7be545b

Browse files
authored
Merge pull request #2 from tinic/add-image-to-sixel-api
Add dynamic image to sixel api
2 parents 4910b63 + aba610b commit 7be545b

1 file changed

Lines changed: 249 additions & 0 deletions

File tree

constixel.hpp

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1877,6 +1877,37 @@ class format_4bit : public format {
18771877
/// @endcond
18781878
};
18791879

1880+
/// @cond DOXYGEN_EXCLUDE
1881+
class format_8bit_dyn;
1882+
/// Runtime palette (<=256 sRGB entries, BGR-packed). Shared by
1883+
/// image<format_8bit,...> and dynamic_image<format_8bit_dyn>.
1884+
struct runtime_palette {
1885+
std::array<uint32_t, 256> pal{};
1886+
size_t count_{0};
1887+
constexpr runtime_palette() = default;
1888+
explicit runtime_palette(std::span<const std::array<uint8_t, 3>> rgb) {
1889+
count_ = std::min(rgb.size(), size_t{256});
1890+
for (size_t i = 0; i < count_; ++i) {
1891+
pal[i] = uint32_t(rgb[i][0]) | (uint32_t(rgb[i][1]) << 8) | (uint32_t(rgb[i][2]) << 16);
1892+
}
1893+
}
1894+
[[nodiscard]] size_t size() const noexcept { return count_; }
1895+
[[nodiscard]] uint32_t at(size_t i) const noexcept { return pal[i]; }
1896+
[[nodiscard]] uint8_t nearest(int32_t r, int32_t g, int32_t b) const {
1897+
int32_t best_d = std::numeric_limits<int32_t>::max();
1898+
uint8_t best_i = 0;
1899+
for (size_t i = 0; i < count_; ++i) {
1900+
const int32_t dr = r - int32_t((pal[i] >> 0) & 0xFF);
1901+
const int32_t dg = g - int32_t((pal[i] >> 8) & 0xFF);
1902+
const int32_t db = b - int32_t((pal[i] >> 16) & 0xFF);
1903+
const int32_t d = dr*dr + dg*dg + db*db;
1904+
if (d < best_d) { best_d = d; best_i = uint8_t(i); if (d == 0) break; }
1905+
}
1906+
return best_i;
1907+
}
1908+
};
1909+
/// @endcond
1910+
18801911
/**
18811912
* @brief 8-bit format, 256 colors total. Use as template parameter for image. Example:
18821913
*
@@ -1892,6 +1923,8 @@ template <size_t W, size_t H, bool GRAYSCALE, bool USE_SPAN>
18921923
class format_8bit : public format {
18931924
public:
18941925
/// @cond DOXYGEN_EXCLUDE
1926+
using runtime_palette = ::constixel::runtime_palette;
1927+
18951928
static constexpr size_t sixel_bitset_size = 256;
18961929
static constexpr size_t bits_per_pixel = 8;
18971930
static constexpr size_t bytes_per_line = W;
@@ -2181,6 +2214,14 @@ class format_8bit : public format {
21812214
}
21822215
});
21832216
}
2217+
2218+
template <typename F>
2219+
static void sixel_with_palette(std::span<const uint8_t, image_size> data,
2220+
const runtime_palette &rp, F &&char_out);
2221+
static void blit_RGBA_with_palette(std::span<uint8_t, image_size> data,
2222+
const rect<int32_t> &r,
2223+
const uint8_t *ptr, int32_t stride,
2224+
const runtime_palette &rp);
21842225
/// @endcond
21852226
};
21862227

@@ -3569,6 +3610,23 @@ class image {
35693610
: data(other) {
35703611
}
35713612

3613+
/**
3614+
* \brief Creates an image that uses a caller-supplied runtime palette
3615+
* instead of the format's compile-time-baked palette.
3616+
* \param palette Up to 256 sRGB byte triples.
3617+
*/
3618+
explicit image(std::span<const std::array<uint8_t, 3>> palette)
3619+
requires(!USE_SPAN && requires { typename T<W, H, GRAYSCALE, USE_SPAN>::runtime_palette; })
3620+
: palette_(palette), has_custom_palette_(true) {}
3621+
3622+
/**
3623+
* \brief USE_SPAN + custom-palette variant.
3624+
*/
3625+
image(const std::span<uint8_t, T<W, H, GRAYSCALE, USE_SPAN>::image_size> &other,
3626+
std::span<const std::array<uint8_t, 3>> palette)
3627+
requires(USE_SPAN && requires { typename T<W, H, GRAYSCALE, USE_SPAN>::runtime_palette; })
3628+
: data(other), palette_(palette), has_custom_palette_(true) {}
3629+
35723630
/**
35733631
* \brief Boolean indicating that the palette is grayscale instead of color.
35743632
* \return If true, the palette is grayscale. If false a colored palette is used.
@@ -4987,6 +5045,12 @@ class image {
49875045
constixel::rect<int32_t> blitrect{.x = x, .y = y, .w = w, .h = h};
49885046
blitrect &= {.x = 0, .y = 0, .w = W, .h = H};
49895047
blitrect &= {.x = x, .y = y, .w = iw, .h = ih};
5048+
if constexpr (requires { typename T<W, H, GRAYSCALE, USE_SPAN>::runtime_palette; }) {
5049+
if (has_custom_palette_) {
5050+
T<W, H, GRAYSCALE, USE_SPAN>::blit_RGBA_with_palette(data, blitrect, ptr, stride, palette_);
5051+
return;
5052+
}
5053+
}
49905054
T<W, H, GRAYSCALE, USE_SPAN>::blit_RGBA(data, blitrect, ptr, stride);
49915055
}
49925056

@@ -5114,6 +5178,12 @@ class image {
51145178
*/
51155179
template <size_t S = 1, typename F>
51165180
constexpr void sixel(F &&char_out) const {
5181+
if constexpr (requires { typename T<W, H, GRAYSCALE, USE_SPAN>::runtime_palette; }) {
5182+
if (has_custom_palette_) {
5183+
T<W, H, GRAYSCALE, USE_SPAN>::sixel_with_palette(data, palette_, std::forward<F>(char_out));
5184+
return;
5185+
}
5186+
}
51175187
T<W, H, GRAYSCALE, USE_SPAN>::template sixel<S>(data, std::forward<F>(char_out), {0, 0, W, H});
51185188
}
51195189

@@ -5133,6 +5203,12 @@ class image {
51335203
*/
51345204
template <size_t S = 1, typename F>
51355205
constexpr void sixel(F &&char_out, const rect<int32_t> &rect) const {
5206+
if constexpr (requires { typename T<W, H, GRAYSCALE, USE_SPAN>::runtime_palette; }) {
5207+
if (has_custom_palette_) {
5208+
T<W, H, GRAYSCALE, USE_SPAN>::sixel_with_palette(data, palette_, std::forward<F>(char_out));
5209+
return;
5210+
}
5211+
}
51365212
T<W, H, GRAYSCALE, USE_SPAN>::template sixel<S>(data, std::forward<F>(char_out), rect);
51375213
}
51385214

@@ -6255,10 +6331,183 @@ class image {
62556331
*/
62566332
T<W, H, GRAYSCALE, USE_SPAN> format{};
62576333

6334+
/**
6335+
* @private
6336+
*/
6337+
runtime_palette palette_{};
6338+
6339+
/**
6340+
* @private
6341+
*/
6342+
bool has_custom_palette_{false};
6343+
62586344
/// @endcond // DOXYGEN_EXCLUDE
62596345
#endif // #ifndef __INTELLISENSE__
62606346
};
62616347

6348+
/**
6349+
* @brief 8-bit indexed format for runtime-sized images. Use as Ops parameter
6350+
* for dynamic_image. Same palette / sixel semantics as format_8bit
6351+
* but with W and H carried at runtime.
6352+
*/
6353+
class format_8bit_dyn : public format {
6354+
public:
6355+
/// @cond DOXYGEN_EXCLUDE
6356+
static constexpr size_t bits_per_pixel = 8;
6357+
static constexpr size_t sixel_bitset_size = 256;
6358+
static constexpr size_t bytes_per_line(size_t w) { return w; }
6359+
static constexpr size_t image_size(size_t w, size_t h) { return bytes_per_line(w) * h; }
6360+
using runtime_palette = ::constixel::runtime_palette;
6361+
6362+
static uint8_t nearest(int32_t r, int32_t g, int32_t b, const runtime_palette *rp) {
6363+
return rp ? rp->nearest(r, g, b)
6364+
: format_8bit<1, 1, false, false>::quant.nearest(r, g, b);
6365+
}
6366+
static size_t palette_size(const runtime_palette *rp) { return rp ? rp->size() : 256; }
6367+
static uint32_t palette_at(size_t c, const runtime_palette *rp) {
6368+
return rp ? rp->at(c) : format_8bit<1, 1, false, false>::quant.palette()[c];
6369+
}
6370+
6371+
static void blit_RGBA(uint8_t *dst, size_t dst_w, size_t dst_h,
6372+
const rect<int32_t> &r, const uint8_t *src, int32_t stride,
6373+
const runtime_palette *rp) {
6374+
rect<int32_t> ir{.x = 0, .y = 0, .w = int32_t(dst_w), .h = int32_t(dst_h)};
6375+
ir &= r;
6376+
if (ir.w <= 0 || ir.h <= 0) return;
6377+
const auto rx = size_t(ir.x), ry = size_t(ir.y);
6378+
const auto rw = size_t(ir.w), rh = size_t(ir.h);
6379+
for (size_t y = 0; y < rh; ++y) {
6380+
for (size_t x = 0; x < rw; ++x) {
6381+
const uint8_t *px = &src[y * size_t(stride) + x * 4];
6382+
dst[(ry + y) * dst_w + (rx + x)] = nearest(px[0], px[1], px[2], rp);
6383+
}
6384+
}
6385+
}
6386+
6387+
template <typename F>
6388+
static void sixel(const uint8_t *data, size_t w, size_t h,
6389+
const runtime_palette *rp, F &&char_out) {
6390+
// \033P;1q — P2=1 (unset pixels transparent). Avoids Microsoft
6391+
// Terminal #17887 where omitted P2 fills cell-padding rows with
6392+
// arbitrary color. Visually equivalent for fully-painted images.
6393+
std::forward<F>(char_out)(char(0x1b));
6394+
std::forward<F>(char_out)('P');
6395+
std::forward<F>(char_out)(';');
6396+
std::forward<F>(char_out)('1');
6397+
std::forward<F>(char_out)('q');
6398+
std::forward<F>(char_out)('"');
6399+
sixel_number(std::forward<F>(char_out), 1); std::forward<F>(char_out)(';');
6400+
sixel_number(std::forward<F>(char_out), 1); std::forward<F>(char_out)(';');
6401+
sixel_number(std::forward<F>(char_out), uint16_t(w)); std::forward<F>(char_out)(';');
6402+
sixel_number(std::forward<F>(char_out), uint16_t(h));
6403+
const size_t pal_n = palette_size(rp);
6404+
for (size_t c = 0; c < pal_n; ++c) {
6405+
sixel_color(std::forward<F>(char_out), uint16_t(c), palette_at(c, rp));
6406+
}
6407+
palette_bitset<uint8_t, sixel_bitset_size> pset{};
6408+
std::array<uint8_t, sixel_bitset_size> stack{};
6409+
for (size_t y = 0; y < h; y += 6) {
6410+
pset.clear();
6411+
const size_t y_end = std::min(y + 6, h);
6412+
for (size_t yy = y; yy < y_end; ++yy)
6413+
for (size_t x = 0; x < w; ++x) pset.mark(data[yy * w + x]);
6414+
const size_t stack_count = pset.genstack(stack);
6415+
for (size_t s = 0; s < stack_count; ++s) {
6416+
const uint8_t col = stack[s];
6417+
if (col != 0) std::forward<F>(char_out)('$');
6418+
std::forward<F>(char_out)('#');
6419+
sixel_number(std::forward<F>(char_out), uint16_t(col));
6420+
auto bits_at = [&](size_t x) -> uint8_t {
6421+
uint8_t bits = 0;
6422+
for (size_t y6 = 0; y6 < 6; ++y6) {
6423+
const size_t yy = y + y6;
6424+
if (yy >= h) break;
6425+
if (data[yy * w + x] == col) bits = uint8_t(bits | (1u << y6));
6426+
}
6427+
return bits;
6428+
};
6429+
size_t x = 0;
6430+
while (x < w) {
6431+
const uint8_t bits6 = bits_at(x);
6432+
size_t run = 0;
6433+
while (x + 1 + run < w && bits_at(x + 1 + run) == bits6 && run < 254) ++run;
6434+
if (run > 3) {
6435+
std::forward<F>(char_out)('!');
6436+
sixel_number(std::forward<F>(char_out), uint16_t(run + 1));
6437+
x += run;
6438+
}
6439+
std::forward<F>(char_out)(char('?' + bits6));
6440+
++x;
6441+
}
6442+
}
6443+
std::forward<F>(char_out)('-');
6444+
}
6445+
sixel_end(std::forward<F>(char_out));
6446+
}
6447+
/// @endcond
6448+
};
6449+
6450+
/// @cond DOXYGEN_EXCLUDE
6451+
template <size_t W, size_t H, bool GRAYSCALE, bool USE_SPAN>
6452+
template <typename F>
6453+
void format_8bit<W, H, GRAYSCALE, USE_SPAN>::sixel_with_palette(
6454+
std::span<const uint8_t, format_8bit<W, H, GRAYSCALE, USE_SPAN>::image_size> data,
6455+
const runtime_palette &rp, F &&char_out) {
6456+
format_8bit_dyn::sixel(data.data(), W, H, &rp, std::forward<F>(char_out));
6457+
}
6458+
template <size_t W, size_t H, bool GRAYSCALE, bool USE_SPAN>
6459+
void format_8bit<W, H, GRAYSCALE, USE_SPAN>::blit_RGBA_with_palette(
6460+
std::span<uint8_t, format_8bit<W, H, GRAYSCALE, USE_SPAN>::image_size> data,
6461+
const rect<int32_t> &r, const uint8_t *ptr, int32_t stride,
6462+
const runtime_palette &rp) {
6463+
format_8bit_dyn::blit_RGBA(data.data(), W, H, r, ptr, stride, &rp);
6464+
}
6465+
/// @endcond
6466+
6467+
/**
6468+
* @brief Runtime-sized indexed image. Sibling of image<F,W,H> for callers
6469+
* that don't know dimensions at compile time. Same operation surface.
6470+
* @tparam Ops Runtime-format ops class (e.g. format_8bit_dyn).
6471+
*/
6472+
template <typename Ops>
6473+
class dynamic_image {
6474+
public:
6475+
dynamic_image(size_t w, size_t h)
6476+
: data_(Ops::image_size(w, h), uint8_t{0}), w_(w), h_(h) {}
6477+
dynamic_image(size_t w, size_t h, std::span<const std::array<uint8_t, 3>> palette)
6478+
: data_(Ops::image_size(w, h), uint8_t{0}), w_(w), h_(h),
6479+
palette_(palette), has_custom_palette_(true) {}
6480+
6481+
[[nodiscard]] size_t width() const noexcept { return w_; }
6482+
[[nodiscard]] size_t height() const noexcept { return h_; }
6483+
6484+
void blit_RGBA(int32_t x, int32_t y, int32_t w, int32_t h,
6485+
const uint8_t *ptr, int32_t /*iw*/, int32_t /*ih*/, int32_t stride) {
6486+
Ops::blit_RGBA(data_.data(), w_, h_, {x, y, w, h}, ptr, stride,
6487+
has_custom_palette_ ? &palette_ : nullptr);
6488+
}
6489+
6490+
template <typename F>
6491+
void sixel(F &&char_out) const {
6492+
Ops::sixel(data_.data(), w_, h_,
6493+
has_custom_palette_ ? &palette_ : nullptr,
6494+
std::forward<F>(char_out));
6495+
}
6496+
6497+
#ifdef CONSTIXEL_ENABLE_COUT
6498+
void sixel_to_cout() const {
6499+
sixel([](char ch) { std::cout.put(ch); });
6500+
std::cout << '\n';
6501+
}
6502+
#endif
6503+
6504+
private:
6505+
std::vector<uint8_t> data_;
6506+
size_t w_, h_;
6507+
typename Ops::runtime_palette palette_{};
6508+
bool has_custom_palette_{false};
6509+
};
6510+
62626511
} // namespace constixel
62636512

62646513
#endif // CONSTIXEL_HPP_

0 commit comments

Comments
 (0)