Mads
Multi-Agent Distributed System
Loading...
Searching...
No Matches
TerminalLogoRenderer.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <array>
5#include <cmath>
6#include <cstdint>
7#include <cstdlib>
8#include <filesystem>
9#include <iosfwd>
10#include <optional>
11#include <stdexcept>
12#include <string>
13#include <string_view>
14#include <vector>
15
16#ifdef _WIN32
17#ifndef NOMINMAX
18#define NOMINMAX
19#endif
20#include <windows.h>
21#endif
22
23#ifndef TERMINAL_LOGO_STB_IMAGE_INCLUDED
24#define TERMINAL_LOGO_STB_IMAGE_INCLUDED
25#define STB_IMAGE_STATIC
26#define STB_IMAGE_IMPLEMENTATION
27#include "stb_image.h"
28#endif
29
30namespace terminal_logo {
31
33public:
34 enum class ColorMode {
35 Auto,
37 Ansi256,
38 Ansi16,
39 Mono
40 };
41
42 struct Options {
43 int width = 80;
45 };
46
47 static ColorMode parse_mode(std::string_view value) {
48 if (value == "auto") {
49 return ColorMode::Auto;
50 }
51 if (value == "truecolor") {
53 }
54 if (value == "256") {
55 return ColorMode::Ansi256;
56 }
57 if (value == "ansi16") {
58 return ColorMode::Ansi16;
59 }
60 if (value == "mono") {
61 return ColorMode::Mono;
62 }
63 throw std::invalid_argument("Invalid mode: " + std::string(value));
64 }
65
66 static void render_from_path(const std::filesystem::path& image_path,
67 std::ostream& out,
68 const Options& options) {
69 if (options.width <= 0) {
70 throw std::invalid_argument("width must be positive");
71 }
72 if (!std::filesystem::exists(image_path)) {
73 throw std::runtime_error("Image path does not exist: " + image_path.string());
74 }
75
76#ifdef _WIN32
77 enable_ansi_on_windows();
78#endif
79
80 const Image image = load_image(image_path.string());
81 const ColorMode resolved_mode = detect_mode(options.mode);
82 render_to_stream(image, options.width, resolved_mode, out);
83 }
84
85 static void render_from_path(const std::filesystem::path& image_path, std::ostream& out) {
86 render_from_path(image_path, out, Options{});
87 }
88
89private:
90 struct Color {
91 std::uint8_t r;
92 std::uint8_t g;
93 std::uint8_t b;
94 std::uint8_t a;
95 };
96
97 struct Image {
98 int width = 0;
99 int height = 0;
100 std::vector<Color> pixels;
101 };
102
103#ifdef _WIN32
104 static void enable_ansi_on_windows() {
105 HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
106 if (handle == INVALID_HANDLE_VALUE || handle == nullptr) {
107 return;
108 }
109
110 DWORD mode = 0;
111 if (!GetConsoleMode(handle, &mode)) {
112 return;
113 }
114
115 mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
116 SetConsoleMode(handle, mode);
117 }
118#endif
119
120 static ColorMode detect_mode(ColorMode preferred) {
121 if (preferred != ColorMode::Auto) {
122 return preferred;
123 }
124
125 if (std::getenv("NO_COLOR") != nullptr) {
126 return ColorMode::Mono;
127 }
128
129 const std::string colorterm = std::getenv("COLORTERM") ? std::getenv("COLORTERM") : "";
130 const std::string term = std::getenv("TERM") ? std::getenv("TERM") : "";
131
132 if (colorterm.find("truecolor") != std::string::npos || colorterm.find("24bit") != std::string::npos) {
134 }
135 if (term.find("256color") != std::string::npos) {
136 return ColorMode::Ansi256;
137 }
138 if (!term.empty() && term != "dumb") {
139 return ColorMode::Ansi16;
140 }
141
142#ifdef _WIN32
143 return ColorMode::Ansi16;
144#else
145 return ColorMode::Mono;
146#endif
147 }
148
149 static Image load_image(const std::string& path) {
150 int width = 0;
151 int height = 0;
152 int channels = 0;
153 unsigned char* data = stbi_load(path.c_str(), &width, &height, &channels, 4);
154 if (data == nullptr) {
155 throw std::runtime_error("Failed to load image: " + path);
156 }
157
158 Image image;
159 image.width = width;
160 image.height = height;
161 image.pixels.resize(static_cast<std::size_t>(width) * static_cast<std::size_t>(height));
162
163 for (int y = 0; y < height; ++y) {
164 for (int x = 0; x < width; ++x) {
165 const std::size_t idx = static_cast<std::size_t>(y) * static_cast<std::size_t>(width) + static_cast<std::size_t>(x);
166 const std::size_t off = idx * 4;
167 image.pixels[idx] = Color{
168 static_cast<std::uint8_t>(data[off + 0]),
169 static_cast<std::uint8_t>(data[off + 1]),
170 static_cast<std::uint8_t>(data[off + 2]),
171 static_cast<std::uint8_t>(data[off + 3])};
172 }
173 }
174
175 stbi_image_free(data);
176 return image;
177 }
178
179 static Color sample_bilinear(const Image& image, float x, float y) {
180 x = std::clamp(x, 0.0f, static_cast<float>(image.width - 1));
181 y = std::clamp(y, 0.0f, static_cast<float>(image.height - 1));
182
183 const int x0 = static_cast<int>(std::floor(x));
184 const int y0 = static_cast<int>(std::floor(y));
185 const int x1 = std::min(x0 + 1, image.width - 1);
186 const int y1 = std::min(y0 + 1, image.height - 1);
187
188 const float tx = x - static_cast<float>(x0);
189 const float ty = y - static_cast<float>(y0);
190
191 const auto pixel = [&](int px, int py) -> Color {
192 const std::size_t idx = static_cast<std::size_t>(py) * static_cast<std::size_t>(image.width) + static_cast<std::size_t>(px);
193 return image.pixels[idx];
194 };
195
196 const Color c00 = pixel(x0, y0);
197 const Color c10 = pixel(x1, y0);
198 const Color c01 = pixel(x0, y1);
199 const Color c11 = pixel(x1, y1);
200
201 auto lerp = [](float a, float b, float t) {
202 return a + (b - a) * t;
203 };
204
205 const float r0 = lerp(static_cast<float>(c00.r), static_cast<float>(c10.r), tx);
206 const float g0 = lerp(static_cast<float>(c00.g), static_cast<float>(c10.g), tx);
207 const float b0 = lerp(static_cast<float>(c00.b), static_cast<float>(c10.b), tx);
208 const float a0 = lerp(static_cast<float>(c00.a), static_cast<float>(c10.a), tx);
209
210 const float r1 = lerp(static_cast<float>(c01.r), static_cast<float>(c11.r), tx);
211 const float g1 = lerp(static_cast<float>(c01.g), static_cast<float>(c11.g), tx);
212 const float b1 = lerp(static_cast<float>(c01.b), static_cast<float>(c11.b), tx);
213 const float a1 = lerp(static_cast<float>(c01.a), static_cast<float>(c11.a), tx);
214
215 return Color{
216 static_cast<std::uint8_t>(std::round(lerp(r0, r1, ty))),
217 static_cast<std::uint8_t>(std::round(lerp(g0, g1, ty))),
218 static_cast<std::uint8_t>(std::round(lerp(b0, b1, ty))),
219 static_cast<std::uint8_t>(std::round(lerp(a0, a1, ty)))};
220 }
221
222 static std::vector<Color> resize_image(const Image& image, int out_w, int out_h) {
223 std::vector<Color> out(static_cast<std::size_t>(out_w) * static_cast<std::size_t>(out_h));
224 for (int y = 0; y < out_h; ++y) {
225 const float src_y = (static_cast<float>(y) + 0.5f) * static_cast<float>(image.height) / static_cast<float>(out_h) - 0.5f;
226 for (int x = 0; x < out_w; ++x) {
227 const float src_x = (static_cast<float>(x) + 0.5f) * static_cast<float>(image.width) / static_cast<float>(out_w) - 0.5f;
228 out[static_cast<std::size_t>(y) * static_cast<std::size_t>(out_w) + static_cast<std::size_t>(x)] =
229 sample_bilinear(image, src_x, src_y);
230 }
231 }
232 return out;
233 }
234
235 static int ansi256_index(Color c) {
236 const int r = static_cast<int>(std::round((c.r / 255.0) * 5.0));
237 const int g = static_cast<int>(std::round((c.g / 255.0) * 5.0));
238 const int b = static_cast<int>(std::round((c.b / 255.0) * 5.0));
239 return 16 + 36 * r + 6 * g + b;
240 }
241
242 static int ansi16_index(Color c, bool bright) {
243 const int r = c.r >= 128 ? 1 : 0;
244 const int g = c.g >= 128 ? 1 : 0;
245 const int b = c.b >= 128 ? 1 : 0;
246 int idx = r + 2 * g + 4 * b;
247 if (idx == 0 && bright) {
248 idx = 7;
249 }
250 return idx;
251 }
252
253 static std::string fg_escape(Color c, ColorMode mode) {
254 if (mode == ColorMode::TrueColor) {
255 return "\x1b[38;2;" + std::to_string(c.r) + ";" + std::to_string(c.g) + ";" + std::to_string(c.b) + "m";
256 }
257 if (mode == ColorMode::Ansi256) {
258 return "\x1b[38;5;" + std::to_string(ansi256_index(c)) + "m";
259 }
260 if (mode == ColorMode::Ansi16) {
261 const int idx = ansi16_index(c, true);
262 return "\x1b[" + std::to_string((idx < 8 ? 30 : 90) + (idx % 8)) + "m";
263 }
264 return "";
265 }
266
267 static std::string bg_escape(Color c, ColorMode mode) {
268 if (mode == ColorMode::TrueColor) {
269 return "\x1b[48;2;" + std::to_string(c.r) + ";" + std::to_string(c.g) + ";" + std::to_string(c.b) + "m";
270 }
271 if (mode == ColorMode::Ansi256) {
272 return "\x1b[48;5;" + std::to_string(ansi256_index(c)) + "m";
273 }
274 if (mode == ColorMode::Ansi16) {
275 const int idx = ansi16_index(c, false);
276 return "\x1b[" + std::to_string((idx < 8 ? 40 : 100) + (idx % 8)) + "m";
277 }
278 return "";
279 }
280
281 static char mono_char(Color top, Color bottom) {
282 if (top.a < 16 && bottom.a < 16) {
283 return ' ';
284 }
285 const int lum_top = (static_cast<int>(top.r) * 2126 + static_cast<int>(top.g) * 7152 + static_cast<int>(top.b) * 722) / 10000;
286 const int lum_bottom =
287 (static_cast<int>(bottom.r) * 2126 + static_cast<int>(bottom.g) * 7152 + static_cast<int>(bottom.b) * 722) / 10000;
288 const int avg = (lum_top + lum_bottom) / 2;
289 constexpr std::array<char, 10> ramp = {' ', '.', ':', '-', '=', '+', '*', '#', '%', '@'};
290 const std::size_t idx = static_cast<std::size_t>(std::clamp(avg, 0, 255)) * (ramp.size() - 1) / 255;
291 return ramp[idx];
292 }
293
294 static void render_to_stream(const Image& image, int out_w, ColorMode mode, std::ostream& out) {
295 constexpr float pixel_aspect = 0.5f;
296 const float out_h_f = static_cast<float>(image.height) * static_cast<float>(out_w) * pixel_aspect /
297 static_cast<float>(image.width);
298 const int out_h = std::max(2, static_cast<int>(std::round(out_h_f)));
299
300 const std::vector<Color> scaled = resize_image(image, out_w, out_h);
301
302 for (int y = 0; y < out_h; y += 2) {
303 for (int x = 0; x < out_w; ++x) {
304 const Color top = scaled[static_cast<std::size_t>(y) * static_cast<std::size_t>(out_w) + static_cast<std::size_t>(x)];
305 const Color bottom = scaled[static_cast<std::size_t>(std::min(y + 1, out_h - 1)) * static_cast<std::size_t>(out_w) +
306 static_cast<std::size_t>(x)];
307 const bool top_visible = top.a >= 16;
308 const bool bottom_visible = bottom.a >= 16;
309
310 if (mode == ColorMode::Mono) {
311 out << mono_char(top, bottom);
312 } else {
313 if (!top_visible && !bottom_visible) {
314 out << "\x1b[0m ";
315 } else if (top_visible && bottom_visible) {
316 out << fg_escape(top, mode) << bg_escape(bottom, mode) << "▀";
317 } else if (top_visible) {
318 // Upper half only; keep lower half as terminal background.
319 out << "\x1b[0m" << fg_escape(top, mode) << "▀";
320 } else {
321 // Lower half only; keep upper half as terminal background.
322 out << "\x1b[0m" << fg_escape(bottom, mode) << "▄";
323 }
324 }
325 }
326 if (mode != ColorMode::Mono) {
327 out << "\x1b[0m";
328 }
329 out << '\n';
330 }
331 }
332};
333
334} // namespace terminal_logo
static ColorMode parse_mode(std::string_view value)
static void render_from_path(const std::filesystem::path &image_path, std::ostream &out)
static void render_from_path(const std::filesystem::path &image_path, std::ostream &out, const Options &options)
STBIDEF void stbi_image_free(void *retval_from_stbi_load)
STBIDEF stbi_uc * stbi_load(char const *filename, int *x, int *y, int *channels_in_file, int desired_channels)