ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
ansi_fast.c
Go to the documentation of this file.
1
7#include <ascii-chat/common.h>
8#include <ascii-chat/video/simd/ascii_simd.h>
9#include <ascii-chat/video/ansi_fast.h>
10#include <ascii-chat/util/math.h>
11#include <ascii-chat/platform/init.h>
12#include <string.h>
13#include <time.h>
14#ifndef _WIN32
15#include <unistd.h>
16#endif
17#include <limits.h>
18
19// 256-color lookup table (optional)
20static char color256_strings[256][16]; // Pre-built SGR strings like "\033[38;5;123m"
21static bool color256_initialized = false;
22static static_mutex_t g_color256_mutex = STATIC_MUTEX_INIT;
23
24// Fast foreground color: \033[38;2;R;G;Bm
25// Maximum output: 19 bytes (\033[38;2;255;255;255m)
26char *append_truecolor_fg(char *dst, uint8_t r, uint8_t g, uint8_t b) {
27 // Ensure dec3 cache is initialized
28 if (!g_dec3_cache.dec3_initialized) {
29 init_dec3();
30 }
31
32 // Static prefix - 7 bytes
33 SAFE_MEMCPY(dst, 19, "\033[38;2;", 7);
34 dst += 7;
35
36 // Red component + semicolon
37 SAFE_MEMCPY(dst, 12, g_dec3_cache.dec3_table[r].s, g_dec3_cache.dec3_table[r].len);
38 dst += g_dec3_cache.dec3_table[r].len;
39 *dst++ = ';';
40
41 // Green component + semicolon
42 SAFE_MEMCPY(dst, 8, g_dec3_cache.dec3_table[g].s, g_dec3_cache.dec3_table[g].len);
43 dst += g_dec3_cache.dec3_table[g].len;
44 *dst++ = ';';
45
46 // Blue component + suffix
47 SAFE_MEMCPY(dst, 4, g_dec3_cache.dec3_table[b].s, g_dec3_cache.dec3_table[b].len);
48 dst += g_dec3_cache.dec3_table[b].len;
49 *dst++ = 'm';
50
51 return dst;
52}
53
54// Fast background color: \033[48;2;R;G;Bm
55// Maximum output: 19 bytes (\033[48;2;255;255;255m)
56char *append_truecolor_bg(char *dst, uint8_t r, uint8_t g, uint8_t b) {
57 // Ensure dec3 cache is initialized
58 if (!g_dec3_cache.dec3_initialized) {
59 init_dec3();
60 }
61
62 SAFE_MEMCPY(dst, 19, "\033[48;2;", 7);
63 dst += 7;
64
65 SAFE_MEMCPY(dst, 12, g_dec3_cache.dec3_table[r].s, g_dec3_cache.dec3_table[r].len);
66 dst += g_dec3_cache.dec3_table[r].len;
67 *dst++ = ';';
68
69 SAFE_MEMCPY(dst, 8, g_dec3_cache.dec3_table[g].s, g_dec3_cache.dec3_table[g].len);
70 dst += g_dec3_cache.dec3_table[g].len;
71 *dst++ = ';';
72
73 SAFE_MEMCPY(dst, 4, g_dec3_cache.dec3_table[b].s, g_dec3_cache.dec3_table[b].len);
74 dst += g_dec3_cache.dec3_table[b].len;
75 *dst++ = 'm';
76
77 return dst;
78}
79
80// Combined foreground + background: \033[38;2;R;G;B;48;2;r;g;bm
81// Maximum output: 38 bytes (\033[38;2;255;255;255;48;2;255;255;255m)
82char *append_truecolor_fg_bg(char *dst, uint8_t fg_r, uint8_t fg_g, uint8_t fg_b, uint8_t bg_r, uint8_t bg_g,
83 uint8_t bg_b) {
84 // Ensure dec3 cache is initialized
85 if (!g_dec3_cache.dec3_initialized) {
86 init_dec3();
87 }
88
89 SAFE_MEMCPY(dst, 38, "\033[38;2;", 7);
90 dst += 7;
91
92 // Foreground RGB (remaining: 31 bytes max)
93 SAFE_MEMCPY(dst, 31, g_dec3_cache.dec3_table[fg_r].s, g_dec3_cache.dec3_table[fg_r].len);
94 dst += g_dec3_cache.dec3_table[fg_r].len;
95 *dst++ = ';';
96
97 // Remaining: 27 bytes max
98 SAFE_MEMCPY(dst, 27, g_dec3_cache.dec3_table[fg_g].s, g_dec3_cache.dec3_table[fg_g].len);
99 dst += g_dec3_cache.dec3_table[fg_g].len;
100 *dst++ = ';';
101
102 // Remaining: 23 bytes max
103 SAFE_MEMCPY(dst, 23, g_dec3_cache.dec3_table[fg_b].s, g_dec3_cache.dec3_table[fg_b].len);
104 dst += g_dec3_cache.dec3_table[fg_b].len;
105
106 // Background RGB (remaining: 20 bytes max)
107 SAFE_MEMCPY(dst, 20, ";48;2;", 6);
108 dst += 6;
109
110 // Remaining: 14 bytes max
111 SAFE_MEMCPY(dst, 14, g_dec3_cache.dec3_table[bg_r].s, g_dec3_cache.dec3_table[bg_r].len);
112 dst += g_dec3_cache.dec3_table[bg_r].len;
113 *dst++ = ';';
114
115 // Remaining: 10 bytes max
116 SAFE_MEMCPY(dst, 10, g_dec3_cache.dec3_table[bg_g].s, g_dec3_cache.dec3_table[bg_g].len);
117 dst += g_dec3_cache.dec3_table[bg_g].len;
118 *dst++ = ';';
119
120 // Remaining: 6 bytes max
121 SAFE_MEMCPY(dst, 6, g_dec3_cache.dec3_table[bg_b].s, g_dec3_cache.dec3_table[bg_b].len);
122 dst += g_dec3_cache.dec3_table[bg_b].len;
123 *dst++ = 'm';
124
125 return dst;
126}
127
128// Initialize run-length encoding context
129void ansi_rle_init(ansi_rle_context_t *ctx, char *buffer, size_t capacity, ansi_color_mode_t mode) {
130 ctx->buffer = buffer;
131 ctx->capacity = capacity;
132 ctx->length = 0;
133 ctx->mode = mode;
134 ctx->first_pixel = true;
135 // Initialize with impossible color values to force first SGR
136 ctx->last_r = 0xFF;
137 ctx->last_g = 0xFF;
138 ctx->last_b = 0xFF;
139}
140
141// Add pixel with run-length encoding - only emit SGR when color changes
142void ansi_rle_add_pixel(ansi_rle_context_t *ctx, uint8_t r, uint8_t g, uint8_t b, char ascii_char) {
143 // Check if we need to emit a new SGR sequence
144 bool color_changed = ctx->first_pixel || (r != ctx->last_r) || (g != ctx->last_g) || (b != ctx->last_b);
145
146 if (color_changed) {
147 if (ctx->length + 40 >= ctx->capacity) {
148 // Log when we can't add a color change due to buffer capacity
149 log_warn_every(US_PER_SEC_INT, "RLE_CAPACITY: Cannot add SGR at length=%zu, capacity=%zu (need 40 bytes)",
150 ctx->length, ctx->capacity);
151 }
152 }
153
154 if (color_changed && (ctx->length + 40 < ctx->capacity)) { // Reserve 40 bytes for SGR (FG+BG max is 38)
155 char *pos = ctx->buffer + ctx->length;
156
157 switch (ctx->mode) {
158 case ANSI_MODE_FOREGROUND:
159 pos = append_truecolor_fg(pos, r, g, b);
160 break;
161 case ANSI_MODE_BACKGROUND:
162 pos = append_truecolor_bg(pos, r, g, b);
163 break;
164 case ANSI_MODE_FOREGROUND_BACKGROUND:
165 // For FG+BG mode, use a default background (black) or implement dual-color logic
166 pos = append_truecolor_fg_bg(pos, r, g, b, 0, 0, 0);
167 break;
168 }
169
170 ctx->length = (size_t)(pos - ctx->buffer);
171 ctx->last_r = r;
172 ctx->last_g = g;
173 ctx->last_b = b;
174 ctx->first_pixel = false;
175 }
176
177 // Add the ASCII character
178 if (ctx->length < ctx->capacity - 1) {
179 ctx->buffer[ctx->length++] = ascii_char;
180 }
181}
182
183// Finish RLE sequence with reset and null terminator
184void ansi_rle_finish(ansi_rle_context_t *ctx) {
185 // Add reset sequence (requires 4 bytes + 1 for null terminator = 5 bytes total)
186 if (ctx->length + 5 <= ctx->capacity) {
187 SAFE_MEMCPY(ctx->buffer + ctx->length, 4, "\033[0m", 4);
188 ctx->length += 4;
189 }
190
191 // Null terminate
192 if (ctx->length < ctx->capacity) {
193 ctx->buffer[ctx->length] = '\0';
194 }
195}
196
197// Initialize the decimal lookup table (call once at startup)
198void ansi_fast_init(void) {
199 // Initialize the dec3 cache used by truecolor functions
201}
202
203// 256-color mode initialization (optional high-speed mode)
204// Thread-safe: uses mutex to prevent duplicate initialization
206 static_mutex_lock(&g_color256_mutex);
207
208 if (color256_initialized) {
209 static_mutex_unlock(&g_color256_mutex);
210 return;
211 }
212
213 for (int i = 0; i < 256; i++) {
214 SAFE_SNPRINTF(color256_strings[i], sizeof(color256_strings[i]), "\033[38;5;%dm", i);
215 }
216
217 color256_initialized = true;
218 static_mutex_unlock(&g_color256_mutex);
219}
220
221// Fast 256-color foreground
222char *append_256color_fg(char *dst, uint8_t color_index) {
223 const char *color_str = color256_strings[color_index];
224 size_t len = strlen(color_str);
225 SAFE_MEMCPY(dst, len, color_str, len);
226 return dst + len;
227}
228
229// Convert RGB to closest 256-color palette index
230uint8_t rgb_to_256color(uint8_t r, uint8_t g, uint8_t b) {
231 // Map to 6x6x6 color cube (216 colors) + grayscale ramp
232
233 // Check if it's close to grayscale
234 int avg = (r + g + b) / 3;
235 int gray_diff = abs(r - avg) + abs(g - avg) + abs(b - avg);
236
237 if (gray_diff < 30) {
238 // Use grayscale ramp (colors 232-255)
239 int gray_level = (avg * 23) / 255;
240 return (uint8_t)(232 + gray_level);
241 }
242
243 // Use 6x6x6 color cube (colors 16-231)
244 int r6 = (r * 5) / 255;
245 int g6 = (g * 5) / 255;
246 int b6 = (b * 5) / 255;
247
248 return (uint8_t)(16 + (r6 * 36) + (g6 * 6) + b6);
249}
250
251// 16-color mode support
252static char color16_fg_strings[16][16];
253static char color16_bg_strings[16][16];
254static bool color16_initialized = false;
255static static_mutex_t g_color16_mutex = STATIC_MUTEX_INIT;
256
258 static_mutex_lock(&g_color16_mutex);
259
260 if (color16_initialized) {
261 static_mutex_unlock(&g_color16_mutex);
262 return;
263 }
264
265 // Standard ANSI color codes
266 const char *fg_codes[] = {"30", "31", "32", "33", "34", "35", "36", "37", // Normal colors (30-37)
267 "90", "91", "92", "93", "94", "95", "96", "97"}; // Bright colors (90-97)
268 const char *bg_codes[] = {"40", "41", "42", "43", "44", "45", "46", "47", // Normal colors (40-47)
269 "100", "101", "102", "103", "104", "105", "106", "107"}; // Bright colors (100-107)
270
271 for (int i = 0; i < 16; i++) {
272 SAFE_SNPRINTF(color16_fg_strings[i], sizeof(color16_fg_strings[i]), "\033[%sm", fg_codes[i]);
273 SAFE_SNPRINTF(color16_bg_strings[i], sizeof(color16_bg_strings[i]), "\033[%sm", bg_codes[i]);
274 }
275
276 color16_initialized = true;
277 static_mutex_unlock(&g_color16_mutex);
278}
279
280char *append_16color_fg(char *dst, uint8_t color_index) {
281 if (!color16_initialized) {
283 }
284
285 if (color_index >= 16) {
286 color_index = 7; // Default to white
287 }
288
289 const char *color_str = color16_fg_strings[color_index];
290 while (*color_str) {
291 *dst++ = *color_str++;
292 }
293
294 return dst;
295}
296
297char *append_16color_bg(char *dst, uint8_t color_index) {
298 if (!color16_initialized) {
300 }
301
302 if (color_index >= 16) {
303 color_index = 0; // Default to black background
304 }
305
306 const char *color_str = color16_bg_strings[color_index];
307 while (*color_str) {
308 *dst++ = *color_str++;
309 }
310
311 return dst;
312}
313
314uint8_t rgb_to_16color(uint8_t r, uint8_t g, uint8_t b) {
315 // Convert RGB to the closest 16-color ANSI color
316 // This uses a simple distance-based approach
317
318 // Define the 16 ANSI colors in RGB
319 static const uint8_t ansi_colors[16][3] = {
320 {0, 0, 0}, // 0: Black
321 {128, 0, 0}, // 1: Dark Red
322 {0, 128, 0}, // 2: Dark Green
323 {128, 128, 0}, // 3: Dark Yellow (Brown)
324 {0, 0, 128}, // 4: Dark Blue
325 {128, 0, 128}, // 5: Dark Magenta
326 {0, 128, 128}, // 6: Dark Cyan
327 {192, 192, 192}, // 7: Light Gray
328 {128, 128, 128}, // 8: Dark Gray
329 {255, 0, 0}, // 9: Bright Red
330 {0, 255, 0}, // 10: Bright Green
331 {255, 255, 0}, // 11: Bright Yellow
332 {0, 0, 255}, // 12: Bright Blue
333 {255, 0, 255}, // 13: Bright Magenta
334 {0, 255, 255}, // 14: Bright Cyan
335 {255, 255, 255} // 15: White
336 };
337
338 uint8_t best_match = 0;
339 int min_distance = INT_MAX;
340
341 for (int i = 0; i < 16; i++) {
342 int dr = (int)r - (int)ansi_colors[i][0];
343 int dg = (int)g - (int)ansi_colors[i][1];
344 int db = (int)b - (int)ansi_colors[i][2];
345 int distance = dr * dr + dg * dg + db * db;
346
347 if (distance < min_distance) {
348 min_distance = distance;
349 best_match = i;
350 }
351 }
352
353 return best_match;
354}
355
356// Get the actual RGB values for a 16-color ANSI index
357void get_16color_rgb(uint8_t color_index, uint8_t *r, uint8_t *g, uint8_t *b) {
358 // Same color table as rgb_to_16color()
359 static const uint8_t ansi_colors[16][3] = {
360 {0, 0, 0}, // 0: Black
361 {128, 0, 0}, // 1: Dark Red
362 {0, 128, 0}, // 2: Dark Green
363 {128, 128, 0}, // 3: Dark Yellow (Brown)
364 {0, 0, 128}, // 4: Dark Blue
365 {128, 0, 128}, // 5: Dark Magenta
366 {0, 128, 128}, // 6: Dark Cyan
367 {192, 192, 192}, // 7: Light Gray
368 {128, 128, 128}, // 8: Dark Gray
369 {255, 0, 0}, // 9: Bright Red
370 {0, 255, 0}, // 10: Bright Green
371 {255, 255, 0}, // 11: Bright Yellow
372 {0, 0, 255}, // 12: Bright Blue
373 {255, 0, 255}, // 13: Bright Magenta
374 {0, 255, 255}, // 14: Bright Cyan
375 {255, 255, 255} // 15: White
376 };
377
378 if (color_index >= 16) {
379 color_index = 7; // Default to light gray
380 }
381
382 *r = ansi_colors[color_index][0];
383 *g = ansi_colors[color_index][1];
384 *b = ansi_colors[color_index][2];
385}
386
387// Floyd-Steinberg dithering for 16-color terminals
388uint8_t rgb_to_16color_dithered(int r, int g, int b, int x, int y, int width, int height, rgb_error_t *error_buffer) {
389 // Add accumulated error from previous pixels
390 if (error_buffer) {
391 // Use size_t for index calculation to prevent integer overflow on large images
392 size_t error_idx = (size_t)y * (size_t)width + (size_t)x;
393 r += error_buffer[error_idx].r;
394 g += error_buffer[error_idx].g;
395 b += error_buffer[error_idx].b;
396
397 // Reset error for this pixel
398 error_buffer[error_idx].r = 0;
399 error_buffer[error_idx].g = 0;
400 error_buffer[error_idx].b = 0;
401 }
402
403 // Clamp values to [0, 255]
404 uint8_t r_clamped = clamp_rgb((int)r);
405 uint8_t g_clamped = clamp_rgb((int)g);
406 uint8_t b_clamped = clamp_rgb((int)b);
407
408 // Find the closest 16-color match
409 uint8_t closest_color = rgb_to_16color(r_clamped, g_clamped, b_clamped);
410
411 // Calculate quantization error if dithering is enabled
412 if (error_buffer) {
413 uint8_t actual_r, actual_g, actual_b;
414 get_16color_rgb(closest_color, &actual_r, &actual_g, &actual_b);
415
416 int error_r = r - (int)actual_r;
417 int error_g = g - (int)actual_g;
418 int error_b = b - (int)actual_b;
419
420 // Distribute error using Floyd-Steinberg weights:
421 // * 7/16
422 // 3/16 5/16 1/16
423
424 // Error to right pixel (x+1, y)
425 // Use size_t for all index calculations to prevent integer overflow
426 if (x + 1 < width) {
427 size_t right_idx = (size_t)y * (size_t)width + (size_t)(x + 1);
428 error_buffer[right_idx].r += (error_r * 7) / 16;
429 error_buffer[right_idx].g += (error_g * 7) / 16;
430 error_buffer[right_idx].b += (error_b * 7) / 16;
431 }
432
433 // Error to pixels on next row (y+1)
434 if (y + 1 < height) {
435 // Bottom-left pixel (x-1, y+1)
436 if (x - 1 >= 0) {
437 size_t bl_idx = (size_t)(y + 1) * (size_t)width + (size_t)(x - 1);
438 error_buffer[bl_idx].r += (error_r * 3) / 16;
439 error_buffer[bl_idx].g += (error_g * 3) / 16;
440 error_buffer[bl_idx].b += (error_b * 3) / 16;
441 }
442
443 // Bottom pixel (x, y+1)
444 size_t bottom_idx = (size_t)(y + 1) * (size_t)width + (size_t)x;
445 error_buffer[bottom_idx].r += (error_r * 5) / 16;
446 error_buffer[bottom_idx].g += (error_g * 5) / 16;
447 error_buffer[bottom_idx].b += (error_b * 5) / 16;
448
449 // Bottom-right pixel (x+1, y+1)
450 if (x + 1 < width) {
451 size_t br_idx = (size_t)(y + 1) * (size_t)width + (size_t)(x + 1);
452 error_buffer[br_idx].r += (error_r * 1) / 16;
453 error_buffer[br_idx].g += (error_g * 1) / 16;
454 error_buffer[br_idx].b += (error_b * 1) / 16;
455 }
456 }
457 }
458
459 return closest_color;
460}
461
462// Terminal capability-aware color function
463char *append_color_fg_for_mode(char *dst, uint8_t r, uint8_t g, uint8_t b, terminal_color_mode_t mode) {
464 switch (mode) {
465 case COLOR_MODE_TRUECOLOR:
466 return append_truecolor_fg(dst, r, g, b);
467
468 case COLOR_MODE_256_COLOR: {
469 uint8_t color_index = rgb_to_256color(r, g, b);
470 return append_256color_fg(dst, color_index);
471 }
472
473 case COLOR_MODE_16_COLOR: {
474 uint8_t color_index = rgb_to_16color(r, g, b);
475 return append_16color_fg(dst, color_index);
476 }
477
478 case COLOR_MODE_NONE:
479 case COLOR_MODE_AUTO:
480 default:
481 // No color output for monochrome mode or auto mode (fallback)
482 return dst;
483 }
484}
char * append_16color_fg(char *dst, uint8_t color_index)
Definition ansi_fast.c:280
char * append_16color_bg(char *dst, uint8_t color_index)
Definition ansi_fast.c:297
char * append_truecolor_fg(char *dst, uint8_t r, uint8_t g, uint8_t b)
Definition ansi_fast.c:26
void ansi_fast_init_256color(void)
Definition ansi_fast.c:205
uint8_t rgb_to_16color(uint8_t r, uint8_t g, uint8_t b)
Definition ansi_fast.c:314
uint8_t rgb_to_16color_dithered(int r, int g, int b, int x, int y, int width, int height, rgb_error_t *error_buffer)
Definition ansi_fast.c:388
void ansi_rle_init(ansi_rle_context_t *ctx, char *buffer, size_t capacity, ansi_color_mode_t mode)
Definition ansi_fast.c:129
void ansi_fast_init(void)
Definition ansi_fast.c:198
char * append_truecolor_fg_bg(char *dst, uint8_t fg_r, uint8_t fg_g, uint8_t fg_b, uint8_t bg_r, uint8_t bg_g, uint8_t bg_b)
Definition ansi_fast.c:82
void ansi_rle_finish(ansi_rle_context_t *ctx)
Definition ansi_fast.c:184
void get_16color_rgb(uint8_t color_index, uint8_t *r, uint8_t *g, uint8_t *b)
Definition ansi_fast.c:357
void ansi_rle_add_pixel(ansi_rle_context_t *ctx, uint8_t r, uint8_t g, uint8_t b, char ascii_char)
Definition ansi_fast.c:142
char * append_256color_fg(char *dst, uint8_t color_index)
Definition ansi_fast.c:222
char * append_color_fg_for_mode(char *dst, uint8_t r, uint8_t g, uint8_t b, terminal_color_mode_t mode)
Definition ansi_fast.c:463
uint8_t rgb_to_256color(uint8_t r, uint8_t g, uint8_t b)
Definition ansi_fast.c:230
void ansi_fast_init_16color(void)
Definition ansi_fast.c:257
char * append_truecolor_bg(char *dst, uint8_t r, uint8_t g, uint8_t b)
Definition ansi_fast.c:56
global_dec3_cache_t g_dec3_cache
Definition ascii_simd.c:25
void init_dec3(void)
Definition ascii_simd.c:66
void ascii_simd_init(void)
Definition ascii_simd.c:98