ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
ascii.c File Reference

🖼️ Image-to-ASCII conversion with SIMD acceleration, color matching, and terminal optimization More...

Go to the source code of this file.

Functions

asciichat_error_t ascii_read_init (unsigned short int webcam_index)
 
asciichat_error_t ascii_write_init (int fd, bool reset_terminal)
 
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])
 
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)
 
asciichat_error_t ascii_write (const char *frame)
 
void ascii_write_destroy (int fd, bool reset_terminal)
 
void ascii_read_destroy (void)
 
char * ascii_pad_frame_width (const char *frame, size_t pad_left)
 
char * ascii_create_grid (ascii_frame_source_t *sources, int source_count, int width, int height, size_t *out_size)
 
char * ascii_pad_frame_height (const char *frame, size_t pad_top)
 

Detailed Description

🖼️ Image-to-ASCII conversion with SIMD acceleration, color matching, and terminal optimization

Definition in file ascii.c.

Function Documentation

◆ ascii_convert()

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] 
)

Definition at line 69 of file ascii.c.

71 {
72 if (original == NULL || !palette_chars || !luminance_palette) {
73 log_error("ascii_convert: invalid parameters");
74 return NULL;
75 }
76
77 // Check for empty strings
78 if (palette_chars[0] == '\0' || luminance_palette[0] == '\0') {
79 log_error("ascii_convert: empty palette strings");
80 return NULL;
81 }
82
83 // Start with the target dimensions requested by the user (or detected from
84 // the terminal). These can be modified by aspect_ratio() if stretching is
85 // disabled and one of the dimensions was left to be calculated
86 // automatically.
87 ssize_t resized_width = width;
88 ssize_t resized_height = height;
89
90 // If stretch is enabled, use full dimensions, otherwise calculate aspect ratio
91 if (_aspect_ratio) {
92 // The server now provides images at width*2 x height pixels
93 // The aspect_ratio function will handle terminal character aspect ratio
94 aspect_ratio(original->w, original->h, resized_width, resized_height, stretch, &resized_width, &resized_height);
95 }
96
97 // Calculate padding for centering
98 size_t pad_width = 0;
99 size_t pad_height = 0;
100
101 if (_aspect_ratio) {
102 // Only calculate padding when not stretching
103 ssize_t pad_width_ss = width > resized_width ? (width - resized_width) / 2 : 0;
104 pad_width = (size_t)pad_width_ss;
105
106 ssize_t pad_height_ss = height > resized_height ? (height - resized_height) / 2 : 0;
107 pad_height = (size_t)pad_height_ss;
108 }
109
110 // Resize the captured frame to the aspect-correct dimensions.
111 if (resized_width <= 0 || resized_height <= 0) {
112 log_error("Invalid dimensions for resize: width=%zd, height=%zd", resized_width, resized_height);
113 return NULL;
114 }
115
116 // Validate dimensions fit in image_t's int fields before casting
117 if (resized_width > INT_MAX || resized_height > INT_MAX) {
118 log_error("Dimensions exceed INT_MAX: width=%zd, height=%zd", resized_width, resized_height);
119 return NULL;
120 }
121
122 // Always resize to target dimensions
123 image_t *resized = image_new((size_t)resized_width, (size_t)resized_height);
124 if (!resized) {
125 log_error("Failed to allocate resized image");
126 return NULL;
127 }
128
129 image_clear(resized);
130 image_resize(original, resized);
131
132 char *ascii;
133 if (color) {
134 // Check for half-block mode first (requires NEON)
135 if (GET_OPTION(render_mode) == RENDER_MODE_HALF_BLOCK) {
136#if SIMD_SUPPORT_NEON
137 // Use NEON half-block renderer
138 const uint8_t *rgb_data = (const uint8_t *)resized->pixels;
139 ascii = rgb_to_truecolor_halfblocks_neon(rgb_data, resized->w, resized->h, 0);
140#else
141 log_error("Half-block mode requires NEON support (ARM architecture)");
142 image_destroy(resized);
143 return NULL;
144#endif
145 } else {
146#ifdef SIMD_SUPPORT
147 // Standard color modes (foreground/background)
148 bool use_background = (GET_OPTION(render_mode) == RENDER_MODE_BACKGROUND);
149 ascii = image_print_color_simd(resized, use_background, false, palette_chars);
150#else
151 ascii = image_print_color(resized, palette_chars);
152#endif
153 }
154 } else {
155 // Use grayscale/monochrome conversion with client's palette
156#ifdef SIMD_SUPPORT
157 ascii = image_print_simd(resized, luminance_palette);
158#else
159 ascii = image_print(resized, palette_chars);
160#endif
161 }
162
163 if (!ascii) {
164 log_error("Failed to convert image to ASCII");
165 image_destroy(resized);
166 return NULL;
167 }
168
169 size_t ascii_len = strlen(ascii);
170 if (ascii_len == 0) {
171 log_error("ASCII conversion returned empty string (resized dimensions: %dx%d)", resized->w, resized->h);
172 SAFE_FREE(ascii);
173 image_destroy(resized);
174 return NULL;
175 }
176
177 char *ascii_width_padded = ascii_pad_frame_width(ascii, pad_width);
178 SAFE_FREE(ascii);
179
180 char *ascii_padded = ascii_pad_frame_height(ascii_width_padded, pad_height);
181 SAFE_FREE(ascii_width_padded);
182
183 // Only destroy resized if we allocated it (not when using original directly)
184 image_destroy(resized);
185
186 return ascii_padded;
187}
char * ascii_pad_frame_width(const char *frame, size_t pad_left)
Definition ascii.c:398
char * ascii_pad_frame_height(const char *frame, size_t pad_top)
Definition ascii.c:842
char * image_print_simd(image_t *image, const char *ascii_chars)
Definition ascii_simd.c:260
char * image_print_color_simd(image_t *image, bool use_background_mode, bool use_256color, const char *ascii_chars)
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)
char * image_print(const image_t *p, const char *palette)
char * image_print_color(const image_t *p, const char *palette)
void image_resize(const image_t *s, image_t *d)
void image_clear(image_t *p)
void image_destroy(image_t *p)
Definition video/image.c:85
image_t * image_new(size_t width, size_t height)
Definition video/image.c:36

