ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
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
18#include "platform/terminal.h"
19
20#include "ascii.h"
21#include "common.h"
22#include "image.h"
23#include "util/aspect_ratio.h"
24#include "util/overflow.h"
25#include "video/webcam/webcam.h"
26#include "options/options.h"
27#include "simd/ascii_simd.h"
28
29/* ============================================================================
30 * ASCII Art Video Processing
31 * ============================================================================
32 */
33
34asciichat_error_t ascii_read_init(unsigned short int webcam_index) {
35 log_info("Initializing ASCII reader with webcam index %u", webcam_index);
36 webcam_init(webcam_index);
37 return ASCIICHAT_OK;
38}
39
40asciichat_error_t ascii_write_init(int fd, bool reset_terminal) {
41 // Validate file descriptor
42 if (fd < 0) {
43 log_error("Invalid file descriptor %d", fd);
45 }
46
47 // Skip terminal control sequences in snapshot mode or when testing - just print raw ASCII
48 const char *testing_env = SAFE_GETENV("TESTING");
49 if (!GET_OPTION(snapshot_mode) && reset_terminal && testing_env == NULL) {
50 console_clear(fd);
51 cursor_reset(fd);
52
53 // Disable echo using platform abstraction
54 if (terminal_set_echo(false) != 0) {
55 log_error("Failed to disable echo for fd %d", fd);
56 return ERROR_TERMINAL;
57 }
58 // Hide cursor using platform abstraction
59 if (terminal_hide_cursor(fd, true) != 0) {
60 log_warn("Failed to hide cursor");
61 }
62 }
63 log_debug("ASCII writer initialized");
64 return ASCIICHAT_OK;
65}
66
67char *ascii_convert(image_t *original, const ssize_t width, const ssize_t height, const bool color,
68 const bool _aspect_ratio, const bool stretch, const char *palette_chars,
69 const char luminance_palette[256]) {
70 if (original == NULL || !palette_chars || !luminance_palette) {
71 log_error("ascii_convert: invalid parameters");
72 return NULL;
73 }
74
75 // Check for empty strings
76 if (palette_chars[0] == '\0' || luminance_palette[0] == '\0') {
77 log_error("ascii_convert: empty palette strings");
78 return NULL;
79 }
80
81 // Start with the target dimensions requested by the user (or detected from
82 // the terminal). These can be modified by aspect_ratio() if stretching is
83 // disabled and one of the dimensions was left to be calculated
84 // automatically.
85 ssize_t resized_width = width;
86 ssize_t resized_height = height;
87
88 // If stretch is enabled, use full dimensions, otherwise calculate aspect ratio
89 if (_aspect_ratio) {
90 // The server now provides images at width*2 x height pixels
91 // The aspect_ratio function will handle terminal character aspect ratio
92 aspect_ratio(original->w, original->h, resized_width, resized_height, stretch, &resized_width, &resized_height);
93 }
94
95 // Calculate padding for centering
96 size_t pad_width = 0;
97 size_t pad_height = 0;
98
99 if (_aspect_ratio) {
100 // Only calculate padding when not stretching
101 ssize_t pad_width_ss = width > resized_width ? (width - resized_width) / 2 : 0;
102 pad_width = (size_t)pad_width_ss;
103
104 ssize_t pad_height_ss = height > resized_height ? (height - resized_height) / 2 : 0;
105 pad_height = (size_t)pad_height_ss;
106 }
107
108 // Resize the captured frame to the aspect-correct dimensions.
109 if (resized_width <= 0 || resized_height <= 0) {
110 log_error("Invalid dimensions for resize: width=%zd, height=%zd", resized_width, resized_height);
111 return NULL;
112 }
113
114 // Validate dimensions fit in image_t's int fields before casting
115 if (resized_width > INT_MAX || resized_height > INT_MAX) {
116 log_error("Dimensions exceed INT_MAX: width=%zd, height=%zd", resized_width, resized_height);
117 return NULL;
118 }
119
120 // Always resize to target dimensions
121 image_t *resized = image_new((size_t)resized_width, (size_t)resized_height);
122 if (!resized) {
123 log_error("Failed to allocate resized image");
124 return NULL;
125 }
126
127 image_clear(resized);
128 image_resize(original, resized);
129
130 char *ascii;
131 if (color) {
132 // Check for half-block mode first (requires NEON)
133 if (GET_OPTION(render_mode) == RENDER_MODE_HALF_BLOCK) {
134#if SIMD_SUPPORT_NEON
135 // Use NEON half-block renderer
136 const uint8_t *rgb_data = (const uint8_t *)resized->pixels;
137 ascii = rgb_to_truecolor_halfblocks_neon(rgb_data, resized->w, resized->h, 0);
138#else
139 log_error("Half-block mode requires NEON support (ARM architecture)");
140 image_destroy(resized);
141 return NULL;
142#endif
143 } else {
144#ifdef SIMD_SUPPORT
145 // Standard color modes (foreground/background)
146 bool use_background = (GET_OPTION(render_mode) == RENDER_MODE_BACKGROUND);
147 ascii = image_print_color_simd(resized, use_background, false, palette_chars);
148#else
149 ascii = image_print_color(resized, palette_chars);
150#endif
151 }
152 } else {
153 // Use grayscale/monochrome conversion with client's palette
154#ifdef SIMD_SUPPORT
155 ascii = image_print_simd(resized, luminance_palette);
156#else
157 ascii = image_print(resized, palette_chars);
158#endif
159 }
160
161 if (!ascii) {
162 log_error("Failed to convert image to ASCII");
163 image_destroy(resized);
164 return NULL;
165 }
166
167 size_t ascii_len = strlen(ascii);
168 if (ascii_len == 0) {
169 log_error("ASCII conversion returned empty string (resized dimensions: %dx%d)", resized->w, resized->h);
170 SAFE_FREE(ascii);
171 image_destroy(resized);
172 return NULL;
173 }
174
175 char *ascii_width_padded = ascii_pad_frame_width(ascii, pad_width);
176 SAFE_FREE(ascii);
177
178 char *ascii_padded = ascii_pad_frame_height(ascii_width_padded, pad_height);
179 SAFE_FREE(ascii_width_padded);
180
181 // Only destroy resized if we allocated it (not when using original directly)
182 image_destroy(resized);
183
184 return ascii_padded;
185}
186
187// Capability-aware ASCII conversion using terminal capabilities
188char *ascii_convert_with_capabilities(image_t *original, const ssize_t width, const ssize_t height,
189 const terminal_capabilities_t *caps, const bool use_aspect_ratio,
190 const bool stretch, const char *palette_chars,
191 const char luminance_palette[256]) {
192
193 if (original == NULL || caps == NULL) {
194 log_error("Invalid parameters for ascii_convert_with_capabilities");
195 return NULL;
196 }
197
198 // Start with the target dimensions requested by the user
199 ssize_t resized_width = width;
200 ssize_t resized_height = height;
201
202 // Height doubling for half-block mode is now handled by the server
203
204 // If stretch is enabled, use full dimensions, otherwise calculate aspect ratio
205 if (use_aspect_ratio && caps->render_mode != RENDER_MODE_HALF_BLOCK) {
206 // Normal modes: apply aspect ratio correction
207 aspect_ratio(original->w, original->h, resized_width, resized_height, stretch, &resized_width, &resized_height);
208 }
209 // Half-block mode: skip aspect ratio to preserve full doubled dimensions for 2x resolution
210
211 // Calculate padding for centering
212 size_t pad_width = 0;
213 size_t pad_height = 0;
214
215 if (use_aspect_ratio) {
216 ssize_t pad_width_ss = width > resized_width ? (width - resized_width) / 2 : 0;
217 pad_width = (size_t)pad_width_ss;
218
219 ssize_t pad_height_ss = height > resized_height ? (height - resized_height) / 2 : 0;
220 pad_height = (size_t)pad_height_ss;
221 }
222
223 // Resize the captured frame to the aspect-correct dimensions
224 if (resized_width <= 0 || resized_height <= 0) {
225 log_error("Invalid dimensions for resize: width=%zd, height=%zd", resized_width, resized_height);
226 return NULL;
227 }
228
229 // Validate dimensions fit in image_t's int fields before casting
230 if (resized_width > INT_MAX || resized_height > INT_MAX) {
231 log_error("Dimensions exceed INT_MAX: width=%zd, height=%zd", resized_width, resized_height);
232 return NULL;
233 }
234
235 // PROFILING: Time image allocation and resize
236 struct timespec prof_alloc_start, prof_alloc_end, prof_resize_start, prof_resize_end;
237 (void)clock_gettime(CLOCK_MONOTONIC, &prof_alloc_start);
238
239 image_t *resized = image_new((size_t)resized_width, (size_t)resized_height);
240 if (!resized) {
241 log_error("Failed to allocate resized image");
242 return NULL;
243 }
244
245 image_clear(resized);
246
247 (void)clock_gettime(CLOCK_MONOTONIC, &prof_alloc_end);
248 (void)clock_gettime(CLOCK_MONOTONIC, &prof_resize_start);
249
250 image_resize(original, resized);
251
252 (void)clock_gettime(CLOCK_MONOTONIC, &prof_resize_end);
253
254 // PROFILING: Time ASCII print
255 struct timespec prof_print_start, prof_print_end;
256 (void)clock_gettime(CLOCK_MONOTONIC, &prof_print_start);
257
258 // Use the capability-aware image printing function with client's palette
259 char *ascii = image_print_with_capabilities(resized, caps, palette_chars, luminance_palette);
260
261 (void)clock_gettime(CLOCK_MONOTONIC, &prof_print_end);
262
263 uint64_t alloc_time_us = ((uint64_t)prof_alloc_end.tv_sec * 1000000 + (uint64_t)prof_alloc_end.tv_nsec / 1000) -
264 ((uint64_t)prof_alloc_start.tv_sec * 1000000 + (uint64_t)prof_alloc_start.tv_nsec / 1000);
265 uint64_t resize_time_us = ((uint64_t)prof_resize_end.tv_sec * 1000000 + (uint64_t)prof_resize_end.tv_nsec / 1000) -
266 ((uint64_t)prof_resize_start.tv_sec * 1000000 + (uint64_t)prof_resize_start.tv_nsec / 1000);
267 uint64_t print_time_us = ((uint64_t)prof_print_end.tv_sec * 1000000 + (uint64_t)prof_print_end.tv_nsec / 1000) -
268 ((uint64_t)prof_print_start.tv_sec * 1000000 + (uint64_t)prof_print_start.tv_nsec / 1000);
269
270 // PROFILING: Time padding
271 struct timespec prof_pad_start, prof_pad_end;
272 (void)clock_gettime(CLOCK_MONOTONIC, &prof_pad_start);
273
274 if (!ascii) {
275 log_error("Failed to convert image to ASCII using terminal capabilities");
276 image_destroy(resized);
277 return NULL;
278 }
279
280 size_t ascii_len = strlen(ascii);
281 if (ascii_len == 0) {
282 log_error("Capability-aware ASCII conversion returned empty string (resized dimensions: %dx%d)", resized->w,
283 resized->h);
284 SAFE_FREE(ascii);
285 image_destroy(resized);
286 return NULL;
287 }
288
289 char *ascii_width_padded = ascii_pad_frame_width(ascii, pad_width);
290 SAFE_FREE(ascii);
291
292 char *ascii_padded = ascii_pad_frame_height(ascii_width_padded, pad_height);
293 SAFE_FREE(ascii_width_padded);
294
295 (void)clock_gettime(CLOCK_MONOTONIC, &prof_pad_end);
296
297 uint64_t pad_time_us = ((uint64_t)prof_pad_end.tv_sec * 1000000 + (uint64_t)prof_pad_end.tv_nsec / 1000) -
298 ((uint64_t)prof_pad_start.tv_sec * 1000000 + (uint64_t)prof_pad_start.tv_nsec / 1000);
299 (void)alloc_time_us;
300 (void)resize_time_us;
301 (void)print_time_us;
302 (void)pad_time_us;
303
304 image_destroy(resized);
305
306 return ascii_padded;
307}
308
309// NOTE: ascii_convert_with_custom_palette removed - use ascii_convert_with_capabilities() with enhanced
310// terminal_capabilities_t
311
312asciichat_error_t ascii_write(const char *frame) {
313 if (frame == NULL) {
314 log_warn("Attempted to write NULL frame");
315 return ERROR_INVALID_PARAM;
316 }
317
318 // Skip cursor reset in snapshot mode or when testing - just print raw ASCII
319 const char *testing_env = SAFE_GETENV("TESTING");
320 if (!GET_OPTION(snapshot_mode) && testing_env == NULL) {
321 cursor_reset(STDOUT_FILENO);
322 }
323
324 size_t frame_len = strlen(frame);
325 size_t written = fwrite(frame, 1, frame_len, stdout);
326 if (written != frame_len) {
327 log_error("Failed to write ASCII frame");
328 return ERROR_TERMINAL;
329 }
330
331 return ASCIICHAT_OK;
332}
333
334void ascii_write_destroy(int fd, bool reset_terminal) {
335#if PLATFORM_WINDOWS
336 (void)fd; // Unused on Windows - terminal operations use stdout directly
337#endif
338 // console_clear(fd);
339 // cursor_reset(fd);
340 // Skip cursor show in snapshot mode - leave terminal as-is
341 if (!GET_OPTION(snapshot_mode) && reset_terminal) {
342 // Show cursor using platform abstraction
343 if (terminal_hide_cursor(fd, false) != 0) {
344 log_warn("Failed to show cursor");
345 }
346
347 // Re-enable echo using platform abstraction
348 if (terminal_set_echo(true) != 0) {
349 log_warn("Failed to re-enable echo");
350 }
351 }
352 log_debug("ASCII writer destroyed");
353}
354
357 log_debug("ASCII reader destroyed");
358}
359
360/*
361 * Pads each line of an ASCII frame with a given number of leading space
362 * characters. The function allocates a new buffer large enough to hold the
363 * padded frame and returns a pointer to it. The caller is responsible for
364 * freeing the returned buffer.
365 *
366 * Parameters:
367 * frame The original, null-terminated ASCII frame. It is expected to
368 * contain `\n` at the end of every visual row.
369 * pad_left How many space characters to add in front of every visual row.
370 *
371 * Returns:
372 * A newly allocated, null-terminated string that contains the padded frame
373 * on success, or NULL if either `frame`.
374 */
375char *ascii_pad_frame_width(const char *frame, size_t pad_left) {
376 if (!frame) {
377 return NULL;
378 }
379
380 if (pad_left == 0) {
381 // Nothing to do; return a copy so the caller can free it safely without
382 // worrying about the original allocation strategy.
383 size_t orig_len = strlen(frame);
384 char *copy;
385 copy = SAFE_MALLOC(orig_len + 1, char *);
386 SAFE_MEMCPY(copy, orig_len + 1, frame, orig_len + 1);
387 return copy;
388 }
389
390 // Count how many visual rows we have (lines terminated by '\n') to determine
391 // the final buffer size.
392 size_t line_count = 1; // There is always at least the first line
393 const char *char_in_frame = frame;
394 while (*char_in_frame) {
395 if (*char_in_frame == '\n') {
396 line_count++;
397 }
398 char_in_frame++;
399 }
400
401 // Total length of the source plus padding.
402 const size_t frame_len = strlen(frame);
403 const size_t left_padding_len = line_count * pad_left;
404 const size_t total_len = frame_len + left_padding_len;
405
406 char *buffer;
407 buffer = SAFE_MALLOC(total_len + 1, char *);
408
409 // Build the padded frame.
410 bool at_line_start = true;
411 const char *src = frame;
412 char *position = buffer;
413
414 while (*src) {
415 if (at_line_start) {
416 // Insert the requested amount of spaces in front of every visual row.
417 size_t remaining = (size_t)((ptrdiff_t)(buffer + total_len + 1) - (ptrdiff_t)position);
418 SAFE_MEMSET(position, remaining, ' ', (size_t)pad_left);
419 position += pad_left;
420 at_line_start = false;
421 }
422
423 *position++ = *src;
424
425 if (*src == '\n') {
426 at_line_start = true;
427 }
428
429 src++;
430 }
431
432 *position = '\0';
433 return buffer;
434}
435
450char *ascii_create_grid(ascii_frame_source_t *sources, int source_count, int width, int height, size_t *out_size) {
451 if (!sources || source_count <= 0 || width <= 0 || height <= 0 || !out_size) {
452 return NULL;
453 }
454
455 // If no sources, return empty frame
456
457 // If only one source, center it properly to maintain aspect ratio and look good
458 if (source_count == 1) {
459 // Create a frame of the target size filled with spaces
460 // Check for integer overflow before multiplication
461 size_t w = (size_t)width;
462 size_t h = (size_t)height;
463 size_t w_times_h;
464 if (checked_size_mul(w, h, &w_times_h) != ASCIICHAT_OK) {
465 SET_ERRNO(ERROR_INVALID_PARAM, "ascii_create_grid: dimensions would overflow: %dx%d", width, height);
466 return NULL;
467 }
468
469 size_t w_times_h_plus_h;
470 if (checked_size_add(w_times_h, h, &w_times_h_plus_h) != ASCIICHAT_OK) {
471 SET_ERRNO(ERROR_INVALID_PARAM, "ascii_create_grid: buffer size would overflow: %dx%d", width, height);
472 return NULL;
473 }
474
475 size_t target_size;
476 if (checked_size_add(w_times_h_plus_h, 1, &target_size) != ASCIICHAT_OK) {
477 SET_ERRNO(ERROR_INVALID_PARAM, "ascii_create_grid: buffer size would overflow: %dx%d", width, height);
478 return NULL;
479 }
480 char *result;
481 result = SAFE_MALLOC(target_size, char *);
482 SAFE_MEMSET(result, target_size, ' ', target_size - 1);
483 result[target_size - 1] = '\0';
484
485 // Add newlines at the end of each row
486 for (int row = 0; row < height; row++) {
487 result[row * (width + 1) + width] = '\n';
488 }
489
490 // Copy the source frame into the result, line by line, centering it
491 // Handle NULL frame_data gracefully
492 const char *src_data = sources[0].frame_data;
493 int src_pos = 0;
494 int src_size = (int)sources[0].frame_size;
495
496 // If source data is NULL or empty, just return the empty frame
497 if (!src_data || src_size <= 0) {
498 *out_size = target_size - 1; // Don't count null terminator
499 return result;
500 }
501
502 // Count lines in source to calculate vertical padding
503 int src_lines = 0;
504 for (int i = 0; i < src_size; i++) {
505 if (src_data[i] == '\n')
506 src_lines++;
507 }
508
509 int v_padding = (height - src_lines) / 2;
510 if (v_padding < 0)
511 v_padding = 0;
512
513 int dst_row = v_padding;
514 src_pos = 0;
515
516 while (src_pos < src_size && dst_row < height) {
517 // Find end of current line in source
518 int line_start = src_pos;
519 int line_len = 0;
520 while (src_pos < src_size && src_data[src_pos] != '\n') {
521 line_len++;
522 src_pos++;
523 }
524
525 // Calculate horizontal padding to center the line
526 int h_padding = (width - line_len) / 2;
527 if (h_padding < 0)
528 h_padding = 0;
529
530 // Copy line to result with padding
531 // Use size_t for position calculation to prevent integer underflow
532 size_t row_offset = (size_t)dst_row * (size_t)(width + 1);
533 size_t dst_pos = row_offset + (size_t)h_padding;
534 int copy_len = (line_len > width - h_padding) ? width - h_padding : line_len;
535
536 if (copy_len > 0 && dst_pos + (size_t)copy_len < target_size) {
537 SAFE_MEMCPY(&result[dst_pos], target_size - dst_pos, &src_data[line_start], (size_t)copy_len);
538 }
539
540 // Skip newline in source
541 if (src_pos < src_size && src_data[src_pos] == '\n') {
542 src_pos++;
543 }
544
545 dst_row++;
546 }
547
548 *out_size = target_size - 1; // Don't count null terminator
549 return result;
550 }
551
552 // Multiple sources: create grid layout
553 // Calculate grid dimensions that maximize the use of terminal space
554 // Character aspect ratio: terminal chars are typically ~2x taller than wide
555 float char_aspect = 2.0f;
556
557 int grid_cols, grid_rows;
558 float best_score = -1.0f;
559 int best_cols = 1;
560 int best_rows = source_count;
561
562 // Try all possible grid configurations
563 for (int test_cols = 1; test_cols <= source_count; test_cols++) {
564 int test_rows = (int)ceil((double)source_count / test_cols);
565
566 // Skip configurations with too many empty cells
567 int empty_cells = (test_cols * test_rows) - source_count;
568 if (empty_cells > source_count / 2)
569 continue; // Don't waste more than 50% space
570
571 // Calculate the size each cell would have
572 int cell_width = (width - (test_cols - 1)) / test_cols; // -1 per separator
573 int cell_height = (height - (test_rows - 1)) / test_rows; // -1 per separator
574
575 // Skip if cells would be too small
576 if (cell_width < 10 || cell_height < 3)
577 continue;
578
579 // Calculate the aspect ratio of each cell (accounting for char aspect)
580 float cell_aspect = ((float)cell_width / (float)cell_height) / char_aspect;
581
582 // Score based on how close to square (1:1) each video cell would be
583 // This naturally adapts to any terminal size
584 float aspect_score = 1.0f - fabsf(logf(cell_aspect)); // log makes it symmetric around 1
585 if (aspect_score < 0)
586 aspect_score = 0;
587
588 // Bonus for better space utilization
589 float utilization = (float)source_count / (float)(test_cols * test_rows);
590
591 // For 2 clients specifically, heavily weight the aspect score
592 // This makes 2 clients naturally go horizontal on wide terminals and vertical on tall ones
593 float total_score;
594 if (source_count == 2) {
595 // For 2 clients, we want the layout that gives the most square-ish cells
596 total_score = aspect_score * 0.9f + utilization * 0.1f;
597 } else {
598 // For 3+ clients, balance aspect ratio with space utilization
599 total_score = aspect_score * 0.7f + utilization * 0.3f;
600 }
601
602 // Small bonus for simpler grids (prefer 2x2 over 3x1, etc.)
603 if (test_cols == test_rows) {
604 total_score += 0.05f; // Slight preference for square grids
605 }
606
607 if (total_score > best_score) {
608 best_score = total_score;
609 best_cols = test_cols;
610 best_rows = test_rows;
611 }
612 }
613
614 grid_cols = best_cols;
615 grid_rows = best_rows;
616
617 // Calculate dimensions for each cell (leave 1 char for separators)
618 int cell_width = (width - (grid_cols - 1)) / grid_cols;
619 int cell_height = (height - (grid_rows - 1)) / grid_rows;
620
621 if (cell_width < 10 || cell_height < 3) {
622 // Too small for grid layout, just use first source
623 char *result;
624 result = SAFE_MALLOC(sources[0].frame_size + 1, char *);
625 if (sources[0].frame_data && sources[0].frame_size > 0) {
626 SAFE_MEMCPY(result, sources[0].frame_size + 1, sources[0].frame_data, sources[0].frame_size);
627 result[sources[0].frame_size] = '\0';
628 *out_size = sources[0].frame_size;
629 } else {
630 // Handle NULL or empty frame data
631 result[0] = '\0';
632 *out_size = 0;
633 }
634 return result;
635 }
636
637 // Allocate mixed frame buffer
638 // Check for integer overflow before multiplication
639 size_t w_sz = (size_t)width;
640 size_t h_sz = (size_t)height;
641 if (w_sz > SIZE_MAX / h_sz) {
642 SET_ERRNO(ERROR_INVALID_PARAM, "ascii_create_grid: dimensions would overflow: %dx%d", width, height);
643 return NULL;
644 }
645 size_t mixed_size = w_sz * h_sz + h_sz + 1; // +1 for null terminator, +height for newlines
646 char *mixed_frame;
647 mixed_frame = SAFE_MALLOC(mixed_size, char *);
648
649 // Initialize mixed frame with spaces
650 SAFE_MEMSET(mixed_frame, mixed_size, ' ', mixed_size - 1);
651 mixed_frame[mixed_size - 1] = '\0';
652
653 // Add newlines at the end of each row
654 for (int row = 0; row < height; row++) {
655 mixed_frame[row * (width + 1) + width] = '\n';
656 }
657
658 // Place each video source in the grid
659 for (int src = 0; src < source_count; src++) {
660 int grid_row = src / grid_cols;
661 int grid_col = src % grid_cols;
662
663 // Calculate position in mixed frame
664 int start_row = grid_row * (cell_height + 1); // +1 for separator
665 int start_col = grid_col * (cell_width + 1); // +1 for separator
666
667 // Parse source frame line by line and place in grid
668 const char *src_data = sources[src].frame_data;
669 int src_row = 0;
670 int src_pos = 0;
671
672 while (src_pos < (int)sources[src].frame_size && src_row < cell_height && start_row + src_row < height) {
673 // Find end of current line in source
674 int line_start = src_pos;
675 while (src_pos < (int)sources[src].frame_size && src_data[src_pos] != '\n') {
676 src_pos++;
677 }
678 int line_len = src_pos - line_start;
679
680 // Copy line to mixed frame (truncate if too long)
681 int copy_len = (line_len < cell_width) ? line_len : cell_width;
682 if (copy_len > 0 && start_col + copy_len <= width) {
683 int mixed_pos = (start_row + src_row) * (width + 1) + start_col;
684 SAFE_MEMCPY(mixed_frame + mixed_pos, mixed_size - (size_t)mixed_pos, src_data + line_start, (size_t)copy_len);
685 }
686
687 // Move to next line
688 if (src_pos < (int)sources[src].frame_size && src_data[src_pos] == '\n') {
689 src_pos++;
690 }
691 src_row++;
692 }
693
694 // Draw separators with bounds checking to prevent buffer overflow
695 if (grid_col < grid_cols - 1 && start_col + cell_width < width) {
696 // Vertical separator
697 for (int row = start_row; row < start_row + cell_height && row < height; row++) {
698 size_t idx = (size_t)row * (size_t)(width + 1) + (size_t)(start_col + cell_width);
699 if (idx < mixed_size - 1) { // -1 to preserve null terminator
700 mixed_frame[idx] = '|';
701 }
702 }
703 }
704
705 if (grid_row < grid_rows - 1 && start_row + cell_height < height) {
706 // Horizontal separator
707 for (int col = start_col; col < start_col + cell_width && col < width; col++) {
708 size_t idx = (size_t)(start_row + cell_height) * (size_t)(width + 1) + (size_t)col;
709 if (idx < mixed_size - 1) { // -1 to preserve null terminator
710 mixed_frame[idx] = '_';
711 }
712 }
713 // Corner character where separators meet
714 if (grid_col < grid_cols - 1 && start_col + cell_width < width) {
715 size_t idx = (size_t)(start_row + cell_height) * (size_t)(width + 1) + (size_t)(start_col + cell_width);
716 if (idx < mixed_size - 1) { // -1 to preserve null terminator
717 mixed_frame[idx] = '+';
718 }
719 }
720 }
721 }
722
723 *out_size = strlen(mixed_frame);
724 return mixed_frame;
725}
726
738char *ascii_pad_frame_height(const char *frame, size_t pad_top) {
739 if (!frame) {
740 return NULL;
741 }
742
743 if (pad_top == 0) {
744 // Nothing to do; return a copy because the caller knows to free() the value.
745 size_t orig_len = strlen(frame);
746 char *copy;
747 copy = SAFE_MALLOC(orig_len + 1, char *);
748 SAFE_MEMCPY(copy, orig_len + 1, frame, orig_len + 1);
749 return copy;
750 }
751
752 // Calculate buffer size needed
753 size_t frame_len = strlen(frame);
754 size_t top_padding_len = pad_top; // Just newlines, no spaces
755 size_t total_len = top_padding_len + frame_len;
756
757 char *buffer;
758 buffer = SAFE_MALLOC(total_len + 1, char *);
759
760 char *position = buffer;
761
762 // Add top padding (blank lines - just newlines)
763 for (size_t i = 0; i < pad_top; i++) {
764 *position++ = '\n';
765 }
766
767 // Copy the original frame
768 size_t remaining = total_len + 1 - pad_top;
769 SAFE_MEMCPY(position, remaining, frame, frame_len);
770 position += frame_len;
771 *position = '\0';
772
773 return buffer;
774}
🔌 Cross-platform abstraction layer umbrella header for ascii-chat
🖼️ ASCII Art Conversion and Output Interface
SIMD-optimized ASCII conversion interface.
📐 Aspect Ratio Calculation Functions
#define SAFE_FREE(ptr)
Definition common.h:320
#define SAFE_MEMSET(dest, dest_size, ch, count)
Definition common.h:389
#define SAFE_MALLOC(size, cast)
Definition common.h:208
#define SAFE_GETENV(name)
Definition common.h:378
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:388
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:46
@ ASCIICHAT_OK
Definition error_codes.h:48
@ ERROR_INVALID_PARAM
@ ERROR_TERMINAL
Definition error_codes.h:66
#define log_warn(...)
Log a WARN message.
#define log_error(...)
Log an ERROR message.
#define log_info(...)
Log an INFO message.
#define log_debug(...)
Log a DEBUG message.
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
Definition options.h:644
asciichat_error_t terminal_set_echo(bool enable)
Set terminal echo mode.
asciichat_error_t terminal_hide_cursor(int fd, bool hide)
Hide or show cursor.
@ RENDER_MODE_BACKGROUND
Background colors (block colors)
Definition terminal.h:471
@ RENDER_MODE_HALF_BLOCK
Unicode half-block characters (mixed foreground/background)
Definition terminal.h:473
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 * image_print(const image_t *p, const char *palette)
Print image as ASCII art (monochrome)
asciichat_error_t ascii_write_init(int fd, bool reset_terminal)
Initialize ASCII write subsystem.
Definition ascii.c:40
char * image_print_color(const image_t *p, const char *palette)
Print image as ASCII art with color.
void ascii_read_destroy(void)
Destroy ASCII read subsystem.
Definition ascii.c:355
void ascii_write_destroy(int fd, bool reset_terminal)
Destroy ASCII write subsystem.
Definition ascii.c:334
asciichat_error_t ascii_write(const char *frame)
Write ASCII frame to terminal.
Definition ascii.c:312
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:375
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:738
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.
char * image_print_simd(image_t *image, const char *ascii_chars)
Print image as ASCII using SIMD (monochrome)
Definition ascii_simd.c:252
#define console_clear(fd)
Clear console and move cursor to home position.
Definition ascii.h:524
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:67
#define cursor_reset(fd)
Reset cursor to home position.
Definition ascii.h:535
asciichat_error_t ascii_read_init(unsigned short int webcam_index)
Initialize ASCII read subsystem (e.g., webcam)
Definition ascii.c:34
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.
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:450
void image_destroy(image_t *p)
Destroy an image allocated with image_new()
Definition video/image.c:85
image_t * image_new(size_t width, size_t height)
Create a new image with standard allocation.
Definition video/image.c:36
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, const char luminance_palette[256])
Convert image to ASCII art with terminal capability awareness.
Definition ascii.c:188
asciichat_error_t webcam_init(unsigned short int webcam_index)
Initialize global webcam interface.
Definition webcam.c:18
void webcam_cleanup(void)
Clean up global webcam interface.
Definition webcam.c:204
🔢 Mathematical Utility Functions
⚙️ Command-line options parsing and configuration management for ascii-chat
✅ Safe Integer Arithmetic and Overflow Detection
Frame source structure for grid layout.
Definition ascii.h:342
size_t frame_size
Frame data size in bytes (length of frame string)
Definition ascii.h:344
const char * frame_data
Frame data pointer (ASCII frame string, not owned)
Definition ascii.h:343
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)
Complete terminal capabilities structure.
Definition terminal.h:485
render_mode_t render_mode
Preferred rendering mode (render_mode_t)
Definition terminal.h:497
🖥️ Cross-platform terminal interface for ascii-chat
⏱️ High-precision timing utilities using sokol_time.h and uthash
char * image_print_with_capabilities(const image_t *image, const terminal_capabilities_t *caps, const char *palette, const char luminance_palette[256] __attribute__((unused)))
Image Data Structures and Operations.