69char *
ascii_convert(image_t *original,
const ssize_t width,
const ssize_t height,
const bool color,
70 const bool _aspect_ratio,
const bool stretch,
const char *palette_chars,
71 const char luminance_palette[256]) {
72 if (original == NULL || !palette_chars || !luminance_palette) {
73 log_error(
"ascii_convert: invalid parameters");
78 if (palette_chars[0] ==
'\0' || luminance_palette[0] ==
'\0') {
79 log_error(
"ascii_convert: empty palette strings");
87 ssize_t resized_width = width;
88 ssize_t resized_height = height;
94 aspect_ratio(original->w, original->h, resized_width, resized_height, stretch, &resized_width, &resized_height);
99 size_t pad_height = 0;
103 ssize_t pad_width_ss = width > resized_width ? (width - resized_width) / 2 : 0;
104 pad_width = (size_t)pad_width_ss;
106 ssize_t pad_height_ss = height > resized_height ? (height - resized_height) / 2 : 0;
107 pad_height = (size_t)pad_height_ss;
111 if (resized_width <= 0 || resized_height <= 0) {
112 log_error(
"Invalid dimensions for resize: width=%zd, height=%zd", resized_width, resized_height);
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);
123 image_t *resized =
image_new((
size_t)resized_width, (
size_t)resized_height);
125 log_error(
"Failed to allocate resized image");
135 if (GET_OPTION(render_mode) == RENDER_MODE_HALF_BLOCK) {
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);
141 log_error(
"Half-block mode requires NEON support (ARM architecture)");
148 bool use_background = (GET_OPTION(render_mode) == RENDER_MODE_BACKGROUND);
164 log_error(
"Failed to convert image to ASCII");
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);
181 SAFE_FREE(ascii_width_padded);
191 const terminal_capabilities_t *caps,
const bool use_aspect_ratio,
192 const bool stretch,
const char *palette_chars) {
194 if (original == NULL || caps == NULL) {
195 log_error(
"Invalid parameters for ascii_convert_with_capabilities");
200 ssize_t resized_width = width;
201 ssize_t resized_height = height;
206 if (use_aspect_ratio && caps->render_mode != RENDER_MODE_HALF_BLOCK) {
208 aspect_ratio(original->w, original->h, resized_width, resized_height, stretch, &resized_width, &resized_height);
213 size_t pad_width = 0;
214 size_t pad_height = 0;
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;
220 ssize_t pad_height_ss = height > resized_height ? (height - resized_height) / 2 : 0;
221 pad_height = (size_t)pad_height_ss;
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",
234 if (resized_width <= 0 || resized_height <= 0) {
235 log_error(
"Invalid dimensions for resize: width=%zd, height=%zd", resized_width, resized_height);
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);
246 START_TIMER(
"image_alloc");
249 image_t *resized =
image_new((
size_t)resized_width, (
size_t)resized_height);
251 log_error(
"Failed to allocate resized image");
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)");
261 START_TIMER(
"image_resize");
262 uint64_t prof_resize_start_ns = prof_alloc_end_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)");
271 uint64_t prof_print_start_ns = prof_resize_end_ns;
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);
279 START_TIMER(
"image_print_with_capabilities");
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)");
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));
291 START_TIMER(
"ascii_padding");
295 log_error(
"Failed to convert image to ASCII using terminal capabilities");
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,
313 SAFE_FREE(ascii_width_padded);
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)");
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);
542char *
ascii_create_grid(ascii_frame_source_t *sources,
int source_count,
int width,
int height,
size_t *out_size) {
543 if (!sources || source_count <= 0 || width <= 0 || height <= 0 || !out_size) {
550 if (source_count == 1) {
553 size_t w = (size_t)width;
554 size_t h = (size_t)height;
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);
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);
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);
573 result = SAFE_MALLOC(target_size,
char *);
574 SAFE_MEMSET(result, target_size,
' ', target_size - 1);
575 result[target_size - 1] =
'\0';
578 for (
int row = 0; row < height; row++) {
579 result[row * (width + 1) + width] =
'\n';
584 const char *src_data = sources[0].frame_data;
586 int src_size = (int)sources[0].frame_size;
589 if (!src_data || src_size <= 0) {
590 *out_size = target_size - 1;
596 for (
int i = 0; i < src_size; i++) {
597 if (src_data[i] ==
'\n')
601 int v_padding = (height - src_lines) / 2;
605 int dst_row = v_padding;
608 while (src_pos < src_size && dst_row < height) {
610 int line_start = src_pos;
612 while (src_pos < src_size && src_data[src_pos] !=
'\n') {
618 int visual_line_width = ansi_visual_width(&src_data[line_start], line_len);
621 int h_padding = (width - visual_line_width) / 2;
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;
631 int copy_len = ansi_truncate_to_visual_width(&src_data[line_start], line_len, max_visual_width);
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);
638 if (src_pos < src_size && src_data[src_pos] ==
'\n') {
645 *out_size = target_size - 1;
652 float char_aspect = 2.0f;
654 int grid_cols, grid_rows;
655 float best_score = -1.0f;
657 int best_rows = source_count;
660 for (
int test_cols = 1; test_cols <= source_count; test_cols++) {
661 int test_rows = (int)ceil((
double)source_count / test_cols);
664 int empty_cells = (test_cols * test_rows) - source_count;
665 if (empty_cells > source_count / 2)
669 int cell_width = (width - (test_cols - 1)) / test_cols;
670 int cell_height = (height - (test_rows - 1)) / test_rows;
673 if (cell_width < 10 || cell_height < 3)
677 float cell_aspect = ((float)cell_width / (
float)cell_height) / char_aspect;
681 float aspect_score = 1.0f - fabsf(logf(cell_aspect));
682 if (aspect_score < 0)
686 float utilization = (float)source_count / (
float)(test_cols * test_rows);
691 if (source_count == 2) {
693 total_score = aspect_score * 0.9f + utilization * 0.1f;
696 total_score = aspect_score * 0.7f + utilization * 0.3f;
700 if (test_cols == test_rows) {
701 total_score += 0.05f;
704 if (total_score > best_score) {
705 best_score = total_score;
706 best_cols = test_cols;
707 best_rows = test_rows;
711 grid_cols = best_cols;
712 grid_rows = best_rows;
715 int cell_width = (width - (grid_cols - 1)) / grid_cols;
716 int cell_height = (height - (grid_rows - 1)) / grid_rows;
718 if (cell_width < 10 || cell_height < 3) {
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;
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);
742 size_t mixed_size = w_sz * h_sz + h_sz + 1;
744 mixed_frame = SAFE_MALLOC(mixed_size,
char *);
747 SAFE_MEMSET(mixed_frame, mixed_size,
' ', mixed_size - 1);
748 mixed_frame[mixed_size - 1] =
'\0';
751 for (
int row = 0; row < height; row++) {
752 mixed_frame[row * (width + 1) + width] =
'\n';
756 for (
int src = 0; src < source_count; src++) {
757 int grid_row = src / grid_cols;
758 int grid_col = src % grid_cols;
761 int start_row = grid_row * (cell_height + 1);
762 int start_col = grid_col * (cell_width + 1);
765 const char *src_data = sources[src].frame_data;
769 while (src_pos < (
int)sources[src].frame_size && src_row < cell_height && start_row + src_row < height) {
771 int line_start = src_pos;
772 while (src_pos < (
int)sources[src].frame_size && src_data[src_pos] !=
'\n') {
775 int line_len = src_pos - line_start;
779 int copy_len = ansi_truncate_to_visual_width(src_data + line_start, line_len, cell_width);
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);
788 if (src_pos < (
int)sources[src].frame_size && src_data[src_pos] ==
'\n') {
795 if (grid_col < grid_cols - 1 && start_col + cell_width < width) {
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) {
800 mixed_frame[idx] =
'|';
805 if (grid_row < grid_rows - 1 && start_row + cell_height < height) {
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) {
810 mixed_frame[idx] =
'_';
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) {
817 mixed_frame[idx] =
'+';
823 *out_size = strlen(mixed_frame);