References ascii_pad_frame_height(), ascii_pad_frame_width(), aspect_ratio(), image_clear(), image_destroy(), image_new(), image_print(), image_print_color(), image_print_color_simd(), image_print_simd(), and image_resize().

Referenced by benchmark_simd_color_conversion_with_source(), and convert_frame_to_ascii().

◆ ascii_convert_with_capabilities()

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 
)

Definition at line 190 of file ascii.c.

192 {
193
194 if (original == NULL || caps == NULL) {
195 log_error("Invalid parameters for ascii_convert_with_capabilities");
196 return NULL;
197 }
198
199 // Start with the target dimensions requested by the user
200 ssize_t resized_width = width;
201 ssize_t resized_height = height;
202
203 // Height doubling for half-block mode is now handled by the server
204
205 // If stretch is enabled, use full dimensions, otherwise calculate aspect ratio
206 if (use_aspect_ratio && caps->render_mode != RENDER_MODE_HALF_BLOCK) {
207 // Normal modes: apply aspect ratio correction
208 aspect_ratio(original->w, original->h, resized_width, resized_height, stretch, &resized_width, &resized_height);
209 }
210 // Half-block mode: skip aspect ratio to preserve full doubled dimensions for 2x resolution
211
212 // Calculate padding for centering (only if client wants padding)
213 size_t pad_width = 0;
214 size_t pad_height = 0;
215
216 if (use_aspect_ratio && caps->wants_padding) {
217 ssize_t pad_width_ss = width > resized_width ? (width - resized_width) / 2 : 0;
218 pad_width = (size_t)pad_width_ss;
219
220 ssize_t pad_height_ss = height > resized_height ? (height - resized_height) / 2 : 0;
221 pad_height = (size_t)pad_height_ss;
222
223 log_debug_every(10 * US_PER_SEC_INT,
224 "ascii_convert_with_capabilities: width=%zd, height=%zd, resized_width=%zd, resized_height=%zd, "
225 "pad_width=%zu, pad_height=%zu, stretch=%d, wants_padding=%d",
226 width, height, resized_width, resized_height, pad_width, pad_height, stretch, caps->wants_padding);
227 } else if (!caps->wants_padding) {
228 log_debug_every(10 * US_PER_SEC_INT,
229 "ascii_convert_with_capabilities: padding disabled (wants_padding=false), width=%zd, height=%zd",
230 width, height);
231 }
232
233 // Resize the captured frame to the aspect-correct dimensions
234 if (resized_width <= 0 || resized_height <= 0) {
235 log_error("Invalid dimensions for resize: width=%zd, height=%zd", resized_width, resized_height);
236 return NULL;
237 }
238
239 // Validate dimensions fit in image_t's int fields before casting
240 if (resized_width > INT_MAX || resized_height > INT_MAX) {
241 log_error("Dimensions exceed INT_MAX: width=%zd, height=%zd", resized_width, resized_height);
242 return NULL;
243 }
244
245 // PROFILING: Time image allocation and resize
246 START_TIMER("image_alloc");
247 uint64_t prof_alloc_start_ns = time_get_ns();
248
249 image_t *resized = image_new((size_t)resized_width, (size_t)resized_height);
250 if (!resized) {
251 log_error("Failed to allocate resized image");
252 return NULL;
253 }
254
255 image_clear(resized);
256
257 uint64_t prof_alloc_end_ns = time_get_ns();
258 STOP_TIMER_AND_LOG_EVERY(dev, 3 * NS_PER_SEC_INT, 5 * NS_PER_MS_INT, "image_alloc",
259 "IMAGE_ALLOC: Alloc+clear complete (%.2f ms)");
260
261 START_TIMER("image_resize");
262 uint64_t prof_resize_start_ns = prof_alloc_end_ns;
263
264 image_resize(original, resized);
265
266 uint64_t prof_resize_end_ns = time_get_ns();
267 STOP_TIMER_AND_LOG_EVERY(dev, 3 * NS_PER_SEC_INT, 5 * NS_PER_MS_INT, "image_resize",
268 "IMAGE_RESIZE: Resize complete (%.2f ms)");
269
270 // PROFILING: Time ASCII print
271 uint64_t prof_print_start_ns = prof_resize_end_ns;
272
273 // DEBUG: Log dimensions going to renderer
274 log_debug_every(10 * US_PER_SEC_INT,
275 "ascii_convert: original=%dx%d, requested=%zdx%zd, resized=%dx%d, pad=%zux%zu (mode=%d)", original->w,
276 original->h, width, height, resized->w, resized->h, pad_width, pad_height, caps->render_mode);
277
278 // Use the capability-aware image printing function with client's palette
279 START_TIMER("image_print_with_capabilities");
280 char *ascii = image_print_with_capabilities(resized, caps, palette_chars);
281
282 uint64_t prof_print_end_ns = time_get_ns();
283 STOP_TIMER_AND_LOG_EVERY(dev, 3 * NS_PER_SEC_INT, 5 * NS_PER_MS_INT, "image_print_with_capabilities",
284 "IMAGE_PRINT: Print complete (%.2f ms)");
285
286 uint64_t alloc_time_us = time_ns_to_us(time_elapsed_ns(prof_alloc_start_ns, prof_alloc_end_ns));
287 uint64_t resize_time_us = time_ns_to_us(time_elapsed_ns(prof_resize_start_ns, prof_resize_end_ns));
288 uint64_t print_time_us = time_ns_to_us(time_elapsed_ns(prof_print_start_ns, prof_print_end_ns));
289
290 // PROFILING: Time padding
291 START_TIMER("ascii_padding");
292 uint64_t prof_pad_start_ns = time_get_ns();
293
294 if (!ascii) {
295 log_error("Failed to convert image to ASCII using terminal capabilities");
296 image_destroy(resized);
297 return NULL;
298 }
299
300 size_t ascii_len = strlen(ascii);
301 if (ascii_len == 0) {
302 log_error("Capability-aware ASCII conversion returned empty string (resized dimensions: %dx%d)", resized->w,
303 resized->h);
304 SAFE_FREE(ascii);
305 image_destroy(resized);
306 return NULL;
307 }
308
309 char *ascii_width_padded = ascii_pad_frame_width(ascii, pad_width);
310 SAFE_FREE(ascii);
311
312 char *ascii_padded = ascii_pad_frame_height(ascii_width_padded, pad_height);
313 SAFE_FREE(ascii_width_padded);
314
315 uint64_t prof_pad_end_ns = time_get_ns();
316 STOP_TIMER_AND_LOG_EVERY(dev, 3 * NS_PER_SEC_INT, 2 * NS_PER_MS_INT, "ascii_padding",
317 "ASCII_PADDING: Padding complete (%.2f ms)");
318
319 uint64_t pad_time_us = time_ns_to_us(time_elapsed_ns(prof_pad_start_ns, prof_pad_end_ns));
320 log_dev("ASCII_BREAKDOWN: alloc=%.2f ms, resize=%.2f ms, print=%.2f ms, pad=%.2f ms (total=%.2f ms)",
321 (double)alloc_time_us / 1000.0, (double)resize_time_us / 1000.0, (double)print_time_us / 1000.0,
322 (double)pad_time_us / 1000.0,
323 (double)(alloc_time_us + resize_time_us + print_time_us + pad_time_us) / 1000.0);
324
325 image_destroy(resized);
326
327 return ascii_padded;
328}
uint64_t time_get_ns(void)
Definition util/time.c:48
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Definition util/time.c:90
char * image_print_with_capabilities(const image_t *image, const terminal_capabilities_t *caps, const char *palette)

