#ifndef AMT_PIXEL_HPP #define AMT_PIXEL_HPP #include "matrix.hpp" #include #include #include #include #include #include #include #include namespace amt { enum class PixelFormat { rgba, abgr, rgb , bgr , ga , // gray scale and alpha ag , // alpha and gray scale g // gray scale }; inline static constexpr auto get_pixel_format_from_channel(std::size_t c, bool little_endian = false) -> PixelFormat { switch (c) { case 1: return PixelFormat::g; case 2: return little_endian ? PixelFormat::ag : PixelFormat::ga; case 3: return little_endian ? PixelFormat::bgr : PixelFormat::rgb; case 4: return little_endian ? PixelFormat::abgr : PixelFormat::abgr; } throw std::runtime_error(std::string("get_pixel_format_from_channel: unknown channel ") + std::to_string(c)); } namespace detail { static constexpr auto compare_float(float l, float r) noexcept -> bool { return std::abs(l - r) < std::numeric_limits::epsilon(); } } // namespace detail enum class BlendMode { normal, multiply, screen, overlay, darken, lighten, colorDodge, colorBurn, hardLight, softLight, difference, exclusion }; struct RGBA { using pixel_t = std::uint8_t; using pixels_t = std::uint32_t; constexpr RGBA() noexcept = default; constexpr RGBA(RGBA const&) noexcept = default; constexpr RGBA(RGBA &&) noexcept = default; constexpr RGBA& operator=(RGBA const&) noexcept = default; constexpr RGBA& operator=(RGBA &&) noexcept = default; constexpr ~RGBA() noexcept = default; // NOTE: Accepts RRGGBBAA explicit constexpr RGBA(pixels_t color) noexcept : RGBA((color >> (8 * 3)) & 0xff, (color >> (8 * 2)) & 0xff, (color >> (8 * 1)) & 0xff, (color >> (8 * 0)) & 0xff) {} constexpr RGBA(pixel_t r, pixel_t g, pixel_t b, pixel_t a = 0xff) noexcept : m_data {r, g, b, a} {} constexpr RGBA(pixel_t color, pixel_t a = 0xff) noexcept : RGBA(color, color, color, a) {} // NOTE: Returns RRGGBBAA constexpr auto to_hex() const noexcept -> pixels_t { auto r = static_cast(this->r()); auto b = static_cast(this->b()); auto g = static_cast(this->g()); auto a = static_cast(this->a()); return (r << (8 * 3)) | (g << (8 * 2)) | (b << (8 * 1)) | (a << (8 * 0)); } constexpr auto r() const noexcept -> pixel_t { return m_data[0]; } constexpr auto g() const noexcept -> pixel_t { return m_data[1]; } constexpr auto b() const noexcept -> pixel_t { return m_data[2]; } constexpr auto a() const noexcept -> pixel_t { return m_data[3]; } constexpr auto r() noexcept -> pixel_t& { return m_data[0]; } constexpr auto g() noexcept -> pixel_t& { return m_data[1]; } constexpr auto b() noexcept -> pixel_t& { return m_data[2]; } constexpr auto a() noexcept -> pixel_t& { return m_data[3]; } /** * @returns the value is between 0 and 1 */ constexpr auto brightness() const noexcept -> float { // 0.299*R + 0.587*G + 0.114*B auto tr = normalize(r()); auto tg = normalize(g()); auto tb = normalize(b()); return (0.299 * tr + 0.587 * tg + 0.114 * tb); } template requires std::is_arithmetic_v constexpr auto operator/(T val) const noexcept { auto d = static_cast(val); return RGBA( static_cast(r() / d), static_cast(g() / d), static_cast(b() / d), a() ); } template requires std::is_arithmetic_v constexpr auto operator*(T val) const noexcept { auto d = static_cast(val); return RGBA( static_cast(r() * d), static_cast(g() * d), static_cast(b() * d), a() ); } private: static constexpr auto normalize(pixel_t p) noexcept -> float { return float(p) / 255; } static constexpr auto to_pixel(float p) noexcept -> pixel_t { return static_cast(p * 255); } template static constexpr auto blend_helper() noexcept { constexpr auto mix_helper = [](float s, float b, float a) -> float { // (1 - αb) x Cs + αb x B(Cb, Cs) return (1 - a) * s + a * b; }; if constexpr (M == BlendMode::normal) { return [mix_helper](float bg, float fg, float alpha) { return mix_helper(bg, fg, alpha); }; } else if constexpr (M == BlendMode::multiply) { return [mix_helper](float bg, float fg, float alpha) { return mix_helper(bg, bg * fg, alpha); }; } else if constexpr (M == BlendMode::screen) { return [mix_helper](float bg, float fg, float alpha) { // Cb + Cs -(Cb x Cs) auto bf = bg + fg - (bg * fg); return mix_helper(bg, bf, alpha); }; } else if constexpr (M == BlendMode::overlay) { return [mix_helper](float bg, float fg, float alpha, auto&& hard_light_fn, auto&& multiply_fn, auto&& screen_fn) { // HardLight(Cs, Cb) auto hl = hard_light_fn(bg, fg, alpha, multiply_fn, screen_fn); return mix_helper(bg, hl, alpha); }; } else if constexpr (M == BlendMode::darken) { return [mix_helper](float bg, float fg, float alpha) { return mix_helper(bg, std::min(bg, fg), alpha); }; } else if constexpr (M == BlendMode::lighten) { return [mix_helper](float bg, float fg, float alpha) { return mix_helper(bg, std::max(bg, fg), alpha); }; } else if constexpr (M == BlendMode::colorDodge) { return [mix_helper](float bg, float fg, float alpha) { constexpr auto fn = [](float b, float s) -> float { if (b == 0) return 0; if (s == 255) return 255; return std::min(1.f, b / (1.f - s)); }; auto bf = fn(bg, fg); return mix_helper(bg, bf, alpha); }; } else if constexpr (M == BlendMode::colorBurn) { return [mix_helper](float bg, float fg, float alpha) { constexpr auto fn = [](float b, float s) -> float { if (b == 255) return 255; if (s == 0) return 0; return 1.f - std::min(1.f, (1.f - b) / s); }; auto bf = fn(bg, fg); return mix_helper(bg, bf, alpha); }; } else if constexpr (M == BlendMode::hardLight) { return [mix_helper](float bg, float fg, float alpha, auto&& multiply_fn, auto&& screen_fn) { auto fn = [&multiply_fn, &screen_fn](float b, float s, float a) -> float { if (s <= 0.5f) { return multiply_fn(b, 2.f * s, a); } else { return screen_fn(b, 2.f * s - 1.f, a); } }; auto bf = fn(bg, fg, alpha); return mix_helper(bg, bf, alpha); }; } else if constexpr (M == BlendMode::softLight) { return [mix_helper](float bg, float fg, float alpha) { constexpr auto fn = [](float b, float s) -> float { if (s <= 0.5f) { // B(Cb, Cs) = Cb - (1 - 2 x Cs) x Cb x (1 - Cb) return b - (1.f - 2.f * s) * b * (1 - b); } else { float d{}; if (b <= 0.5f) { // D(Cb) = ((16 * Cb - 12) x Cb + 4) x Cb d = ((16 * b - 12) * b + 4) * b; } else { // D(Cb) = sqrt(Cb) d = std::sqrt(b); } // B(Cb, Cs) = Cb + (2 x Cs - 1) x (D(Cb) - Cb) return b + (2 * s - 1) * (d - b); } }; auto bf = fn(bg, fg); return mix_helper(bg, bf, alpha); }; } else if constexpr (M == BlendMode::difference) { return [mix_helper](float bg, float fg, float alpha) { // B(Cb, Cs) = | Cb - Cs | return mix_helper(bg, (bg > fg ? (bg - fg) : (fg - bg)), alpha); }; } else if constexpr (M == BlendMode::exclusion) { return [mix_helper](float bg, float fg, float alpha) { constexpr auto fn = [](float b, float s) -> float { // B(Cb, Cs) = Cb + Cs - 2 x Cb x Cs return b + s - 2 * b * s; }; auto bf = fn(bg, fg); return mix_helper(bg, bf, alpha); }; } }; public: template constexpr auto blend(RGBA color) const noexcept -> RGBA { auto ab = normalize(a()); auto as = normalize(color.a()); // αs x 1 + αb x (1 – αs) auto alpha = to_pixel(as + ab * (1 - as)); auto lr = normalize(r()); auto lg = normalize(g()); auto lb = normalize(b()); auto rr = normalize(color.r()); auto rg = normalize(color.g()); auto rb = normalize(color.b()); auto nr = 0.f; auto ng = 0.f; auto nb = 0.f; if constexpr (M == BlendMode::normal) { auto fn = blend_helper(); nr = fn(lr, rr, ab); ng = fn(lg, rg, ab); nb = fn(lb, rb, ab); } else if constexpr (M == BlendMode::multiply) { auto fn = blend_helper(); nr = fn(lr, rr, ab); ng = fn(lg, rg, ab); nb = fn(lb, rb, ab); } else if constexpr (M == BlendMode::screen) { auto fn = blend_helper(); nr = fn(lr, rr, ab); ng = fn(lg, rg, ab); nb = fn(lb, rb, ab); } else if constexpr (M == BlendMode::overlay) { auto fn = blend_helper(); auto hard_light_fn = blend_helper(); auto multiply_fn = blend_helper(); auto screen_fn = blend_helper(); nr = fn(lr, rr, ab, hard_light_fn, multiply_fn, screen_fn); ng = fn(lg, rg, ab, hard_light_fn, multiply_fn, screen_fn); nb = fn(lb, rb, ab, hard_light_fn, multiply_fn, screen_fn); } else if constexpr (M == BlendMode::darken) { auto fn = blend_helper(); nr = fn(lr, rr, ab); ng = fn(lg, rg, ab); nb = fn(lb, rb, ab); } else if constexpr (M == BlendMode::lighten) { auto fn = blend_helper(); nr = fn(lr, rr, ab); ng = fn(lg, rg, ab); nb = fn(lb, rb, ab); } else if constexpr (M == BlendMode::colorDodge) { auto fn = blend_helper(); nr = fn(lr, rr, ab); ng = fn(lg, rg, ab); nb = fn(lb, rb, ab); } else if constexpr (M == BlendMode::colorBurn) { auto fn = blend_helper(); nr = fn(lr, rr, ab); ng = fn(lg, rg, ab); nb = fn(lb, rb, ab); } else if constexpr (M == BlendMode::hardLight) { auto fn = blend_helper(); auto multiply_fn = blend_helper(); auto screen_fn = blend_helper(); nr = fn(lr, rr, ab, multiply_fn, screen_fn); ng = fn(lg, rg, ab, multiply_fn, screen_fn); nb = fn(lb, rb, ab, multiply_fn, screen_fn); } else if constexpr (M == BlendMode::softLight) { auto fn = blend_helper(); nr = fn(lr, rr, ab); ng = fn(lg, rg, ab); nb = fn(lb, rb, ab); } else if constexpr (M == BlendMode::difference) { auto fn = blend_helper(); nr = fn(lr, rr, ab); ng = fn(lg, rg, ab); nb = fn(lb, rb, ab); } else if constexpr (M == BlendMode::exclusion) { auto fn = blend_helper(); nr = fn(lr, rr, ab); ng = fn(lg, rg, ab); nb = fn(lb, rb, ab); } return RGBA( to_pixel(nr), to_pixel(ng), to_pixel(nb), alpha ); } constexpr auto blend(RGBA color, BlendMode mode) const noexcept -> RGBA { switch (mode) { case BlendMode::normal: return blend(color); case BlendMode::multiply: return blend(color); case BlendMode::screen: return blend(color); case BlendMode::overlay: return blend(color); case BlendMode::darken: return blend(color); case BlendMode::lighten: return blend(color); case BlendMode::colorDodge: return blend(color); case BlendMode::colorBurn: return blend(color); case BlendMode::hardLight: return blend(color); case BlendMode::softLight: return blend(color); case BlendMode::difference: return blend(color); case BlendMode::exclusion: return blend(color); } } private: pixel_t m_data[4]{}; }; struct HSLA { using pixel_t = float; using pixels_t = float[4]; // ensures pixel to be in range template struct PixelWrapper { pixel_t& p; constexpr PixelWrapper& operator=(float val) noexcept { p = std::clamp(val, float(min), float(max)); } constexpr operator pixel_t() const noexcept { return p; } }; constexpr HSLA() noexcept = default; constexpr HSLA(HSLA const&) noexcept = default; constexpr HSLA(HSLA &&) noexcept = default; constexpr HSLA& operator=(HSLA const&) noexcept = default; constexpr HSLA& operator=(HSLA &&) noexcept = default; constexpr ~HSLA() noexcept = default; constexpr HSLA(pixel_t h, pixel_t s, pixel_t l, pixel_t a = 100) noexcept : m_data({ .hsla = { .h = h, .s = s, .l = l, .a = a } }) {} constexpr HSLA(RGBA color) noexcept { auto min = std::min({color.r(), color.g(), color.b()}); auto max = std::max({color.r(), color.g(), color.b()}); auto c = (max - min) / 255.f; auto tr = float(color.r()) / 255; auto tg = float(color.g()) / 255; auto tb = float(color.b()) / 255; auto ta = float(color.a()) / 255; float hue = 0; float s = 0; auto l = ((max + min) / 2.f) / 255.f; if (min == max) { if (max == color.r()) { auto seg = (tg - tb) / c; auto shift = (seg < 0 ? 360.f : 0.f) / 60.f; hue = seg + shift; } else if (max == color.g()) { auto seg = (tb - tr) / c; auto shift = 120.f / 60.f; hue = seg + shift; } else { auto seg = (tr - tg) / c; auto shift = 240.f / 60.f; hue = seg + shift; } s = c / (1 - std::abs(2 * l - 1)); } hue = hue * 60.f + 360.f; auto q = static_cast(static_cast(hue / 360.f)); hue -= q * 360.f; m_data.hsla.h = hue; m_data.hsla.s = s * 100.f; m_data.hsla.l = l * 100.f; m_data.hsla.a = ta * 100.f; } constexpr operator RGBA() const noexcept { auto ts = s() / 100.f; auto tl = l() / 100.f; auto ta = a() / 100.f; if (s() == 0) return RGBA(to_pixel(tl), to_pixel(ta)); auto th = h() / 360.f; float const q = tl < 0.5 ? tl * (1 + ts) : tl + ts - tl * ts; float const p = 2 * tl - q; return RGBA( to_pixel(convert_hue(p, q, th + 1.f / 3)), to_pixel(convert_hue(p, q, th)), to_pixel(convert_hue(p, q, th - 1.f / 3)), to_pixel(ta) ); } constexpr auto blend(HSLA color, BlendMode mode) const noexcept -> HSLA { auto lhs = RGBA(*this); auto rhs = RGBA(color); return HSLA(lhs.blend(rhs, mode)); } constexpr auto h() const noexcept -> pixel_t { return m_data.hsla.h; } constexpr auto s() const noexcept -> pixel_t { return m_data.hsla.s; } constexpr auto l() const noexcept -> pixel_t { return m_data.hsla.l; } constexpr auto a() const noexcept -> pixel_t { return m_data.hsla.a; } constexpr auto h() noexcept -> PixelWrapper<0, 360> { return { m_data.hsla.h }; } constexpr auto s() noexcept -> PixelWrapper<0, 100> { return { m_data.hsla.s }; } constexpr auto l() noexcept -> PixelWrapper<0, 100> { return { m_data.hsla.l }; } constexpr auto a() noexcept -> PixelWrapper<0, 100> { return { m_data.hsla.a }; } private: static constexpr auto to_pixel(float a) noexcept -> RGBA::pixel_t { return static_cast(a * 255); } static constexpr auto convert_hue(float p, float q, float t) noexcept -> float { t = t - (t > 1) + (t < 0); if (t * 6 < 1) return p + (q - p) * 6 * t; if (t * 2 < 1) return q; if (t * 3 < 2) return p + (q - p) * (2.f / 3 - t) * 6; return p; } private: union { struct { pixel_t h{}; // hue: 0-360 pixel_t s{}; // saturation: 0-100% pixel_t l{}; // lightness: 0-100% pixel_t a{}; // alpha: 0-100% } hsla; pixels_t color; } m_data{}; }; namespace detail { template inline static constexpr auto parse_pixel_helper(RGBA color, std::uint8_t* out_ptr) noexcept { if constexpr (F == PixelFormat::rgba) { out_ptr[0] = color.r(); out_ptr[1] = color.g(); out_ptr[2] = color.b(); out_ptr[3] = color.a(); } else if constexpr (F == PixelFormat::abgr) { out_ptr[0] = color.a(); out_ptr[1] = color.b(); out_ptr[2] = color.g(); out_ptr[3] = color.r(); } else if constexpr (F == PixelFormat::rgb) { out_ptr[0] = color.r(); out_ptr[1] = color.g(); out_ptr[2] = color.b(); } else if constexpr (F == PixelFormat::bgr) { out_ptr[0] = color.b(); out_ptr[1] = color.g(); out_ptr[2] = color.r(); } else if constexpr (F == PixelFormat::ga) { out_ptr[0] = color.r(); out_ptr[1] = color.a(); } else if constexpr (F == PixelFormat::ag) { out_ptr[0] = color.a(); out_ptr[1] = color.r(); } else { out_ptr[0] = color.r(); } } template inline static constexpr auto parse_pixel_helper(std::uint8_t const* in_ptr) noexcept -> RGBA { if constexpr (F == PixelFormat::rgba) { return { in_ptr[0], in_ptr[1], in_ptr[2], in_ptr[3] }; } else if constexpr (F == PixelFormat::abgr) { return { in_ptr[3], in_ptr[2], in_ptr[1], in_ptr[0] }; } else if constexpr (F == PixelFormat::rgb) { return { in_ptr[0], in_ptr[1], in_ptr[2], 0xff }; } else if constexpr (F == PixelFormat::bgr) { return { in_ptr[2], in_ptr[1], in_ptr[0], 0xff }; } else if constexpr (F == PixelFormat::ga) { return { in_ptr[0], in_ptr[1], }; } else if constexpr (F == PixelFormat::ag) { return { in_ptr[1], in_ptr[0], }; } else { return { in_ptr[0], 0xff }; } } } // namespace detail inline static constexpr auto get_pixel_format_channel(PixelFormat format) noexcept -> std::size_t { switch (format) { case PixelFormat::rgba: case PixelFormat::abgr: return 4u; case PixelFormat::rgb: case PixelFormat::bgr: return 3u; case PixelFormat::ga: case PixelFormat::ag: return 2u; case PixelFormat::g: return 1u; } assert(false && "unreachable"); } struct PixelBuf { private: public: using value_type = RGBA; using base_type = Matrix; using pointer = typename base_type::pointer; using const_pointer = typename base_type::const_pointer; using reference = typename base_type::reference; using const_reference = typename base_type::const_reference; using iterator = typename base_type::iterator; using const_iterator = typename base_type::const_iterator; using reverse_iterator = typename base_type::reverse_iterator; using const_reverse_iterator = typename base_type::const_reverse_iterator; using difference_type = typename base_type::difference_type; using size_type = typename base_type::size_type; PixelBuf(size_type r, size_type c) : m_data(r, c) {} PixelBuf(size_type r, size_type c, RGBA color) : m_data(r, c, color) {} PixelBuf(std::uint8_t const* in, size_type r, size_type c, PixelFormat format = PixelFormat::rgba) : PixelBuf(r, c) { assert(in != nullptr); switch (format) { case PixelFormat::rgba: from_helper(in, data(), size()); break; case PixelFormat::abgr: from_helper(in, data(), size()); break; case PixelFormat::rgb: from_helper(in, data(), size()); break; case PixelFormat::bgr: from_helper(in, data(), size()); break; case PixelFormat::ga: from_helper(in, data(), size()); break; case PixelFormat::ag: from_helper(in, data(), size()); break; case PixelFormat::g: from_helper(in, data(), size()); break; } } PixelBuf() noexcept = default; PixelBuf(PixelBuf const&) = default; PixelBuf(PixelBuf &&) noexcept = default; PixelBuf& operator=(PixelBuf const&) = default; PixelBuf& operator=(PixelBuf &&) noexcept = default; ~PixelBuf() = default; constexpr auto size() const noexcept -> size_type { return m_data.size(); } constexpr auto rows() const noexcept -> size_type { return m_data.rows(); } constexpr auto cols() const noexcept -> size_type { return m_data.cols(); } constexpr auto data() noexcept -> pointer { return m_data.data(); } constexpr auto data() const noexcept -> const_pointer { return m_data.data(); } auto to_raw_buf() noexcept -> std::uint8_t* { return reinterpret_cast(data()); } auto to_raw_buf() const noexcept -> std::uint8_t const* { return reinterpret_cast(data()); } constexpr auto raw_buf_size() const noexcept { return size() * sizeof(RGBA); } constexpr auto begin() noexcept -> iterator { return m_data.begin(); } constexpr auto end() noexcept -> iterator { return m_data.end(); } constexpr auto begin() const noexcept -> const_iterator { return m_data.begin(); } constexpr auto end() const noexcept -> const_iterator { return m_data.end(); } constexpr auto rbegin() noexcept -> reverse_iterator { return m_data.rbegin(); } constexpr auto rend() noexcept -> reverse_iterator { return m_data.rend(); } constexpr auto rbegin() const noexcept -> const_reverse_iterator { return m_data.rbegin(); } constexpr auto rend() const noexcept -> const_reverse_iterator { return m_data.rend(); } constexpr decltype(auto) operator[](size_type r) noexcept { return m_data[r]; } constexpr decltype(auto) operator[](size_type r) const noexcept { return m_data[r]; } constexpr auto operator()(size_type r, size_type c) noexcept -> reference { return m_data(r, c); } constexpr auto operator()(size_type r, size_type c) const noexcept -> const_reference { return m_data(r, c); } constexpr auto fill(RGBA color) noexcept -> void { std::fill(begin(), end(), color); } constexpr auto copy_to(std::uint8_t* out, PixelFormat format = PixelFormat::rgba) const noexcept { assert(out != nullptr); switch (format) { case PixelFormat::rgba: copy_to_helper(data(), out, size());return; case PixelFormat::abgr: copy_to_helper(data(), out, size());return; case PixelFormat::rgb: copy_to_helper(data(), out, size());return; case PixelFormat::bgr: copy_to_helper(data(), out, size());return; case PixelFormat::ga: copy_to_helper(data(), out, size());return; case PixelFormat::ag: copy_to_helper(data(), out, size());return; case PixelFormat::g: copy_to_helper(data(), out, size());return; } assert(false && "unreachable"); } private: template constexpr auto copy_to_helper(const_pointer in, std::uint8_t* out, size_type size) const noexcept -> void { constexpr auto channels = get_pixel_format_channel(F); for (auto i = size_type{}; i < size; ++i) { detail::parse_pixel_helper(in[i], out + i * channels); } } template constexpr auto from_helper(std::uint8_t const* in, pointer out, size_type size) const noexcept -> void { constexpr auto channels = get_pixel_format_channel(F); for (auto i = size_type{}; i < size; ++i) { out[i] = detail::parse_pixel_helper(in + i * channels); } } private: base_type m_data; }; } // namespace amt #include namespace std { template <> struct formatter { constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } auto format(amt::RGBA const& color, auto& ctx) const { return format_to(ctx.out(), "rgba({}, {}, {}, {})", color.r(), color.g(), color.b(), color.a()); } }; template <> struct formatter { constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } auto format(amt::HSLA const& color, auto& ctx) const { return format_to(ctx.out(), "hsla({:.1f}deg, {:.1f}%, {:.1f}%, {:.1f}%)", color.h(), color.s(), color.l(), color.a()); } }; template <> struct formatter { bool hsla = false; constexpr auto parse(format_parse_context& ctx) { auto it = ctx.begin(); while (it != ctx.end() && *it != '}') { if (*it == 'h') hsla = true; ++it; } return it; } auto format(amt::PixelBuf const& buf, auto& ctx) const { std::string s = "[\n"; for (auto r = std::size_t{}; r < buf.rows(); ++r) { for (auto c = std::size_t{}; c < buf.cols(); ++c) { auto color = buf(r, c); if (hsla) s += std::format("{}, ", amt::HSLA(color)); else s += std::format("{}, ", color); } s += '\n'; } s += "]"; return format_to(ctx.out(), "{}", s); } }; } // namespace std #endif // AMT_PIXEL_HPP