* @brief Create a plot of an image as characters, using either 24-bit ANSI colors or monochrome blocks of different brightness. * * @param image The image to plot. * @param out_width The plot width in terminal characters. Should be even, since in the monochrome case each pixel spans two characters horizontally. * @param use_color `true` to generate a colored plot, `false` to generate a monochr
| 3503 | * @return The plot as a string. |
| 3504 | */ |
| 3505 | std::string plot_image_chars(const image_matrix<color>& image, const std::size_t out_width, const bool use_color) |
| 3506 | { |
| 3507 | // Get the source image dimensions. |
| 3508 | const std::size_t src_width = image.get_width(); |
| 3509 | const std::size_t src_height = image.get_height(); |
| 3510 | // Compute the plot height in terminal characters, keeping in mind that characters have an aspect ratio of about 1:2 (width:height). |
| 3511 | const std::size_t out_height = static_cast<std::size_t>(std::llround(0.5 * static_cast<double>(src_height) * static_cast<double>(out_width) / static_cast<double>(src_width))); |
| 3512 | // Create a buffer for the plot. |
| 3513 | std::string plot; |
| 3514 | |
| 3515 | // For the colored plot we use Unicode lower half blocks to pack two pixels per character. The background color sets the color of the top (empty) half, and the foreground color sets the color of the bottom (filled) half. |
| 3516 | if (use_color) |
| 3517 | { |
| 3518 | // Each character row contains two pixel rows (top and bottom). |
| 3519 | const std::size_t out_height_pixels = out_height * 2; |
| 3520 | // Create a mapping of output x coordinates to source pixels, such that x_map[0] = 0 and x_map[out_width - 1] = src_width - 1. |
| 3521 | std::vector<std::size_t> x_map(out_width); |
| 3522 | for (std::size_t x = 0; x < out_width; ++x) |
| 3523 | x_map[x] = (x * (src_width - 1)) / (out_width - 1); |
| 3524 | // Create a mapping of output pixel y coordinates to source pixels, such that y_map[0] = 0 and y_map[out_height_pixels - 1] = src_height - 1. |
| 3525 | std::vector<std::size_t> y_map(out_height_pixels); |
| 3526 | for (std::size_t y = 0; y < out_height_pixels; ++y) |
| 3527 | y_map[y] = (y * (src_height - 1)) / (out_height_pixels - 1); |
| 3528 | // Reserve enough capacity for the entire plot to avoid reallocations. Each row contains ANSI codes of the form `\033[48;2;RRR;GGG;BBBm\033[38;2;RRR;GGG;BBBm` = up to 38 bytes, plus the Unicode lower half block U+2584 in UTF-8 = 3 bytes, for a total of 41 bytes per character of the plot, plus the `\033[0m` reset code at the end and the newline = 5 bytes. |
| 3529 | plot.reserve(out_height * ((out_width * 41) + 5)); |
| 3530 | // Iterate over the rows and columns. |
| 3531 | for (std::size_t y = 0; y < out_height; ++y) |
| 3532 | { |
| 3533 | for (std::size_t x = 0; x < out_width; ++x) |
| 3534 | { |
| 3535 | // Fetch the sampled source pixel for the top and bottom halves. |
| 3536 | const color col_top = image(x_map[x], y_map[2 * y]); |
| 3537 | const color col_bottom = image(x_map[x], y_map[(2 * y) + 1]); |
| 3538 | // Create the background color ANSI sequence for the top half. |
| 3539 | plot.append("\033[48;2;"); |
| 3540 | plot.append(std::to_string(col_top.r)); |
| 3541 | plot.push_back(';'); |
| 3542 | plot.append(std::to_string(col_top.g)); |
| 3543 | plot.push_back(';'); |
| 3544 | plot.append(std::to_string(col_top.b)); |
| 3545 | // Finish the sequence with `m` and create the foreground color ANSI sequence for the bottom half. |
| 3546 | plot.append("m\033[38;2;"); |
| 3547 | plot.append(std::to_string(col_bottom.r)); |
| 3548 | plot.push_back(';'); |
| 3549 | plot.append(std::to_string(col_bottom.g)); |
| 3550 | plot.push_back(';'); |
| 3551 | plot.append(std::to_string(col_bottom.b)); |
| 3552 | // Finish the sequence with `m` and append a lower half block (U+2584) in UTF-8, which is the actual character that will be displayed. |
| 3553 | plot.append("m\xE2\x96\x84"); |
| 3554 | } |
| 3555 | // Reset the ANSI style at the end of the row and add a newline character. |
| 3556 | plot.append("\033[0m\n"); |
| 3557 | } |
| 3558 | } |
| 3559 | // For the monochrome plot we use Unicode block characters of different brightness levels. Each pixel is rendered as two identical blocks side-by-side to make the pixels square (so essentially, the opposite of the colored plot). |
| 3560 | else |
| 3561 | { |
| 3562 | // Each pixel spans two terminal characters horizontally. |
no test coverage detected