References ascii_pad_frame_height(), ascii_pad_frame_width(), aspect_ratio(), image_clear(), image_destroy(), image_new(), image_print_with_capabilities(), image_resize(), time_elapsed_ns(), and time_get_ns().

Referenced by mirror_convert_frame(), and session_display_convert_to_ascii().

◆ ascii_create_grid()

char * ascii_create_grid ( ascii_frame_source_t *  sources,
int  source_count,
int  width,
int  height,
size_t *  out_size 
)

Creates a grid layout from multiple ASCII frame sources with | and _ separators.

Parameters: sources Array of ASCII frame sources to combine source_count Number of sources in the array width Target width of the output grid height Target height of the output grid out_size Output parameter for the size of the returned buffer

Returns: A newly allocated, null-terminated string containing the grid layout, or NULL on error. Caller must free the returned buffer.

Definition at line 542 of file ascii.c.

542 {
543 if (!sources || source_count <= 0 || width <= 0 || height <= 0 || !out_size) {
544 return NULL;
545 }
546
547 // If no sources, return empty frame
548
549 // If only one source, center it properly to maintain aspect ratio and look good
550 if (source_count == 1) {
551 // Create a frame of the target size filled with spaces
552 // Check for integer overflow before multiplication
553 size_t w = (size_t)width;
554 size_t h = (size_t)height;
555 size_t w_times_h;
556 if (checked_size_mul(w, h, &w_times_h) != ASCIICHAT_OK) {
557 SET_ERRNO(ERROR_INVALID_PARAM, "ascii_create_grid: dimensions would overflow: %dx%d", width, height);
558 return NULL;
559 }
560
561 size_t w_times_h_plus_h;
562 if (checked_size_add(w_times_h, h, &w_times_h_plus_h) != ASCIICHAT_OK) {
563 SET_ERRNO(ERROR_INVALID_PARAM, "ascii_create_grid: buffer size would overflow: %dx%d", width, height);
564 return NULL;
565 }
566
567 size_t target_size;
568 if (checked_size_add(w_times_h_plus_h, 1, &target_size) != ASCIICHAT_OK) {
569 SET_ERRNO(ERROR_INVALID_PARAM, "ascii_create_grid: buffer size would overflow: %dx%d", width, height);
570 return NULL;
571 }
572 char *result;
573 result = SAFE_MALLOC(target_size, char *);
574 SAFE_MEMSET(result, target_size, ' ', target_size - 1);
575 result[target_size - 1] = '\0';
576
577 // Add newlines at the end of each row
578 for (int row = 0; row < height; row++) {
579 result[row * (width + 1) + width] = '\n';
580 }
581
582 // Copy the source frame into the result, line by line, centering it
583 // Handle NULL frame_data gracefully
584 const char *src_data = sources[0].frame_data;
585 int src_pos = 0;
586 int src_size = (int)sources[0].frame_size;
587
588 // If source data is NULL or empty, just return the empty frame
589 if (!src_data || src_size <= 0) {
590 *out_size = target_size - 1; // Don't count null terminator
591 return result;
592 }
593
594 // Count lines in source to calculate vertical padding
595 int src_lines = 0;
596 for (int i = 0; i < src_size; i++) {
597 if (src_data[i] == '\n')
598 src_lines++;
599 }
600
601 int v_padding = (height - src_lines) / 2;
602 if (v_padding < 0)
603 v_padding = 0;
604
605 int dst_row = v_padding;
606 src_pos = 0;
607
608 while (src_pos < src_size && dst_row < height) {
609 // Find end of current line in source
610 int line_start = src_pos;
611 int line_len = 0;
612 while (src_pos < src_size && src_data[src_pos] != '\n') {
613 line_len++;
614 src_pos++;
615 }
616
617 // Calculate visual width of the line (excluding ANSI escape sequences)
618 int visual_line_width = ansi_visual_width(&src_data[line_start], line_len);
619
620 // Calculate horizontal padding to center the line based on visual width
621 int h_padding = (width - visual_line_width) / 2;
622 if (h_padding < 0)
623 h_padding = 0;
624
625 // Copy line to result with padding
626 // Use size_t for position calculation to prevent integer underflow
627 size_t row_offset = (size_t)dst_row * (size_t)(width + 1);
628 size_t dst_pos = row_offset + (size_t)h_padding;
629 int max_visual_width = width - h_padding;
630 // Truncate to visual width while preserving complete ANSI sequences
631 int copy_len = ansi_truncate_to_visual_width(&src_data[line_start], line_len, max_visual_width);
632
633 if (copy_len > 0 && dst_pos + (size_t)copy_len < target_size) {
634 SAFE_MEMCPY(&result[dst_pos], target_size - dst_pos, &src_data[line_start], (size_t)copy_len);
635 }
636
637 // Skip newline in source
638 if (src_pos < src_size && src_data[src_pos] == '\n') {
639 src_pos++;
640 }
641
642 dst_row++;
643 }
644
645 *out_size = target_size - 1; // Don't count null terminator
646 return result;
647 }
648
649 // Multiple sources: create grid layout
650 // Calculate grid dimensions that maximize the use of terminal space
651 // Character aspect ratio: terminal chars are typically ~2x taller than wide
652 float char_aspect = 2.0f;
653
654 int grid_cols, grid_rows;
655 float best_score = -1.0f;
656 int best_cols = 1;
657 int best_rows = source_count;
658
659 // Try all possible grid configurations
660 for (int test_cols = 1; test_cols <= source_count; test_cols++) {
661 int test_rows = (int)ceil((double)source_count / test_cols);
662
663 // Skip configurations with too many empty cells
664 int empty_cells = (test_cols * test_rows) - source_count;
665 if (empty_cells > source_count / 2)
666 continue; // Don't waste more than 50% space
667
668 // Calculate the size each cell would have
669 int cell_width = (width - (test_cols - 1)) / test_cols; // -1 per separator
670 int cell_height = (height - (test_rows - 1)) / test_rows; // -1 per separator
671
672 // Skip if cells would be too small
673 if (cell_width < 10 || cell_height < 3)
674 continue;
675
676 // Calculate the aspect ratio of each cell (accounting for char aspect)
677 float cell_aspect = ((float)cell_width / (float)cell_height) / char_aspect;
678
679 // Score based on how close to square (1:1) each video cell would be
680 // This naturally adapts to any terminal size
681 float aspect_score = 1.0f - fabsf(logf(cell_aspect)); // log makes it symmetric around 1
682 if (aspect_score < 0)
683 aspect_score = 0;
684
685 // Bonus for better space utilization
686 float utilization = (float)source_count / (float)(test_cols * test_rows);
687
688 // For 2 clients specifically, heavily weight the aspect score
689 // This makes 2 clients naturally go horizontal on wide terminals and vertical on tall ones
690 float total_score;
691 if (source_count == 2) {
692 // For 2 clients, we want the layout that gives the most square-ish cells
693 total_score = aspect_score * 0.9f + utilization * 0.1f;
694 } else {
695 // For 3+ clients, balance aspect ratio with space utilization
696 total_score = aspect_score * 0.7f + utilization * 0.3f;
697 }
698
699 // Small bonus for simpler grids (prefer 2x2 over 3x1, etc.)
700 if (test_cols == test_rows) {
701 total_score += 0.05f; // Slight preference for square grids
702 }
703
704 if (total_score > best_score) {
705 best_score = total_score;
706 best_cols = test_cols;
707 best_rows = test_rows;
708 }
709 }
710
711 grid_cols = best_cols;
712 grid_rows = best_rows;
713
714 // Calculate dimensions for each cell (leave 1 char for separators)
715 int cell_width = (width - (grid_cols - 1)) / grid_cols;
716 int cell_height = (height - (grid_rows - 1)) / grid_rows;
717
718 if (cell_width < 10 || cell_height < 3) {
719 // Too small for grid layout, just use first source
720 char *result;
721 result = SAFE_MALLOC(sources[0].frame_size + 1, char *);
722 if (sources[0].frame_data && sources[0].frame_size > 0) {
723 SAFE_MEMCPY(result, sources[0].frame_size + 1, sources[0].frame_data, sources[0].frame_size);
724 result[sources[0].frame_size] = '\0';
725 *out_size = sources[0].frame_size;
726 } else {
727 // Handle NULL or empty frame data
728 result[0] = '\0';
729 *out_size = 0;
730 }
731 return result;
732 }
733
734 // Allocate mixed frame buffer
735 // Check for integer overflow before multiplication
736 size_t w_sz = (size_t)width;
737 size_t h_sz = (size_t)height;
738 if (w_sz > SIZE_MAX / h_sz) {
739 SET_ERRNO(ERROR_INVALID_PARAM, "ascii_create_grid: dimensions would overflow: %dx%d", width, height);
740 return NULL;
741 }
742 size_t mixed_size = w_sz * h_sz + h_sz + 1; // +1 for null terminator, +height for newlines
743 char *mixed_frame;
744 mixed_frame = SAFE_MALLOC(mixed_size, char *);
745
746 // Initialize mixed frame with spaces
747 SAFE_MEMSET(mixed_frame, mixed_size, ' ', mixed_size - 1);
748 mixed_frame[mixed_size - 1] = '\0';
749
750 // Add newlines at the end of each row
751 for (int row = 0; row < height; row++) {
752 mixed_frame[row * (width + 1) + width] = '\n';
753 }
754
755 // Place each video source in the grid
756 for (int src = 0; src < source_count; src++) {
757 int grid_row = src / grid_cols;
758 int grid_col = src % grid_cols;
759
760 // Calculate position in mixed frame
761 int start_row = grid_row * (cell_height + 1); // +1 for separator
762 int start_col = grid_col * (cell_width + 1); // +1 for separator
763
764 // Parse source frame line by line and place in grid
765 const char *src_data = sources[src].frame_data;
766 int src_row = 0;
767 int src_pos = 0;
768
769 while (src_pos < (int)sources[src].frame_size && src_row < cell_height && start_row + src_row < height) {
770 // Find end of current line in source
771 int line_start = src_pos;
772 while (src_pos < (int)sources[src].frame_size && src_data[src_pos] != '\n') {
773 src_pos++;
774 }
775 int line_len = src_pos - line_start;
776
777 // Copy line to mixed frame (truncate if too long)
778 // Truncate to visual cell width while preserving complete ANSI sequences
779 int copy_len = ansi_truncate_to_visual_width(src_data + line_start, line_len, cell_width);
780 // Check that visual width of truncated line fits
781 int truncated_visual_width = ansi_visual_width(src_data + line_start, copy_len);
782 if (copy_len > 0 && start_col + truncated_visual_width <= width) {
783 int mixed_pos = (start_row + src_row) * (width + 1) + start_col;
784 SAFE_MEMCPY(mixed_frame + mixed_pos, mixed_size - (size_t)mixed_pos, src_data + line_start, (size_t)copy_len);
785 }
786
787 // Move to next line
788 if (src_pos < (int)sources[src].frame_size && src_data[src_pos] == '\n') {
789 src_pos++;
790 }
791 src_row++;
792 }
793
794 // Draw separators with bounds checking to prevent buffer overflow
795 if (grid_col < grid_cols - 1 && start_col + cell_width < width) {
796 // Vertical separator
797 for (int row = start_row; row < start_row + cell_height && row < height; row++) {
798 size_t idx = (size_t)row * (size_t)(width + 1) + (size_t)(start_col + cell_width);
799 if (idx < mixed_size - 1) { // -1 to preserve null terminator
800 mixed_frame[idx] = '|';
801 }
802 }
803 }
804
805 if (grid_row < grid_rows - 1 && start_row + cell_height < height) {
806 // Horizontal separator
807 for (int col = start_col; col < start_col + cell_width && col < width; col++) {
808 size_t idx = (size_t)(start_row + cell_height) * (size_t)(width + 1) + (size_t)col;
809 if (idx < mixed_size - 1) { // -1 to preserve null terminator
810 mixed_frame[idx] = '_';
811 }
812 }
813 // Corner character where separators meet
814 if (grid_col < grid_cols - 1 && start_col + cell_width < width) {
815 size_t idx = (size_t)(start_row + cell_height) * (size_t)(width + 1) + (size_t)(start_col + cell_width);
816 if (idx < mixed_size - 1) { // -1 to preserve null terminator
817 mixed_frame[idx] = '+';
818 }
819 }
820 }
821 }
822
823 *out_size = strlen(mixed_frame);
824 return mixed_frame;
825}

