ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
ascii.c
Go to the documentation of this file.
1
8#include <stdint.h>
9#include <sys/types.h>
10#include <stdio.h>
11#include <stdlib.h>
12#include <string.h>
13#include <sys/stat.h>
14#include <time.h>
15#include <math.h>
16
19
21#include <ascii-chat/common.h>
33
34/* ============================================================================
35 * ASCII Art Video Processing
36 * ============================================================================
37 */
38
39/*
40 * Resize a source image to a terminal viewport while preserving its visual
41 * aspect ratio. The source is cropped to the viewport's aspect ratio first,
42 * so the output fills the viewport instead of introducing letterbox bars.
43 * Terminal cells are approximately twice as tall as they are wide, hence the
44 * factor of two in the source crop ratio.
45 */
46static void image_resize_cover(const image_t *source, image_t *dest, ssize_t viewport_width, ssize_t viewport_height) {
47 if (!source || !dest || source->w <= 0 || source->h <= 0 || dest->w <= 0 || dest->h <= 0 || viewport_width <= 0 ||
48 viewport_height <= 0) {
49 SET_ERRNO(ERROR_INVALID_PARAM, "image_resize_cover: invalid image or viewport dimensions");
50 return;
51 }
52
53 const double source_aspect = (double)source->w / (double)source->h;
54 const double viewport_aspect = (double)viewport_width / ((double)viewport_height * 2.0);
55 int crop_width = source->w;
56 int crop_height = source->h;
57 int crop_x = 0;
58 int crop_y = 0;
59
60 if (source_aspect > viewport_aspect) {
61 crop_width = (int)lround((double)source->h * viewport_aspect);
62 crop_width = crop_width < 1 ? 1 : crop_width;
63 crop_width = crop_width > source->w ? source->w : crop_width;
64 crop_x = (source->w - crop_width) / 2;
65 } else if (source_aspect < viewport_aspect) {
66 crop_height = (int)lround((double)source->w / viewport_aspect);
67 crop_height = crop_height < 1 ? 1 : crop_height;
68 crop_height = crop_height > source->h ? source->h : crop_height;
69 crop_y = (source->h - crop_height) / 2;
70 }
71
72 for (int y = 0; y < dest->h; y++) {
73 int source_y = crop_y + (int)(((int64_t)y * crop_height) / dest->h);
74 const rgb_pixel_t *source_row = source->pixels + ((size_t)source_y * (size_t)source->w);
75 rgb_pixel_t *dest_row = dest->pixels + ((size_t)y * (size_t)dest->w);
76 for (int x = 0; x < dest->w; x++) {
77 int source_x = crop_x + (int)(((int64_t)x * crop_width) / dest->w);
78 dest_row[x] = source_row[source_x];
79 }
80 }
81}
82
83asciichat_error_t ascii_write_init(int fd, bool reset_terminal) {
84 // Validate file descriptor
85 if (fd < 0) {
86 log_error("Invalid file descriptor %d", fd);
88 }
89
90 // Only apply terminal control sequences if:
91 // 1. reset_terminal is true (caller wants terminal reset)
92 // 2. terminal_should_use_control_sequences() confirms it's safe (TTY, not snapshot, not testing)
93 if (reset_terminal && terminal_should_use_control_sequences(fd)) {
94 console_clear(fd);
95 cursor_reset(fd);
96
97 // Disable echo using platform abstraction
98 if (terminal_set_echo(false) != 0) {
99 log_error("Failed to disable echo for fd %d", fd);
100 return ERROR_TERMINAL;
101 }
102 // Hide cursor using platform abstraction
103 if (terminal_cursor_hide() != 0) {
104 log_warn("Failed to hide cursor");
105 }
106 }
107 log_dev("ASCII writer initialized");
108 return ASCIICHAT_OK;
109}
110
111char *ascii_convert(image_t *original, const ssize_t width, const ssize_t height, const bool color,
112 const bool _aspect_ratio, const bool stretch, const char *palette_chars,
113 const char luminance_palette[256]) {
114 if (original == NULL || !palette_chars || !luminance_palette) {
115 log_error("ascii_convert: invalid parameters");
116 return NULL;
117 }
118
119 // Check for empty strings
120 if (palette_chars[0] == '\0' || luminance_palette[0] == '\0') {
121 log_error("ascii_convert: empty palette strings");
122 return NULL;
123 }
124
125 // Start with the target dimensions requested by the user (or detected from
126 // the terminal). These can be modified by aspect_ratio() if stretching is
127 // disabled and one of the dimensions was left to be calculated
128 // automatically.
129 ssize_t resized_width = width;
130 ssize_t resized_height = height;
131
132 // If stretch is enabled, use full dimensions, otherwise calculate aspect ratio
133 if (_aspect_ratio) {
134 // The server now provides images at width*2 x height pixels
135 // The aspect_ratio function will handle terminal character aspect ratio
136 aspect_ratio(original->w, original->h, resized_width, resized_height, stretch, &resized_width, &resized_height);
137 }
138
139 // Calculate padding for centering
140 size_t pad_width = 0;
141 size_t pad_height = 0;
142
143 if (_aspect_ratio) {
144 // Only calculate padding when not stretching
145 ssize_t pad_width_ss = width > resized_width ? (width - resized_width) / 2 : 0;
146 pad_width = (size_t)pad_width_ss;
147
148 ssize_t pad_height_ss = height > resized_height ? (height - resized_height) / 2 : 0;
149 pad_height = (size_t)pad_height_ss;
150 }
151
152 // Resize the captured frame to the aspect-correct dimensions.
153 if (resized_width <= 0 || resized_height <= 0) {
154 log_error("Invalid dimensions for resize: width=%zd, height=%zd", resized_width, resized_height);
155 return NULL;
156 }
157
158 // Validate dimensions fit in image_t's int fields before casting
159 if (resized_width > INT_MAX || resized_height > INT_MAX) {
160 log_error("Dimensions exceed INT_MAX: width=%zd, height=%zd", resized_width, resized_height);
161 return NULL;
162 }
163
164 // Always resize to target dimensions
165 image_t *resized = image_new((size_t)resized_width, (size_t)resized_height);
166 if (!resized) {
167 log_error("Failed to allocate resized image");
168 return NULL;
169 }
170
171 image_clear(resized);
172 image_resize(original, resized);
173
174 char *ascii;
175 if (color) {
176 // Check for half-block mode first
177 if (GET_OPTION(render_mode) == RENDER_MODE_HALF_BLOCK) {
178 const uint8_t *rgb_data = (const uint8_t *)resized->pixels;
179#if SIMD_SUPPORT_NEON
180 // Use NEON half-block renderer (optimized SIMD path)
181 log_dev("Using NEON halfblock renderer");
182 ascii = rgb_to_truecolor_halfblocks_neon(rgb_data, resized->w, resized->h, 0);
183#else
184 // Fallback to scalar halfblock renderer (works on all platforms)
185 log_dev("Using scalar halfblock renderer (NEON not available)");
186 ascii = rgb_to_truecolor_halfblocks_scalar(rgb_data, resized->w, resized->h, 0);
187#endif
188 } else {
189#ifdef SIMD_SUPPORT
190 // Standard color modes (foreground/background)
191 bool use_background = (GET_OPTION(render_mode) == RENDER_MODE_BACKGROUND);
192 ascii = image_print_color_simd(resized, use_background, false, palette_chars);
193#else
194 if (GET_OPTION(render_mode) == RENDER_MODE_BACKGROUND) {
195 ascii = image_print_color_background(resized, palette_chars);
196 } else {
197 ascii = image_print_color_utf8(resized, palette_chars);
198 }
199#endif
200 }
201 } else {
202 // Use grayscale/monochrome conversion with client's palette
203 ascii = image_print(resized, palette_chars);
204 }
205
206 if (!ascii) {
207 log_error("Failed to convert image to ASCII");
208 image_destroy(resized);
209 return NULL;
210 }
211
212 size_t ascii_len = strlen(ascii);
213 if (ascii_len == 0) {
214 log_error("ASCII conversion returned empty string (resized dimensions: %dx%d)", resized->w, resized->h);
215 SAFE_FREE(ascii);
216 image_destroy(resized);
217 return NULL;
218 }
219
220 char *ascii_width_padded = ascii_pad_frame_width(ascii, pad_width);
221 SAFE_FREE(ascii);
222
223 char *ascii_padded = ascii_pad_frame_height(ascii_width_padded, pad_height);
224 SAFE_FREE(ascii_width_padded);
225
226 // Only destroy resized if we allocated it (not when using original directly)
227 image_destroy(resized);
228
229 return ascii_padded;
230}
231
232// Capability-aware ASCII conversion using terminal capabilities
233char *ascii_convert_with_capabilities(image_t *original, const ssize_t width, const ssize_t height,
234 const terminal_capabilities_t *caps, const bool use_aspect_ratio,
235 const bool stretch, const char *palette_chars) {
236
237 if (original == NULL || caps == NULL) {
238 log_error("Invalid parameters for ascii_convert_with_capabilities");
239 return NULL;
240 }
241
242 // Validate original image dimensions to detect corruption
243 if (original->w <= 0 || original->w > 10000 || original->h <= 0 || original->h > 10000) {
244 log_error("Invalid original image dimensions detected: w=%d, h=%d (likely corrupted)", original->w, original->h);
245 return NULL;
246 }
247
248 if (original->pixels == NULL) {
249 log_error("Original image pixels pointer is NULL");
250 return NULL;
251 }
252
253 // Start with the target dimensions requested by the user.
254 ssize_t resized_width = width;
255 ssize_t resized_height = height;
256
257 // Preserve aspect ratio by cropping the source to cover the viewport. The
258 // destination dimensions remain the full requested viewport dimensions.
259
260 // Half-block mode doubles height for 2x vertical resolution (AFTER aspect ratio)
261 if (caps->render_mode == RENDER_MODE_HALF_BLOCK) {
262 resized_height = resized_height * 2;
263 }
264
265 // Cover rendering fills the viewport, so no aspect-ratio padding is needed.
266 size_t pad_width = 0;
267 size_t pad_height = 0;
268
269 // Resize the captured frame to the aspect-correct dimensions
270 if (resized_width <= 0 || resized_height <= 0) {
271 log_error("Invalid dimensions for resize: width=%zd, height=%zd", resized_width, resized_height);
272 return NULL;
273 }
274
275 // Validate dimensions fit in image_t's int fields before casting
276 if (resized_width > INT_MAX || resized_height > INT_MAX) {
277 log_error("Dimensions exceed INT_MAX: width=%zd, height=%zd", resized_width, resized_height);
278 return NULL;
279 }
280
281 // PROFILING: Time image allocation and resize
282 START_TIMER("image_alloc");
283 uint64_t prof_alloc_start_ns = time_get_ns();
284
285 image_t *resized = image_new((size_t)resized_width, (size_t)resized_height);
286 if (!resized) {
287 log_error("Failed to allocate resized image");
288 return NULL;
289 }
290
291 image_clear(resized);
292
293 uint64_t prof_alloc_end_ns = time_get_ns();
294 STOP_TIMER_AND_LOG_EVERY(dev, 3 * NS_PER_SEC_INT, 5 * NS_PER_MS_INT, "image_alloc",
295 "IMAGE_ALLOC: Alloc+clear complete (%.2f ms)");
296
297 START_TIMER("image_resize");
298 uint64_t prof_resize_start_ns = prof_alloc_end_ns;
299
300 if (use_aspect_ratio && !stretch) {
301 image_resize_cover(original, resized, resized_width, resized_height);
302 } else {
303 image_resize(original, resized);
304 }
305
306 uint64_t prof_resize_end_ns = time_get_ns();
307 STOP_TIMER_AND_LOG_EVERY(dev, 3 * NS_PER_SEC_INT, 5 * NS_PER_MS_INT, "image_resize",
308 "IMAGE_RESIZE: Resize complete (%.2f ms)");
309
310 // Check original image before resize (validate dimensions to prevent integer overflow)
311 int orig_black = 0, orig_total = 0;
312 if (original->w > 0 && original->w <= 10000 && original->h > 0 && original->h <= 10000) {
313 int64_t total_pixels = (int64_t)original->w * original->h;
314 int sample_size = (total_pixels < 1000) ? (int)total_pixels : 1000;
315 for (int i = 0; i < sample_size; i++) {
316 rgb_pixel_t p = original->pixels[i];
317 orig_total++;
318 if (p.r == 0 && p.g == 0 && p.b == 0)
319 orig_black++;
320 }
322 ">>> ORIGINAL image: %d/%d pixels are black (%.1f%%), first pixel=RGB(%d,%d,%d)\n", orig_black,
323 orig_total, orig_total > 0 ? 100.0 * orig_black / orig_total : 0, original->pixels[0].r,
324 original->pixels[0].g, original->pixels[0].b);
325 } else {
326 log_warn("Skipping debug check: corrupted image dimensions (w=%d, h=%d)", original->w, original->h);
327 }
328
329 // PROFILING: Time ASCII print
330 uint64_t prof_print_start_ns = prof_resize_end_ns;
331
332 // DEBUG: Log dimensions going to renderer
334 "ascii_convert: original=%dx%d, requested=%zdx%zd, resized=%dx%d, pad=%zux%zu (mode=%d)", original->w,
335 original->h, width, height, resized->w, resized->h, pad_width, pad_height, caps->render_mode);
336
337 // Check if image is all black
338 int black_pixels = 0, total_pixels = 0;
339 for (int i = 0; i < resized->w * resized->h && i < 1000; i++) {
340 rgb_pixel_t p = resized->pixels[i];
341 total_pixels++;
342 if (p.r == 0 && p.g == 0 && p.b == 0)
343 black_pixels++;
344 }
346 ">>> Resized image check: %d/%d pixels are black (%.1f%%), first pixel=RGB(%d,%d,%d)\n", black_pixels,
347 total_pixels, total_pixels > 0 ? 100.0 * black_pixels / total_pixels : 0, resized->pixels[0].r,
348 resized->pixels[0].g, resized->pixels[0].b);
349
350 // Apply the target client's filter after resizing, so it is scoped to this
351 // client and never mutates the shared source frame.
352 if (caps->color_filter != COLOR_FILTER_NONE) {
353 const float time_seconds = (float)time_get_ns() / (float)NS_PER_SEC_INT;
354 if (apply_color_filter((uint8_t *)resized->pixels, (uint32_t)resized->w, (uint32_t)resized->h,
355 (uint32_t)(resized->w * sizeof(rgb_pixel_t)), caps->color_filter, time_seconds) != 0) {
356 log_warn("Failed to apply client color filter %d", caps->color_filter);
357 }
358 }
359
360 // Use the capability-aware image printing function with client's palette
361 START_TIMER("image_print_with_capabilities");
362 char *ascii = image_print_with_capabilities(resized, caps, palette_chars);
363
364 uint64_t prof_print_end_ns = time_get_ns();
365 STOP_TIMER_AND_LOG_EVERY(dev, 3 * NS_PER_SEC_INT, 5 * NS_PER_MS_INT, "image_print_with_capabilities",
366 "IMAGE_PRINT: Print complete (%.2f ms)");
367
368 uint64_t alloc_time_us = time_ns_to_us(time_elapsed_ns(prof_alloc_start_ns, prof_alloc_end_ns));
369 uint64_t resize_time_us = time_ns_to_us(time_elapsed_ns(prof_resize_start_ns, prof_resize_end_ns));
370 uint64_t print_time_us = time_ns_to_us(time_elapsed_ns(prof_print_start_ns, prof_print_end_ns));
371
372 // PROFILING: Time padding
373 START_TIMER("ascii_padding");
374 uint64_t prof_pad_start_ns = time_get_ns();
375
376 if (!ascii) {
377 log_error("Failed to convert image to ASCII using terminal capabilities");
378 image_destroy(resized);
379 return NULL;
380 }
381
382 size_t ascii_len = strlen(ascii);
383 if (ascii_len == 0) {
384 log_error("Capability-aware ASCII conversion returned empty string (resized dimensions: %dx%d)", resized->w,
385 resized->h);
386 SAFE_FREE(ascii);
387 image_destroy(resized);
388 return NULL;
389 }
390
391 char *ascii_width_padded = ascii_pad_frame_width(ascii, pad_width);
392 SAFE_FREE(ascii);
393
394 char *ascii_padded = ascii_pad_frame_height(ascii_width_padded, pad_height);
395 SAFE_FREE(ascii_width_padded);
396
397 uint64_t prof_pad_end_ns = time_get_ns();
398 STOP_TIMER_AND_LOG_EVERY(dev, 3 * NS_PER_SEC_INT, 2 * NS_PER_MS_INT, "ascii_padding",
399 "ASCII_PADDING: Padding complete (%.2f ms)");
400
401 uint64_t pad_time_ns = time_elapsed_ns(prof_pad_start_ns, prof_pad_end_ns);
402 char alloc_str[32], resize_str[32], print_str[32], pad_str[32], total_str[32];
403 time_pretty(alloc_time_us * 1000, -1, alloc_str, sizeof(alloc_str));
404 time_pretty(resize_time_us * 1000, -1, resize_str, sizeof(resize_str));
405 time_pretty(print_time_us * 1000, -1, print_str, sizeof(print_str));
406 time_pretty(pad_time_ns, -1, pad_str, sizeof(pad_str));
407 uint64_t total_time_ns = (alloc_time_us + resize_time_us + print_time_us) * 1000 + pad_time_ns;
408 time_pretty(total_time_ns, -1, total_str, sizeof(total_str));
409 log_dev("ASCII_BREAKDOWN: alloc=%s, resize=%s, print=%s, pad=%s (total=%s)", alloc_str, resize_str, print_str,
410 pad_str, total_str);
411
412 image_destroy(resized);
413
414 return ascii_padded;
415}
416
417// NOTE: ascii_convert_with_custom_palette removed - use ascii_convert_with_capabilities() with enhanced
418// terminal_capabilities_t
419
421 if (frame == NULL) {
422 log_warn("Attempted to write NULL frame");
423 return ERROR_INVALID_PARAM;
424 }
425
426 // Only reset cursor if output is connected to a TTY (not piped/redirected)
427 if (terminal_should_use_control_sequences(STDOUT_FILENO)) {
428 cursor_reset(STDOUT_FILENO);
429 }
430
431 size_t frame_len = strlen(frame);
432 // Write all frame data with automatic retry on transient errors
433 platform_write_all(STDOUT_FILENO, frame, frame_len);
434
435 // Flush C stdio buffer and terminal to ensure piped output is written immediately
436 (void)fflush(stdout);
437 terminal_flush(STDOUT_FILENO);
438
439 return ASCIICHAT_OK;
440}
441
442void ascii_write_destroy(int fd, bool reset_terminal) {
443#if PLATFORM_WINDOWS
444 (void)fd; // Unused on Windows - terminal operations use stdout directly
445#endif
446 // console_clear(fd);
447 // cursor_reset(fd);
448 // Only restore terminal state if:
449 // 1. reset_terminal is true (caller wants terminal restore)
450 // 2. terminal_should_use_control_sequences() confirms it's safe (TTY, not snapshot, not testing)
451 if (reset_terminal && terminal_should_use_control_sequences(fd)) {
452 // Show cursor using platform abstraction
453 if (terminal_cursor_show() != 0) {
454 log_warn("Failed to show cursor");
455 }
456
457 // Re-enable echo using platform abstraction
458 if (terminal_set_echo(true) != 0) {
459 log_warn("Failed to re-enable echo");
460 }
461 }
462 log_debug("ASCII writer destroyed");
463}
464
465/*
466 * Pads each line of an ASCII frame with a given number of leading space
467 * characters. The function allocates a new buffer large enough to hold the
468 * padded frame and returns a pointer to it. The caller is responsible for
469 * freeing the returned buffer.
470 *
471 * Parameters:
472 * frame The original, null-terminated ASCII frame. It is expected to
473 * contain `\n` at the end of every visual row.
474 * pad_left How many space characters to add in front of every visual row.
475 *
476 * Returns:
477 * A newly allocated, null-terminated string that contains the padded frame
478 * on success, or NULL if either `frame`.
479 */
480char *ascii_pad_frame_width(const char *frame, size_t pad_left) {
481 if (!frame) {
482 SET_ERRNO(ERROR_INVALID_PARAM, "ascii_pad_frame_width: frame is NULL");
483 return NULL;
484 }
485
486 if (pad_left == 0) {
487 // Nothing to do; return a copy so the caller can free it safely without
488 // worrying about the original allocation strategy.
489 size_t orig_len = strlen(frame);
490 char *copy;
491 copy = SAFE_MALLOC(orig_len + 1, char *);
492 SAFE_MEMCPY(copy, orig_len + 1, frame, orig_len + 1);
493 return copy;
494 }
495
496 // Count how many visual rows we have (lines terminated by '\n') to determine
497 // the final buffer size.
498 size_t line_count = 1; // There is always at least the first line
499 const char *char_in_frame = frame;
500 while (*char_in_frame) {
501 if (*char_in_frame == '\n') {
502 line_count++;
503 }
504 char_in_frame++;
505 }
506
507 // Total length of the source plus padding.
508 const size_t frame_len = strlen(frame);
509 const size_t left_padding_len = line_count * pad_left;
510 const size_t total_len = frame_len + left_padding_len;
511
512 char *buffer;
513 buffer = SAFE_MALLOC(total_len + 1, char *);
514
515 // Build the padded frame.
516 bool at_line_start = true;
517 const char *src = frame;
518 char *position = buffer;
519
520 while (*src) {
521 if (at_line_start) {
522 // Insert the requested amount of spaces in front of every visual row.
523 size_t remaining = (size_t)((ptrdiff_t)(buffer + total_len + 1) - (ptrdiff_t)position);
524 SAFE_MEMSET(position, remaining, ' ', (size_t)pad_left);
525 position += pad_left;
526 at_line_start = false;
527 }
528
529 *position++ = *src;
530
531 if (*src == '\n') {
532 at_line_start = true;
533 }
534
535 src++;
536 }
537
538 *position = '\0';
539 return buffer;
540}
541
550static int ansi_visual_width(const char *data, int data_len) {
551 int visual_width = 0;
552 int i = 0;
553
554 while (i < data_len) {
555 if (data[i] == '\033' && i + 1 < data_len && data[i + 1] == '[') {
556 // Skip ANSI CSI sequence: ESC [ <params> <terminator>
557 i += 2; // Skip ESC [
558 while (i < data_len) {
559 char c = data[i];
560 i++;
561 // ANSI CSI sequences end with a byte in the range 0x40-0x7E (@ through ~)
562 if (c >= '@' && c <= '~') {
563 break;
564 }
565 }
566 } else {
567 // Visible character
568 visual_width++;
569 i++;
570 }
571 }
572
573 return visual_width;
574}
575
585static int ansi_truncate_to_visual_width(const char *data, int data_len, int target_width) {
586 int visual_width = 0;
587 int i = 0;
588
589 while (i < data_len && visual_width < target_width) {
590 if (data[i] == '\033' && i + 1 < data_len && data[i + 1] == '[') {
591 // Skip ANSI CSI sequence: ESC [ <params> <terminator>
592 i += 2; // Skip ESC [
593 while (i < data_len) {
594 char c = data[i];
595 i++;
596 // ANSI CSI sequences end with a byte in the range 0x40-0x7E (@ through ~)
597 if (c >= '@' && c <= '~') {
598 break;
599 }
600 }
601 } else {
602 // Visible character - count it and advance
603 visual_width++;
604 i++;
605 }
606 }
607
608 return i;
609}
610
625char *ascii_create_grid(ascii_frame_source_t *sources, int source_count, int width, int height, size_t *out_size) {
626 if (!sources || source_count <= 0 || width <= 0 || height <= 0 || !out_size) {
627 return NULL;
628 }
629
630 // If no sources, return empty frame
631
632 // If only one source, center it properly to maintain aspect ratio and look good
633 if (source_count == 1) {
634 // Create a frame of the target size filled with spaces
635 // Check for integer overflow before multiplication
636 size_t w = (size_t)width;
637 size_t h = (size_t)height;
638 size_t w_times_h;
639 if (checked_size_mul(w, h, &w_times_h) != ASCIICHAT_OK) {
640 SET_ERRNO(ERROR_INVALID_PARAM, "ascii_create_grid: dimensions would overflow: %dx%d", width, height);
641 return NULL;
642 }
643
644 size_t w_times_h_plus_h;
645 if (checked_size_add(w_times_h, h, &w_times_h_plus_h) != ASCIICHAT_OK) {
646 SET_ERRNO(ERROR_INVALID_PARAM, "ascii_create_grid: buffer size would overflow: %dx%d", width, height);
647 return NULL;
648 }
649
650 size_t target_size;
651 if (checked_size_add(w_times_h_plus_h, 1, &target_size) != ASCIICHAT_OK) {
652 SET_ERRNO(ERROR_INVALID_PARAM, "ascii_create_grid: buffer size would overflow: %dx%d", width, height);
653 return NULL;
654 }
655 char *result;
656 result = SAFE_MALLOC(target_size, char *);
657 SAFE_MEMSET(result, target_size, ' ', target_size - 1);
658 result[target_size - 1] = '\0';
659
660 // Add newlines at the end of each row
661 for (int row = 0; row < height; row++) {
662 result[row * (width + 1) + width] = '\n';
663 }
664
665 // Copy the source frame into the result, line by line, centering it
666 // Handle NULL frame_data gracefully
667 const char *src_data = sources[0].frame_data;
668 int src_pos = 0;
669 int src_size = (int)sources[0].frame_size;
670
671 // If source data is NULL or empty, just return the empty frame
672 if (!src_data || src_size <= 0) {
673 *out_size = target_size - 1; // Don't count null terminator
674 return result;
675 }
676
677 // Count lines in source to calculate vertical padding
678 int src_lines = 0;
679 for (int i = 0; i < src_size; i++) {
680 if (src_data[i] == '\n')
681 src_lines++;
682 }
683
684 int v_padding = (height - src_lines) / 2;
685 if (v_padding < 0)
686 v_padding = 0;
687
688 int dst_row = v_padding;
689 src_pos = 0;
690
691 while (src_pos < src_size && dst_row < height) {
692 // Find end of current line in source
693 int line_start = src_pos;
694 int line_len = 0;
695 while (src_pos < src_size && src_data[src_pos] != '\n') {
696 line_len++;
697 src_pos++;
698 }
699
700 // Calculate visual width of the line (excluding ANSI escape sequences)
701 int visual_line_width = ansi_visual_width(&src_data[line_start], line_len);
702
703 // Calculate horizontal padding to center the line based on visual width
704 int h_padding = (width - visual_line_width) / 2;
705 if (h_padding < 0)
706 h_padding = 0;
707
708 // Copy line to result with padding
709 // Use size_t for position calculation to prevent integer underflow
710 size_t row_offset = (size_t)dst_row * (size_t)(width + 1);
711 size_t dst_pos = row_offset + (size_t)h_padding;
712 int max_visual_width = width - h_padding;
713 // Truncate to visual width while preserving complete ANSI sequences
714 int copy_len = ansi_truncate_to_visual_width(&src_data[line_start], line_len, max_visual_width);
715
716 if (copy_len > 0 && dst_pos + (size_t)copy_len < target_size) {
717 SAFE_MEMCPY(&result[dst_pos], target_size - dst_pos, &src_data[line_start], (size_t)copy_len);
718 }
719
720 // Skip newline in source
721 if (src_pos < src_size && src_data[src_pos] == '\n') {
722 src_pos++;
723 }
724
725 dst_row++;
726 }
727
728 *out_size = target_size - 1; // Don't count null terminator
729 return result;
730 }
731
732 // Multiple sources: create grid layout
733 // Calculate grid dimensions that maximize the use of terminal space
734 // Character aspect ratio: terminal chars are typically ~2x taller than wide
735 float char_aspect = 2.0f;
736
737 int grid_cols, grid_rows;
738 float best_score = -1.0f;
739 int best_cols = 1;
740 int best_rows = source_count;
741
742 // Try all possible grid configurations
743 for (int test_cols = 1; test_cols <= source_count; test_cols++) {
744 int test_rows = (int)ceil((double)source_count / test_cols);
745
746 // Skip configurations with too many empty cells
747 int empty_cells = (test_cols * test_rows) - source_count;
748 if (empty_cells > source_count / 2)
749 continue; // Don't waste more than 50% space
750
751 // Calculate the size each cell would have
752 int cell_width = (width - (test_cols - 1)) / test_cols; // -1 per separator
753 int cell_height = (height - (test_rows - 1)) / test_rows; // -1 per separator
754
755 // Skip if cells would be too small
756 if (cell_width < 10 || cell_height < 3)
757 continue;
758
759 // Calculate the aspect ratio of each cell (accounting for char aspect)
760 float cell_aspect = ((float)cell_width / (float)cell_height) / char_aspect;
761
762 // Score based on how close to square (1:1) each video cell would be
763 // This naturally adapts to any terminal size
764 float aspect_score = 1.0f - fabsf(logf(cell_aspect)); // log makes it symmetric around 1
765 if (aspect_score < 0)
766 aspect_score = 0;
767
768 // Bonus for better space utilization
769 float utilization = (float)source_count / (float)(test_cols * test_rows);
770
771 // For 2 clients specifically, heavily weight the aspect score
772 // This makes 2 clients naturally go horizontal on wide terminals and vertical on tall ones
773 float total_score;
774 if (source_count == 2) {
775 // For 2 clients, we want the layout that gives the most square-ish cells
776 total_score = aspect_score * 0.9f + utilization * 0.1f;
777 } else {
778 // For 3+ clients, balance aspect ratio with space utilization
779 total_score = aspect_score * 0.7f + utilization * 0.3f;
780 }
781
782 // Small bonus for simpler grids (prefer 2x2 over 3x1, etc.)
783 if (test_cols == test_rows) {
784 total_score += 0.05f; // Slight preference for square grids
785 }
786
787 if (total_score > best_score) {
788 best_score = total_score;
789 best_cols = test_cols;
790 best_rows = test_rows;
791 }
792 }
793
794 grid_cols = best_cols;
795 grid_rows = best_rows;
796
797 // Calculate dimensions for each cell (leave 1 char for separators)
798 int cell_width = (width - (grid_cols - 1)) / grid_cols;
799 int cell_height = (height - (grid_rows - 1)) / grid_rows;
800
801 if (cell_width < 10 || cell_height < 3) {
802 // Too small for grid layout, just use first source
803 char *result;
804 result = SAFE_MALLOC(sources[0].frame_size + 1, char *);
805 if (sources[0].frame_data && sources[0].frame_size > 0) {
806 SAFE_MEMCPY(result, sources[0].frame_size + 1, sources[0].frame_data, sources[0].frame_size);
807 result[sources[0].frame_size] = '\0';
808 *out_size = sources[0].frame_size;
809 } else {
810 // Handle NULL or empty frame data
811 result[0] = '\0';
812 *out_size = 0;
813 }
814 return result;
815 }
816
817 // Allocate mixed frame buffer
818 // Check for integer overflow before multiplication
819 size_t w_sz = (size_t)width;
820 size_t h_sz = (size_t)height;
821 if (w_sz > SIZE_MAX / h_sz) {
822 SET_ERRNO(ERROR_INVALID_PARAM, "ascii_create_grid: dimensions would overflow: %dx%d", width, height);
823 return NULL;
824 }
825 size_t mixed_size = w_sz * h_sz + h_sz + 1; // +1 for null terminator, +height for newlines
826 char *mixed_frame;
827 mixed_frame = SAFE_MALLOC(mixed_size, char *);
828
829 // Initialize mixed frame with spaces
830 SAFE_MEMSET(mixed_frame, mixed_size, ' ', mixed_size - 1);
831 mixed_frame[mixed_size - 1] = '\0';
832
833 // Add newlines at the end of each row
834 for (int row = 0; row < height; row++) {
835 mixed_frame[row * (width + 1) + width] = '\n';
836 }
837
838 // Place each video source in the grid
839 for (int src = 0; src < source_count; src++) {
840 int grid_row = src / grid_cols;
841 int grid_col = src % grid_cols;
842
843 // Calculate position in mixed frame
844 int start_row = grid_row * (cell_height + 1); // +1 for separator
845 int start_col = grid_col * (cell_width + 1); // +1 for separator
846
847 // Parse source frame line by line and place in grid
848 const char *src_data = sources[src].frame_data;
849 int src_row = 0;
850 int src_pos = 0;
851
852 while (src_pos < (int)sources[src].frame_size && src_row < cell_height && start_row + src_row < height) {
853 // Find end of current line in source
854 int line_start = src_pos;
855 while (src_pos < (int)sources[src].frame_size && src_data[src_pos] != '\n') {
856 src_pos++;
857 }
858 int line_len = src_pos - line_start;
859
860 // Copy line to mixed frame (truncate if too long)
861 // Truncate to visual cell width while preserving complete ANSI sequences
862 int copy_len = ansi_truncate_to_visual_width(src_data + line_start, line_len, cell_width);
863 // Check that visual width of truncated line fits
864 int truncated_visual_width = ansi_visual_width(src_data + line_start, copy_len);
865 if (copy_len > 0 && start_col + truncated_visual_width <= width) {
866 int mixed_pos = (start_row + src_row) * (width + 1) + start_col;
867 SAFE_MEMCPY(mixed_frame + mixed_pos, mixed_size - (size_t)mixed_pos, src_data + line_start, (size_t)copy_len);
868 }
869
870 // Move to next line
871 if (src_pos < (int)sources[src].frame_size && src_data[src_pos] == '\n') {
872 src_pos++;
873 }
874 src_row++;
875 }
876
877 // Draw separators with bounds checking to prevent buffer overflow
878 if (grid_col < grid_cols - 1 && start_col + cell_width < width) {
879 // Vertical separator
880 for (int row = start_row; row < start_row + cell_height && row < height; row++) {
881 size_t idx = (size_t)row * (size_t)(width + 1) + (size_t)(start_col + cell_width);
882 if (idx < mixed_size - 1) { // -1 to preserve null terminator
883 mixed_frame[idx] = '|';
884 }
885 }
886 }
887
888 if (grid_row < grid_rows - 1 && start_row + cell_height < height) {
889 // Horizontal separator
890 for (int col = start_col; col < start_col + cell_width && col < width; col++) {
891 size_t idx = (size_t)(start_row + cell_height) * (size_t)(width + 1) + (size_t)col;
892 if (idx < mixed_size - 1) { // -1 to preserve null terminator
893 mixed_frame[idx] = '_';
894 }
895 }
896 // Corner character where separators meet
897 if (grid_col < grid_cols - 1 && start_col + cell_width < width) {
898 size_t idx = (size_t)(start_row + cell_height) * (size_t)(width + 1) + (size_t)(start_col + cell_width);
899 if (idx < mixed_size - 1) { // -1 to preserve null terminator
900 mixed_frame[idx] = '+';
901 }
902 }
903 }
904 }
905
906 *out_size = strlen(mixed_frame);
907 return mixed_frame;
908}
909
925char *ascii_pad_frame_height(const char *frame, size_t pad_top) {
926 if (!frame) {
927 return NULL;
928 }
929
930 if (pad_top == 0) {
931 // Nothing to do; return a copy because the caller knows to free() the value.
932 size_t orig_len = strlen(frame);
933 char *copy;
934 copy = SAFE_MALLOC(orig_len + 1, char *);
935 SAFE_MEMCPY(copy, orig_len + 1, frame, orig_len + 1);
936 return copy;
937 }
938
939 // Calculate buffer size needed
940 // Each padding row needs: 1 newline character per padding row
941 size_t frame_len = strlen(frame);
942 size_t top_padding_len = pad_top; // 1 newline per padding row
943 size_t total_len = top_padding_len + frame_len;
944
945 char *buffer;
946 buffer = SAFE_MALLOC(total_len + 1, char *);
947
948 char *position = buffer;
949
950 // Add top padding with plain newlines
951 // Use plain newlines instead of ANSI escape sequences so the output works
952 // when redirected to pipes or files, not just TTY
953 for (size_t i = 0; i < pad_top; i++) {
954 *position++ = '\n';
955 }
956
957 // Copy the original frame
958 size_t remaining = frame_len + 1;
959 SAFE_MEMCPY(position, remaining, frame, frame_len);
960 position += frame_len;
961 *position = '\0';
962
963 return buffer;
964}
965
978char *image_print_with_capabilities(const image_t *image, const terminal_capabilities_t *caps, const char *palette) {
979 if (!image || !caps || !palette) {
980 return NULL;
981 }
982
983 terminal_color_mode_t color_level = caps->color_level;
984 render_mode_t render_mode = caps->render_mode;
985
986 // Half-block mode: dispatch to halfblock renderers by color depth
987 if (render_mode == RENDER_MODE_HALF_BLOCK) {
988 const uint8_t *rgb_data = (const uint8_t *)image->pixels;
989 switch (color_level) {
991 return rgb_to_truecolor_halfblocks_scalar(rgb_data, image->w, image->h, 0);
992 case TERM_COLOR_256:
993 return rgb_to_256color_halfblocks_scalar(rgb_data, image->w, image->h, 0, palette);
994 case TERM_COLOR_16:
995 return rgb_to_16color_halfblocks_scalar(rgb_data, image->w, image->h, 0, palette);
996 case TERM_COLOR_NONE:
997 case TERM_COLOR_AUTO:
998 default:
999 return rgb_to_halfblocks_scalar(rgb_data, image->w, image->h, 0, palette);
1000 }
1001 }
1002
1003 // Foreground/background modes: dispatch by color depth
1004 switch (color_level) {
1005 case TERM_COLOR_TRUECOLOR: {
1006 bool use_background = (render_mode == RENDER_MODE_BACKGROUND);
1007#ifdef SIMD_SUPPORT
1008 return image_print_color_simd((image_t *)image, use_background, false, palette);
1009#else
1010 if (use_background) {
1011 return image_print_color_background(image, palette);
1012 }
1013 return image_print_color_utf8(image, palette);
1014#endif
1015 }
1016 case TERM_COLOR_256:
1017 return image_print_256color(image, palette);
1018 case TERM_COLOR_16:
1019 return image_print_16color(image, palette);
1020 case TERM_COLOR_NONE:
1021 case TERM_COLOR_AUTO:
1022 default:
1023 return image_print(image, palette);
1024 }
1025}
Platform abstraction layer umbrella header providing unified cross-platform API.
📐 Aspect Ratio Calculation Functions
char * image_print_color_background(const image_t *p, const char *palette)
Convert image to colored ASCII art with background colors.
Definition background.c:17
⚙️ Common definitions, error codes, macros, and types shared throughout the application
char * image_print_color_utf8(const image_t *p, const char *palette)
Definition foreground.c:310
unsigned int uint32_t
Definition common.h:58
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MEMSET(dest, dest_size, ch, count)
Definition common.h:469
#define SAFE_MALLOC(size, cast)
Definition common.h:264
unsigned long long uint64_t
Definition common.h:59
unsigned char uint8_t
Definition common.h:56
#define SAFE_MEMCPY(dest, dest_size, src, count)
Definition common.h:468
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_INVALID_PARAM
@ ERROR_TERMINAL
Definition error_codes.h:69
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_dev(...)
Log a DEV message (most verbose, development only)
Definition log/log.h:534
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
#define STOP_TIMER_AND_LOG_EVERY(log_level, interval_ns, threshold_ns, timer_name, msg_fmt,...)
Stop a timer and log the result with rate limiting.
Definition time.h:647
uint64_t time_get_ns(void)
Get current monotonic time in nanoseconds.
Definition util/time.c:108
#define NS_PER_SEC_INT
Definition time.h:157
int time_pretty(uint64_t nanoseconds, int decimals, char *buffer, size_t buffer_size)
Format nanoseconds as pretty duration with spaces and configurable precision.
Definition util/time.c:424
#define START_TIMER(name_fmt,...)
Start a timer with formatted name.
Definition time.h:333
#define NS_PER_MS_INT
Definition time.h:156
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Calculate elapsed time with wraparound safety.
Definition util/time.c:150
#define US_PER_SEC_INT
Definition time.h:161
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
asciichat_error_t terminal_cursor_hide(void)
Hide terminal cursor.
size_t platform_write_all(int fd, const void *buf, size_t count)
Write all bytes to a file descriptor, handling partial writes.
Definition system.c:224
asciichat_error_t terminal_flush(int fd)
Flush terminal output.
asciichat_error_t terminal_set_echo(bool enabled)
Set terminal echo mode.
asciichat_error_t terminal_cursor_show(void)
Show terminal cursor.
void aspect_ratio(const ssize_t img_w, const ssize_t img_h, const ssize_t width, const ssize_t height, const bool stretch, ssize_t *out_width, ssize_t *out_height)
Calculate aspect ratio with terminal character correction.
char * rgb_to_truecolor_halfblocks_scalar(const uint8_t *rgb, int width, int height, int stride_bytes)
Scalar truecolor halfblock renderer.
char * rgb_to_16color_halfblocks_scalar(const uint8_t *rgb, int width, int height, int stride_bytes, const char *palette)
Scalar 16-color halfblock renderer Uses 16-color ANSI codes for foreground (top) and implicit backgro...
char * ascii_convert_with_capabilities(image_t *original, const ssize_t width, const ssize_t height, const terminal_capabilities_t *caps, const bool use_aspect_ratio, const bool stretch, const char *palette_chars)
Convert image to ASCII art with terminal capability awareness.
Definition ascii.c:233
char * image_print(const image_t *p, const char *palette)
Print image as ASCII art (monochrome)
Definition foreground.c:27
char * image_print_with_capabilities(const image_t *image, const terminal_capabilities_t *caps, const char *palette)
Convert image to ASCII art with terminal capabilities.
Definition ascii.c:978
asciichat_error_t ascii_write_init(int fd, bool reset_terminal)
Initialize ASCII write subsystem.
Definition ascii.c:83
int apply_color_filter(uint8_t *pixels, uint32_t width, uint32_t height, uint32_t stride, color_filter_t filter, float time)
Apply color filter to entire image in-place.
void ascii_write_destroy(int fd, bool reset_terminal)
Destroy ASCII write subsystem.
Definition ascii.c:442
asciichat_error_t ascii_write(const char *frame)
Write ASCII frame to terminal.
Definition ascii.c:420
char * rgb_to_halfblocks_scalar(const uint8_t *rgb, int width, int height, int stride_bytes, const char *palette)
Scalar monochrome halfblock renderer (no color codes)
char * image_print_16color(const image_t *image, const char *palette)
Print image using 16-color ANSI mode.
Definition foreground.c:533
char * ascii_pad_frame_width(const char *frame, size_t pad_left)
Add leading spaces (left-padding) to each line of a frame.
Definition ascii.c:480
char * ascii_pad_frame_height(const char *frame, size_t pad_top)
Add blank lines (vertical padding) to center a frame vertically.
Definition ascii.c:925
char * rgb_to_256color_halfblocks_scalar(const uint8_t *rgb, int width, int height, int stride_bytes, const char *palette)
Scalar 256-color halfblock renderer Uses 256-color ANSI codes for foreground (top) and implicit backg...
void image_clear(image_t *p)
Clear image (set all pixels to black)
void image_resize(const image_t *s, image_t *d)
Resize image using nearest-neighbor interpolation.
#define console_clear(fd)
Clear console and move cursor to home position.
Definition ascii.h:507
char * ascii_convert(image_t *original, const ssize_t width, const ssize_t height, const bool color, const bool _aspect_ratio, const bool stretch, const char *palette_chars, const char luminance_palette[256])
Convert image to ASCII art.
Definition ascii.c:111
#define cursor_reset(fd)
Reset cursor to home position.
Definition ascii.h:518
char * image_print_256color(const image_t *image, const char *palette)
Print image using 256-color ANSI mode.
Definition foreground.c:431
char * image_print_color_simd(image_t *image, bool use_background_mode, bool use_256color, const char *ascii_chars)
Print image as ASCII with color using SIMD.
Definition sgr.c:413
char * ascii_create_grid(ascii_frame_source_t *sources, int source_count, int width, int height, size_t *out_size)
Create a grid layout from multiple ASCII frames.
Definition ascii.c:625
void image_destroy(image_t *p)
Destroy an image allocated with image_new()
image_t * image_new(size_t width, size_t height)
Create a new image with standard allocation.
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:702
🔢 Mathematical Utility Functions
✅ Safe Integer Arithmetic and Overflow Detection
bool terminal_should_use_control_sequences(int fd)
Check if terminal control sequences should be used for the given fd.
int frame
Definition splash.c:99
Frame source structure for grid layout.
Definition ascii.h:325
size_t frame_size
Frame data size in bytes (length of frame string)
Definition ascii.h:327
const char * frame_data
Frame data pointer (ASCII frame string, not owned)
Definition ascii.h:326
Image structure.
int w
Image width in pixels (must be > 0)
int h
Image height in pixels (must be > 0)
rgb_pixel_t * pixels
Pixel data array (width * height RGB pixels, row-major order)
RGB pixel structure.
uint8_t b
Blue color component (0-255)
uint8_t g
Green color component (0-255)
uint8_t r
Red color component (0-255)
Complete terminal capabilities structure.
Definition terminal.h:709
terminal_color_mode_t color_level
Detected color support level (terminal_color_mode_t)
Definition terminal.h:711
render_mode_t render_mode
Preferred rendering mode (render_mode_t)
Definition terminal.h:721
color_filter_t color_filter
Monochromatic color filter enum value (color_filter_t)
Definition terminal.h:735
🖥️ Cross-platform terminal interface for ascii-chat
@ COLOR_FILTER_NONE
No filtering (default)
Definition terminal.h:601
render_mode_t
Render mode preferences.
Definition terminal.h:662
@ RENDER_MODE_BACKGROUND
Background colors (block colors)
Definition terminal.h:666
@ RENDER_MODE_HALF_BLOCK
Unicode half-block characters (mixed foreground/background)
Definition terminal.h:668
terminal_color_mode_t
Terminal color support levels.
Definition terminal.h:578
@ TERM_COLOR_NONE
No color support (monochrome terminal)
Definition terminal.h:582
@ TERM_COLOR_16
16-color support (standard ANSI colors)
Definition terminal.h:584
@ TERM_COLOR_256
256-color support (extended ANSI palette)
Definition terminal.h:586
@ TERM_COLOR_AUTO
Auto-detect color support from terminal capabilities.
Definition terminal.h:580
@ TERM_COLOR_TRUECOLOR
24-bit truecolor support (RGB colors)
Definition terminal.h:588
⏱️ High-precision timing utilities using sokol_time.h and uthash
SIMD-optimized ASCII conversion interface.