libepaper 2.0.0
A C++23 library for controlling Waveshare e-paper displays on Raspberry Pi, featuring transparent sleep/wake management and a fluent builder API.
Loading...
Searching...
No Matches
color_manager.hpp
Go to the documentation of this file.
1#pragma once
2
7#include <algorithm>
8#include <cstdint>
9#include <span>
10#include <vector>
11
12namespace epaper {
13
69private:
83 [[nodiscard]] static constexpr auto distance_sq(const RGB &c1, const RGB &c2) noexcept -> int {
84 int dr = static_cast<int>(c1.r) - static_cast<int>(c2.r);
85 int dg = static_cast<int>(c1.g) - static_cast<int>(c2.g);
86 int db = static_cast<int>(c1.b) - static_cast<int>(c2.b);
87 return (dr * dr) + (dg * dg) + (db * db);
88 }
89
90public:
91 ColorManager() = default;
92
99 [[nodiscard]] static constexpr auto to_rgb(Color color) noexcept -> RGB {
100 switch (color) {
101 case Color::Black:
102 return colors::Black;
103 case Color::White:
104 return colors::White;
105 case Color::Red:
106 return colors::Red;
107 case Color::Green:
108 return colors::Green;
109 case Color::Blue:
110 return colors::Blue;
111 case Color::Yellow:
112 return colors::Yellow;
113 case Color::Gray1:
114 return colors::LightGray;
115 case Color::Gray2:
116 return colors::DarkGray;
117 default:
118 return colors::White;
119 }
120 }
121
128 [[nodiscard]] static constexpr auto convert_to_bw(const RGB &color) noexcept -> DeviceColor<DisplayMode::BlackWhite> {
129 const auto gray = color.to_grayscale();
131 }
132
158 [[nodiscard]] static constexpr auto convert_to_gray4(const RGB &color) noexcept
160 const auto gray = color.to_grayscale();
161 // Map 0-255 to 0-3
162 // 0-63 -> 0 (black), 64-127 -> 1, 128-191 -> 2, 192-255 -> 3 (white)
163 const std::uint8_t level = gray >> 6; // Divide by 64
165 }
166
167 [[nodiscard]] static constexpr auto convert_to_bwr(const RGB &color) noexcept -> DeviceColor<DisplayMode::BWR> {
168 // Current options: Black, White, Red
169 const int d_black = distance_sq(color, colors::Black);
170 const int d_white = distance_sq(color, colors::White);
171 const int d_red = distance_sq(color, colors::Red);
172
173 if (d_red < d_black && d_red < d_white) {
175 }
176 if (d_black < d_white) {
178 }
180 }
181
182 [[nodiscard]] static constexpr auto convert_to_bwy(const RGB &color) noexcept -> DeviceColor<DisplayMode::BWY> {
183 // Current options: Black, White, Yellow
184 const int d_black = distance_sq(color, colors::Black);
185 const int d_white = distance_sq(color, colors::White);
186 const int d_yellow = distance_sq(color, colors::Yellow);
187
188 if (d_yellow < d_black && d_yellow < d_white) {
190 }
191 if (d_black < d_white) {
193 }
195 }
196
197 [[nodiscard]] static constexpr auto convert_to_spectra6(const RGB &color) noexcept
199 // Colors: Black, White, Red, Green, Blue, Yellow
200 const int d_black = distance_sq(color, colors::Black);
201 const int d_white = distance_sq(color, colors::White);
202 const int d_red = distance_sq(color, colors::Red);
203 const int d_green = distance_sq(color, colors::Green);
204 const int d_blue = distance_sq(color, colors::Blue);
205 const int d_yellow = distance_sq(color, colors::Yellow);
206
207 int min_d = d_black;
209
210 if (d_white < min_d) {
211 min_d = d_white;
213 }
214 if (d_red < min_d) {
215 min_d = d_red;
217 }
218 if (d_green < min_d) {
219 min_d = d_green;
221 }
222 if (d_blue < min_d) {
223 min_d = d_blue;
225 }
226 if (d_yellow < min_d) {
227 min_d = d_yellow;
229 }
231 }
232
240 [[nodiscard]] static constexpr auto convert_to_bw(const RGBA &color, const RGB &background = colors::White) noexcept
242 const auto blended = blend_alpha(color, background);
243 return convert_to_bw(blended);
244 }
245
253 [[nodiscard]] static constexpr auto convert_to_gray4(const RGBA &color,
254 const RGB &background = colors::White) noexcept
256 const auto blended = blend_alpha(color, background);
258 }
259
267 template <DisplayMode Mode>
268 [[nodiscard]] static constexpr auto convert(const RGB &color) noexcept -> DeviceColor<Mode> {
269 if constexpr (Mode == DisplayMode::BlackWhite) {
270 return convert_to_bw(color);
271 } else if constexpr (Mode == DisplayMode::Grayscale4) {
272 return convert_to_gray4(color);
273 } else if constexpr (Mode == DisplayMode::BWR) {
274 return convert_to_bwr(color);
275 } else if constexpr (Mode == DisplayMode::BWY) {
276 return convert_to_bwy(color);
277 } else if constexpr (Mode == DisplayMode::Spectra6) {
278 return convert_to_spectra6(color);
279 } else {
280 return DeviceColor<Mode>{};
281 }
282 }
283
292 template <DisplayMode Mode>
293 [[nodiscard]] static constexpr auto convert(const RGBA &color, const RGB &background = colors::White) noexcept
295 if constexpr (Mode == DisplayMode::BlackWhite) {
296 return convert_to_bw(color, background);
297 } else if constexpr (Mode == DisplayMode::Grayscale4) {
298 return convert_to_gray4(color, background);
299 } else {
300 // For color modes, blend and default
301 const auto blended = blend_alpha(color, background);
302 return convert<Mode>(blended);
303 }
304 }
305
316 template <DisplayMode Mode, typename SetPixelFunc>
317 static void dither_image(std::span<const std::uint8_t> rgb_data, std::size_t width, std::size_t height,
318 SetPixelFunc set_pixel) {
319 if (rgb_data.size() < width * height * 3) {
320 return;
321 }
322
323 // Floyd-Steinberg Error Diffusion Dithering
324 // Algorithm: Quantize each pixel and distribute quantization error to
325 // neighboring unprocessed pixels using weighted error diffusion kernel.
326 //
327 // Error diffusion kernel (fractions of quantization error):
328 // X 7/16
329 // 3/16 5/16 1/16
330 //
331 // Where X is current pixel. Error propagates right and down only,
332 // allowing single-pass left-to-right, top-to-bottom processing.
333
334 struct RGBError {
335 int r, g, b;
336 };
337 std::vector<RGBError> pixels(width * height);
338
339 // Initialize working buffer with RGB values as signed integers
340 // to allow negative intermediate values during error propagation
341 for (std::size_t i = 0; i < width * height; ++i) {
342 pixels[i] = {static_cast<int>(rgb_data[i * 3]), static_cast<int>(rgb_data[(i * 3) + 1]),
343 static_cast<int>(rgb_data[(i * 3) + 2])};
344 }
345
346 // Clamp values to [0, 255] after error accumulation
347 auto clamp = [](int v) -> std::uint8_t { return static_cast<std::uint8_t>(std::max(0, std::min(255, v))); };
348
349 // Process pixels in raster scan order (left-to-right, top-to-bottom)
350 for (std::size_t y = 0; y < height; ++y) {
351 for (std::size_t x = 0; x < width; ++x) {
352 std::size_t i = (y * width) + x;
353 RGB current{clamp(pixels[i].r), clamp(pixels[i].g), clamp(pixels[i].b)};
354
355 // Step 1: Quantize current pixel to nearest device color
357 set_pixel(x, y, dev_color);
358
359 // Step 2: Calculate quantization error (original - quantized)
360 RGB quantized = dev_color.to_rgb();
361 int er = static_cast<int>(current.r) - static_cast<int>(quantized.r);
362 int eg = static_cast<int>(current.g) - static_cast<int>(quantized.g);
363 int eb = static_cast<int>(current.b) - static_cast<int>(quantized.b);
364
365 // Step 3: Distribute quantization error to neighboring pixels
366 // Floyd-Steinberg error diffusion weights:
367 // Right neighbor (x+1, y): 7/16 of error
368 // Bottom-left (x-1, y+1): 3/16 of error
369 // Bottom (x, y+1): 5/16 of error
370 // Bottom-right (x+1, y+1): 1/16 of error
371 auto add_error = [&](std::size_t idx, double factor) {
372 pixels[idx].r += static_cast<int>(er * factor);
373 pixels[idx].g += static_cast<int>(eg * factor);
374 pixels[idx].b += static_cast<int>(eb * factor);
375 };
376
377 // Diffuse to right neighbor (7/16)
378 if (x + 1 < width) {
379 add_error((y * width) + (x + 1), 7.0 / 16.0);
380 }
381
382 // Diffuse to next row (if not at bottom edge)
383 if (y + 1 < height) {
384 // Bottom-left diagonal (3/16)
385 if (x > 0) {
386 add_error(((y + 1) * width) + (x - 1), 3.0 / 16.0);
387 }
388 // Directly below (5/16)
389 add_error(((y + 1) * width) + x, 5.0 / 16.0);
390 // Bottom-right diagonal (1/16)
391 if (x + 1 < width) {
392 add_error(((y + 1) * width) + (x + 1), 1.0 / 16.0);
393 }
394 }
395 }
396 }
397 }
398
399private:
407 [[nodiscard]] static constexpr auto blend_alpha(const RGBA &color, const RGB &background) noexcept -> RGB {
408 const auto alpha = static_cast<double>(color.a) / 255.0;
409 const auto inv_alpha = 1.0 - alpha;
410
411 const auto r = static_cast<std::uint8_t>((color.r * alpha) + (background.r * inv_alpha));
412 const auto g = static_cast<std::uint8_t>((color.g * alpha) + (background.g * inv_alpha));
413 const auto b = static_cast<std::uint8_t>((color.b * alpha) + (background.b * inv_alpha));
414
415 return RGB{r, g, b};
416 }
417};
418
419} // namespace epaper
Definition color_manager.hpp:68
static constexpr auto convert(const RGB &color) noexcept -> DeviceColor< Mode >
Generic conversion based on display mode template parameter.
Definition color_manager.hpp:268
static constexpr auto convert_to_bw(const RGB &color) noexcept -> DeviceColor< DisplayMode::BlackWhite >
Convert RGB color to 1-bit black/white.
Definition color_manager.hpp:128
static constexpr auto convert_to_bwy(const RGB &color) noexcept -> DeviceColor< DisplayMode::BWY >
Definition color_manager.hpp:182
static constexpr auto convert_to_gray4(const RGBA &color, const RGB &background=colors::White) noexcept -> DeviceColor< DisplayMode::Grayscale4 >
Convert RGBA color to 2-bit grayscale with alpha blending.
Definition color_manager.hpp:253
static constexpr auto convert_to_bw(const RGBA &color, const RGB &background=colors::White) noexcept -> DeviceColor< DisplayMode::BlackWhite >
Convert RGBA color to 1-bit black/white with alpha blending.
Definition color_manager.hpp:240
static constexpr auto convert_to_spectra6(const RGB &color) noexcept -> DeviceColor< DisplayMode::Spectra6 >
Definition color_manager.hpp:197
static constexpr auto convert_to_bwr(const RGB &color) noexcept -> DeviceColor< DisplayMode::BWR >
Definition color_manager.hpp:167
static constexpr auto convert_to_gray4(const RGB &color) noexcept -> DeviceColor< DisplayMode::Grayscale4 >
Definition color_manager.hpp:158
static constexpr auto convert(const RGBA &color, const RGB &background=colors::White) noexcept -> DeviceColor< Mode >
Generic conversion based on display mode template parameter with alpha.
Definition color_manager.hpp:293
static constexpr auto to_rgb(Color color) noexcept -> RGB
Convert Color enum to RGB.
Definition color_manager.hpp:99
static void dither_image(std::span< const std::uint8_t > rgb_data, std::size_t width, std::size_t height, SetPixelFunc set_pixel)
Dither an RGB image to device colors using Floyd-Steinberg.
Definition color_manager.hpp:317
constexpr RGB Blue
Definition color.hpp:189
constexpr RGB Yellow
Definition color.hpp:190
constexpr RGB DarkGray
Definition color.hpp:194
constexpr RGB LightGray
Definition color.hpp:195
constexpr RGB Black
Definition color.hpp:185
constexpr RGB White
Definition color.hpp:186
constexpr RGB Red
Definition color.hpp:187
constexpr RGB Green
Definition color.hpp:188
Definition color.hpp:5
Color
Definition types.hpp:32
@ Gray1
First gray level - lighter (Grayscale4 mode only)
@ White
White (or lightest gray in grayscale modes)
@ Yellow
Yellow (BWY and Spectra6 modes)
@ Blue
Blue (Spectra6 mode only)
@ Green
Green (Spectra6 mode only)
@ Gray2
Second gray level - darker (Grayscale4 mode only)
@ Black
Black (or darkest gray in grayscale modes)
@ Red
Red (BWR and Spectra6 modes)
@ Third
Red for BWR, Yellow for BWY.
@ BWY
Black, White, Yellow (3 colors, typically 2-bit)
@ Spectra6
6-color: Black, White, Red, Yellow, Blue, Green (3-bit)
@ BlackWhite
1-bit black and white (2 colors)
@ Grayscale4
2-bit 4-level grayscale
@ BWR
Black, White, Red (3 colors, typically 2-bit)
Device-specific color representation (generic template).
Definition device_color.hpp:26
Definition color.hpp:131
Definition color.hpp:50
std::uint8_t r
Red component (0-255)
Definition color.hpp:53