◆ ascii_pad_frame_height()

char * ascii_pad_frame_height ( const char *  frame,
size_t  pad_top 
)

Adds vertical padding (blank lines) to center a frame vertically.

Parameters: frame The input ASCII frame to pad vertically. pad_top Number of blank lines to add at the top.

Returns: A newly allocated, null-terminated string with vertical padding, or NULL if frame is NULL.

NOTE: Uses plain newlines instead of ANSI escape sequences to support both TTY and piped/redirected output. TTY flicker prevention is handled by the display layer (e.g., display.c) when appropriate.

Definition at line 842 of file ascii.c.

842 {
843 if (!frame) {
844 return NULL;
845 }
846
847 if (pad_top == 0) {
848 // Nothing to do; return a copy because the caller knows to free() the value.
849 size_t orig_len = strlen(frame);
850 char *copy;
851 copy = SAFE_MALLOC(orig_len + 1, char *);
852 SAFE_MEMCPY(copy, orig_len + 1, frame, orig_len + 1);
853 return copy;
854 }
855
856 // Calculate buffer size needed
857 // Each padding row needs: 1 newline character per padding row
858 size_t frame_len = strlen(frame);
859 size_t top_padding_len = pad_top; // 1 newline per padding row
860 size_t total_len = top_padding_len + frame_len;
861
862 char *buffer;
863 buffer = SAFE_MALLOC(total_len + 1, char *);
864
865 char *position = buffer;
866
867 // Add top padding with plain newlines
868 // Use plain newlines instead of ANSI escape sequences so the output works
869 // when redirected to pipes or files, not just TTY
870 for (size_t i = 0; i < pad_top; i++) {
871 *position++ = '\n';
872 }
873
874 // Copy the original frame
875 size_t remaining = frame_len + 1;
876 SAFE_MEMCPY(position, remaining, frame, frame_len);
877 position += frame_len;
878 *position = '\0';
879
880 return buffer;
881}

