| /* | |
| * Copyright © 2026 Behdad Esfahbod | |
| * | |
| * This is part of HarfBuzz, a text shaping library. | |
| * | |
| * Permission is hereby granted, without written agreement and without | |
| * license or royalty fees, to use, copy, modify, and distribute this | |
| * software and its documentation for any purpose, provided that the | |
| * above copyright notice and the following two paragraphs appear in | |
| * all copies of this software. | |
| * | |
| * IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR | |
| * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES | |
| * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN | |
| * IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH | |
| * DAMAGE. | |
| * | |
| * THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, | |
| * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND | |
| * FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS | |
| * ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO | |
| * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. | |
| * | |
| * Author(s): Behdad Esfahbod | |
| */ | |
| #ifndef HELPER_IMAGE_OUTPUT_HH | |
| #define HELPER_IMAGE_OUTPUT_HH | |
| #include "hb.hh" | |
| #ifdef HAVE_CAIRO | |
| #include <cairo.h> | |
| #endif | |
| #ifdef HAVE_CAIRO | |
| using helper_image_status_t = cairo_status_t; | |
| using helper_image_write_func_t = cairo_write_func_t; | |
| #define HELPER_IMAGE_STATUS_SUCCESS CAIRO_STATUS_SUCCESS | |
| #else | |
| using helper_image_status_t = int; | |
| using helper_image_write_func_t = helper_image_status_t (*) (void *closure, | |
| const unsigned char *data, | |
| unsigned int size); | |
| #define HELPER_IMAGE_STATUS_SUCCESS 0 | |
| #endif | |
| #include "ansi-print.hh" | |
| enum class helper_image_protocol_t { | |
| NONE = 0, | |
| ITERM2, | |
| KITTY, | |
| }; | |
| static inline helper_image_status_t | |
| helper_image_stdio_write_func (void *closure, | |
| const unsigned char *data, | |
| unsigned int size) | |
| { | |
| FILE *fp = (FILE *) closure; | |
| while (size) | |
| { | |
| size_t ret = fwrite (data, 1, size, fp); | |
| size -= ret; | |
| data += ret; | |
| if (size && ferror (fp)) | |
| fail (false, "Failed to write output: %s", strerror (errno)); | |
| } | |
| return HELPER_IMAGE_STATUS_SUCCESS; | |
| } | |
| static inline const char * | |
| helper_image_get_implicit_output_format (FILE *fp, | |
| const char *default_format, | |
| helper_image_protocol_t *protocol) | |
| { | |
| *protocol = helper_image_protocol_t::NONE; | |
| #if HAVE_ISATTY | |
| if (isatty (fileno (fp))) | |
| { | |
| #ifdef HAVE_PNG | |
| const char *name; | |
| /* https://gitlab.com/gnachman/iterm2/-/issues/7154 */ | |
| if ((name = getenv ("LC_TERMINAL")) != nullptr && | |
| 0 == g_ascii_strcasecmp (name, "iTerm2")) | |
| { | |
| *protocol = helper_image_protocol_t::ITERM2; | |
| return "png"; | |
| } | |
| if ((name = getenv ("TERM_PROGRAM")) != nullptr && | |
| 0 == g_ascii_strcasecmp (name, "WezTerm")) | |
| { | |
| *protocol = helper_image_protocol_t::ITERM2; | |
| return "png"; | |
| } | |
| if ((name = getenv ("TERM")) != nullptr && | |
| 0 == g_ascii_strcasecmp (name, "xterm-kitty")) | |
| { | |
| *protocol = helper_image_protocol_t::KITTY; | |
| return "png"; | |
| } | |
| #endif | |
| return "ansi"; | |
| } | |
| #endif | |
| return default_format; | |
| } | |
| static inline void | |
| helper_image_write_png_data (const unsigned char *data, | |
| unsigned int length, | |
| helper_image_write_func_t write_func, | |
| void *closure, | |
| helper_image_protocol_t protocol) | |
| { | |
| if (protocol == helper_image_protocol_t::NONE) | |
| { | |
| write_func (closure, data, length); | |
| return; | |
| } | |
| gchar *base64 = g_base64_encode (data, length); | |
| size_t base64_len = strlen (base64); | |
| GString *string = g_string_new (nullptr); | |
| if (protocol == helper_image_protocol_t::ITERM2) | |
| { | |
| /* https://iterm2.com/documentation-images.html */ | |
| g_string_printf (string, "\033]1337;File=inline=1;size=%zu:%s\a\n", | |
| base64_len, base64); | |
| } | |
| else if (protocol == helper_image_protocol_t::KITTY) | |
| { | |
| #define CHUNK_SIZE 4096 | |
| /* https://sw.kovidgoyal.net/kitty/graphics-protocol.html */ | |
| for (size_t pos = 0; pos < base64_len; pos += CHUNK_SIZE) | |
| { | |
| size_t len = base64_len - pos; | |
| if (pos == 0) | |
| g_string_append (string, "\033_Ga=T,f=100,m="); | |
| else | |
| g_string_append (string, "\033_Gm="); | |
| if (len > CHUNK_SIZE) | |
| { | |
| g_string_append (string, "1;"); | |
| g_string_append_len (string, base64 + pos, CHUNK_SIZE); | |
| } | |
| else | |
| { | |
| g_string_append (string, "0;"); | |
| g_string_append_len (string, base64 + pos, len); | |
| } | |
| g_string_append (string, "\033\\"); | |
| } | |
| g_string_append (string, "\n"); | |
| #undef CHUNK_SIZE | |
| } | |
| write_func (closure, (const unsigned char *) string->str, string->len); | |
| g_free (base64); | |
| g_string_free (string, TRUE); | |
| } | |
| #ifdef HAVE_CHAFA | |
| # include <chafa.h> | |
| /* Similar to ansi-print.cc */ | |
| # define CELL_W 8 | |
| # define CELL_H (2 * CELL_W) | |
| static void | |
| chafa_print_image_rgb24 (const void *data, int width, int height, int stride, int level, | |
| helper_image_write_func_t write_func, | |
| void *closure) | |
| { | |
| ChafaTermInfo *term_info; | |
| ChafaSymbolMap *symbol_map; | |
| ChafaCanvasConfig *config; | |
| ChafaCanvas *canvas; | |
| GString *gs; | |
| unsigned int cols = (width + CELL_W - 1) / CELL_W; | |
| unsigned int rows = (height + CELL_H - 1) / CELL_H; | |
| gchar **environ; | |
| ChafaCanvasMode mode; | |
| ChafaPixelMode pixel_mode; | |
| /* Adapt to terminal; use sixels if available, and fall back to symbols | |
| * with as many colors as are supported */ | |
| chafa_set_n_threads (1); // https://github.com/hpjansson/chafa/issues/125#issuecomment-1397475217 | |
| environ = g_get_environ (); | |
| term_info = chafa_term_db_detect (chafa_term_db_get_default (), | |
| environ); | |
| pixel_mode = CHAFA_PIXEL_MODE_SYMBOLS; | |
| if (chafa_term_info_have_seq (term_info, CHAFA_TERM_SEQ_BEGIN_SIXELS)) | |
| { | |
| pixel_mode = CHAFA_PIXEL_MODE_SIXELS; | |
| mode = CHAFA_CANVAS_MODE_TRUECOLOR; | |
| } | |
| else if (chafa_term_info_have_seq (term_info, CHAFA_TERM_SEQ_SET_COLOR_FGBG_DIRECT)) | |
| mode = CHAFA_CANVAS_MODE_TRUECOLOR; | |
| else if (chafa_term_info_have_seq (term_info, CHAFA_TERM_SEQ_SET_COLOR_FGBG_256)) | |
| mode = CHAFA_CANVAS_MODE_INDEXED_240; | |
| else if (chafa_term_info_have_seq (term_info, CHAFA_TERM_SEQ_SET_COLOR_FGBG_16)) | |
| mode = CHAFA_CANVAS_MODE_INDEXED_16; | |
| else if (chafa_term_info_have_seq (term_info, CHAFA_TERM_SEQ_INVERT_COLORS)) | |
| mode = CHAFA_CANVAS_MODE_FGBG_BGFG; | |
| else | |
| mode = CHAFA_CANVAS_MODE_FGBG; | |
| symbol_map = chafa_symbol_map_new (); | |
| chafa_symbol_map_add_by_tags (symbol_map, | |
| (ChafaSymbolTags) (CHAFA_SYMBOL_TAG_BLOCK | |
| | CHAFA_SYMBOL_TAG_SPACE | |
| | (level >= 2 ? CHAFA_SYMBOL_TAG_WEDGE : 0) | |
| | (level >= 3 ? CHAFA_SYMBOL_TAG_ALL : 0) | |
| )); | |
| config = chafa_canvas_config_new (); | |
| chafa_canvas_config_set_canvas_mode (config, mode); | |
| chafa_canvas_config_set_pixel_mode (config, pixel_mode); | |
| chafa_canvas_config_set_cell_geometry (config, CELL_W, CELL_H); | |
| chafa_canvas_config_set_geometry (config, cols, rows); | |
| chafa_canvas_config_set_symbol_map (config, symbol_map); | |
| chafa_canvas_config_set_color_extractor (config, CHAFA_COLOR_EXTRACTOR_MEDIAN); | |
| chafa_canvas_config_set_work_factor (config, 1.0f); | |
| canvas = chafa_canvas_new (config); | |
| chafa_canvas_draw_all_pixels (canvas, | |
| G_BYTE_ORDER == G_LITTLE_ENDIAN | |
| ? CHAFA_PIXEL_BGRA8_PREMULTIPLIED | |
| : CHAFA_PIXEL_ARGB8_PREMULTIPLIED, | |
| (const guint8 *) data, | |
| width, | |
| height, | |
| stride); | |
| gs = chafa_canvas_print (canvas, term_info); | |
| write_func (closure, (const unsigned char *) gs->str, gs->len); | |
| if (pixel_mode != CHAFA_PIXEL_MODE_SIXELS) | |
| write_func (closure, (const unsigned char *) "\n", 1); | |
| g_string_free (gs, TRUE); | |
| chafa_canvas_unref (canvas); | |
| chafa_canvas_config_unref (config); | |
| chafa_symbol_map_unref (symbol_map); | |
| chafa_term_info_unref (term_info); | |
| g_strfreev (environ); | |
| } | |
| #endif /* HAVE_CHAFA */ | |
| static inline helper_image_status_t | |
| helper_image_write_to_ansi_stream_rgb24 (const uint32_t *data, | |
| unsigned int width, | |
| unsigned int height, | |
| unsigned int stride, | |
| helper_image_write_func_t write_func, | |
| void *closure) | |
| { | |
| /* We don't have rows to spare on the terminal window... | |
| * Find the tight image top/bottom and only print in between. */ | |
| uint32_t bg_color = data ? * (uint32_t *) data : 0; | |
| auto orig_data = data; | |
| while (height) | |
| { | |
| unsigned int i; | |
| for (i = 0; i < width; i++) | |
| if (data[i] != bg_color) | |
| break; | |
| if (i < width) | |
| break; | |
| data += stride / 4; | |
| height--; | |
| } | |
| if (orig_data < data) | |
| { | |
| data -= stride / 4; | |
| height++; | |
| } | |
| auto orig_height = height; | |
| while (height) | |
| { | |
| const uint32_t *row = data + (height - 1) * stride / 4; | |
| unsigned int i; | |
| for (i = 0; i < width; i++) | |
| if (row[i] != bg_color) | |
| break; | |
| if (i < width) | |
| break; | |
| height--; | |
| } | |
| if (height < orig_height) | |
| height++; | |
| if (width && height) | |
| { | |
| #ifdef HAVE_CHAFA | |
| const char *env = getenv ("HB_CHAFA"); | |
| int chafa_level = 1; | |
| if (env) | |
| chafa_level = atoi (env); | |
| if (chafa_level) | |
| chafa_print_image_rgb24 (data, width, height, stride, chafa_level, | |
| write_func, closure); | |
| else | |
| #endif | |
| ansi_print_image_rgb24 (data, width, height, stride / 4, | |
| write_func, closure); | |
| } | |
| return HELPER_IMAGE_STATUS_SUCCESS; | |
| } | |
| #endif |