Referenced by ascii_convert(), and ascii_convert_with_capabilities().

◆ ascii_pad_frame_width()

char * ascii_pad_frame_width ( const char *  frame,
size_t  pad_left 
)

Definition at line 398 of file ascii.c.

398 {
399 if (!frame) {
400 return NULL;
401 }
402
403 if (pad_left == 0) {
404 // Nothing to do; return a copy so the caller can free it safely without
405 // worrying about the original allocation strategy.
406 size_t orig_len = strlen(frame);
407 char *copy;
408 copy = SAFE_MALLOC(orig_len + 1, char *);
409 SAFE_MEMCPY(copy, orig_len + 1, frame, orig_len + 1);
410 return copy;
411 }
412
413 // Count how many visual rows we have (lines terminated by '\n') to determine
414 // the final buffer size.
415 size_t line_count = 1; // There is always at least the first line
416 const char *char_in_frame = frame;
417 while (*char_in_frame) {
418 if (*char_in_frame == '\n') {
419 line_count++;
420 }
421 char_in_frame++;
422 }
423
424 // Total length of the source plus padding.
425 const size_t frame_len = strlen(frame);
426 const size_t left_padding_len = line_count * pad_left;
427 const size_t total_len = frame_len + left_padding_len;
428
429 char *buffer;
430 buffer = SAFE_MALLOC(total_len + 1, char *);
431
432 // Build the padded frame.
433 bool at_line_start = true;
434 const char *src = frame;
435 char *position = buffer;
436
437 while (*src) {
438 if (at_line_start) {
439 // Insert the requested amount of spaces in front of every visual row.
440 size_t remaining = (size_t)((ptrdiff_t)(buffer + total_len + 1) - (ptrdiff_t)position);
441 SAFE_MEMSET(position, remaining, ' ', (size_t)pad_left);
442 position += pad_left;
443 at_line_start = false;
444 }
445
446 *position++ = *src;
447
448 if (*src == '\n') {
449 at_line_start = true;
450 }
451
452 src++;
453 }
454
455 *position = '\0';
456 return buffer;
457}

Referenced by ascii_convert(), and ascii_convert_with_capabilities().

◆ ascii_read_destroy()

void ascii_read_destroy ( void  )

Definition at line 378 of file ascii.c.

378 {
380 log_debug("ASCII reader destroyed");
381}
void webcam_destroy(void)

References webcam_destroy().

◆ ascii_read_init()

asciichat_error_t ascii_read_init ( unsigned short int  webcam_index)

Definition at line 35 of file ascii.c.

35 {
36 log_info("Initializing ASCII reader with webcam index %u", webcam_index);
37 webcam_init(webcam_index);
38 return ASCIICHAT_OK;
39}
asciichat_error_t webcam_init(unsigned short int webcam_index)

References webcam_init().

◆ ascii_write()

asciichat_error_t ascii_write ( const char *  frame)

Definition at line 333 of file ascii.c.

333 {
334 if (frame == NULL) {
335 log_warn("Attempted to write NULL frame");
336 return ERROR_INVALID_PARAM;
337 }
338
339 // Only reset cursor if output is connected to a TTY (not piped/redirected)
340 if (terminal_should_use_control_sequences(STDOUT_FILENO)) {
341 cursor_reset(STDOUT_FILENO);
342 }
343
344 size_t frame_len = strlen(frame);
345 // Write all frame data with automatic retry on transient errors
346 (void)platform_write_all(STDOUT_FILENO, frame, frame_len);
347
348 // Flush C stdio buffer and terminal to ensure piped output is written immediately
349 (void)fflush(stdout);
350 (void)terminal_flush(STDOUT_FILENO);
351
352 return ASCIICHAT_OK;
353}
size_t platform_write_all(int fd, const void *buf, size_t count)
Write all data to file descriptor with automatic retry on transient errors.
Definition abstraction.c:39
bool terminal_should_use_control_sequences(int fd)
Check if terminal control sequences should be used for the given fd.
Definition abstraction.c:90
asciichat_error_t terminal_flush(int fd)

References platform_write_all(), terminal_flush(), and terminal_should_use_control_sequences().

◆ ascii_write_destroy()

void ascii_write_destroy ( int  fd,
bool  reset_terminal 
)

Definition at line 355 of file ascii.c.

355 {
356#if PLATFORM_WINDOWS
357 (void)fd; // Unused on Windows - terminal operations use stdout directly
358#endif
359 // console_clear(fd);
360 // cursor_reset(fd);
361 // Only restore terminal state if:
362 // 1. reset_terminal is true (caller wants terminal restore)
363 // 2. terminal_should_use_control_sequences() confirms it's safe (TTY, not snapshot, not testing)
364 if (reset_terminal && terminal_should_use_control_sequences(fd)) {
365 // Show cursor using platform abstraction
366 if (terminal_hide_cursor(fd, false) != 0) {
367 log_warn("Failed to show cursor");
368 }
369
370 // Re-enable echo using platform abstraction
371 if (terminal_set_echo(true) != 0) {
372 log_warn("Failed to re-enable echo");
373 }
374 }
375 log_debug("ASCII writer destroyed");
376}

References terminal_should_use_control_sequences().

Referenced by session_display_destroy().

◆ ascii_write_init()

asciichat_error_t ascii_write_init ( int  fd,
bool  reset_terminal 
)

Definition at line 41 of file ascii.c.

41 {
42 // Validate file descriptor
43 if (fd < 0) {
44 log_error("Invalid file descriptor %d", fd);
45 return ERROR_INVALID_PARAM;
46 }
47
48 // Only apply terminal control sequences if:
49 // 1. reset_terminal is true (caller wants terminal reset)
50 // 2. terminal_should_use_control_sequences() confirms it's safe (TTY, not snapshot, not testing)
51 if (reset_terminal && terminal_should_use_control_sequences(fd)) {
52 console_clear(fd);
53 cursor_reset(fd);
54
55 // Disable echo using platform abstraction
56 if (terminal_set_echo(false) != 0) {
57 log_error("Failed to disable echo for fd %d", fd);
58 return ERROR_TERMINAL;
59 }
60 // Hide cursor using platform abstraction
61 if (terminal_hide_cursor(fd, true) != 0) {
62 log_warn("Failed to hide cursor");
63 }
64 }
65 log_dev("ASCII writer initialized");
66 return ASCIICHAT_OK;
67}

References terminal_should_use_control_sequences().

Referenced by session_display_create().