diff --git a/docs/graphics-protocol.rst b/docs/graphics-protocol.rst index 2b2bf6b6715..585abc3b8be 100644 --- a/docs/graphics-protocol.rst +++ b/docs/graphics-protocol.rst @@ -453,31 +453,45 @@ of lines and columns:: The image will be fit to the specified rectangle, its aspect ratio preserved (in the future more scaling modes may be added to the protocol). Now you can display the image using the placeholder character, encoding the image ID in its -foreground color (in 256 color mode the ID must be within the range ``[1; 255]``; -in 24-bit mode the ID is defined by the formula ``r << 16 | g << 8 | b``). The -row and column values are specified with diacritics listed in -``rowcolumn-diacritics.txt``. For example, here is how you can print a 2x2 +foreground color. The row and column values are specified with diacritics listed +in ``rowcolumn-diacritics.txt``. For example, here is how you can print a 2x2 placeholder for image ID 42:: printf "\e[38;5;42m\U10EEEE\U0305\U0305\U10EEEE\U0305\U030D\n" printf "\e[38;5;42m\U10EEEE\U030D\U0305\U10EEEE\U030D\U030D\n" +By using only the foreground color you are limited to 8-bit IDs in 256 color +mode and to 24-bit IDs in true color mode. If you need more bits for the image +ID, you can specify the most significant byte via the third diacritic. For +example, this is the placeholder for the image ID ``738197504 = 42 + 2 << 24``:: + + printf "\e[38;5;42m\U10EEEE\U0305\U0305\U030E\U10EEEE\U0305\U030D\U030E\n" + printf "\e[38;5;42m\U10EEEE\U030D\U0305\U030E\U10EEEE\U030D\U030D\U030E\n" + You can also specify a placement ID using the underline color (if it's omitted or zero, the terminal may choose any virtual placement of the given image). The background color is interpreted as the background color, visible if the image is transparent. Other text attributes are reserved for future use. -Row and column diacritics may also be omitted, in which case the terminal should -guess row and column values from the surrounding cells, in particular: - -- If only the row diacritic is present, and the cell to the left of the current - one is an image placeholder with the same image and placement IDs and the same - row, then the column of the current cell is the column of the cell to the left - plus one. -- If no diacritics are present, and the cell to the left of the current one is - an image placeholder with the same image and placement IDs, then the row of - the current cell is the row of the cell to the left, and the column is the - column of the cell to the left plus one. +Row, column and most significant byte diacritics may also be omitted, in which +case the placeholder cell will inherit the missing values from the placeholder +cell to the left: + +- If no diacritics are present, and the previous placeholder cell has the same + foreground and underline colors, then the row of the current cell will be the + row of the cell to the left, the column will be the column of the cell to the + left plus one, and the most significant image ID byte will be the most + significant image ID byte of the cell to the left. +- If only the row diacritic is present, and the previous placeholder cell has + the same row and the same foreground and underline colors, then the column of + the current cell will be the column of the cell to the left plus one, and the + most significant image ID byte will be the most significant image ID byte of + the cell to the left. +- If only the row and column diacritics are present, and the previous + placeholder cell has the same row, the same foreground and underline colors, + and its column is one less than the current column, then the most significant + image ID byte of the current cell will be the most significant image ID byte + of the cell to the left. These rules are applied left-to-right, which allows specifying only row diacritics of the first column, i.e. here is a 2 rows by 3 columns placeholder:: diff --git a/kittens/icat/main.py b/kittens/icat/main.py index 1f08358f89e..74bb048aa04 100644 --- a/kittens/icat/main.py +++ b/kittens/icat/main.py @@ -260,6 +260,7 @@ def write_unicode_placeholder(place: Optional['Place'], cmd: GraphicsCommand, al start_x = place.left + 1 start_y = place.top + 1 color = f'\033[38;2;{(cmd.i >> 16) & 255};{(cmd.i >> 8) & 255};{cmd.i & 255}m'.encode('ascii') + id_higher_8bits = (cmd.i >> 24) & 255 for row in range(cmd.r): if place: sys.stdout.buffer.write(f'\033[{start_y + row};{start_x}H'.encode('ascii')) @@ -270,6 +271,7 @@ def write_unicode_placeholder(place: Optional['Place'], cmd: GraphicsCommand, al sys.stdout.buffer.write(placeholder_char) sys.stdout.buffer.write(get_rowcolumn_diacritic(row)) sys.stdout.buffer.write(get_rowcolumn_diacritic(col)) + sys.stdout.buffer.write(get_rowcolumn_diacritic(id_higher_8bits)) sys.stdout.buffer.write(b'\033[39m') if not place: sys.stdout.buffer.write(b'\n') @@ -414,6 +416,19 @@ class ParsedOpts: unicode_placeholder = False +def generate_random_image_id() -> int: + import struct + # Generate a 32-bit image id using rejection sampling such that the most + # significant byte and the two bytes in the middle are non-zero to avoid + # collisions with applications that cannot represent non-zero most + # significant bytes (which is represented by the third combining character) + # or two non-zero bytes in the middle (which requires 24-bit color mode). + while True: + num = struct.unpack('@I', os.urandom(4))[0] + if (num & 0xFF000000 != 0) and (num & 0x00FFFF00 != 0): + return int(num) + + def process(path: str, args: IcatCLIOptions, parsed_opts: ParsedOpts, is_tempfile: bool) -> bool: m = identify(path) ss = get_screen_size() @@ -447,19 +462,8 @@ def process(path: str, args: IcatCLIOptions, parsed_opts: ParsedOpts, is_tempfil outfile, width, height = frame_data.frames[0].path, frame_data.width, frame_data.height use_number = 0 use_image_id = 0 - if parsed_opts.unicode_placeholder or needs_frame_uploading: - import struct - use_number = max(1, struct.unpack('@I', os.urandom(4))[0]) if parsed_opts.unicode_placeholder: - # If we are using the unicode placeholder approach, we need to know the - # image id of the image. We use a random image id, which doesn't - # guarantee uniqueness, but should be good enough. We avoid using image - # ids below 1024 to minimize the probability of collision with images - # displayed using other methods. Also the image id must be less than - # 2**24 to be representable as a foreground color. - min_image_id = 1024 - use_image_id = min_image_id + (use_number % (2**24 - min_image_id)) - use_number = 0 + use_image_id = generate_random_image_id() show( outfile, width, height, parsed_opts.z_index, fmt, transmit_mode, align=args.align, place=parsed_opts.place, use_number=use_number, @@ -467,6 +471,9 @@ def process(path: str, args: IcatCLIOptions, parsed_opts: ParsedOpts, is_tempfil unicode_placeholder=parsed_opts.unicode_placeholder ) if needs_frame_uploading: + if not use_image_id: + import struct + use_number = max(1, struct.unpack('@I', os.urandom(4))[0]) show_frames(frame_data, use_number, use_image_id, args.loop) if not can_transfer_with_files: for fr in frame_data.frames: diff --git a/kitty/screen.c b/kitty/screen.c index ea406ec79dd..456b0a9fe8b 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -2294,58 +2294,77 @@ screen_render_line_graphics(Screen *self, Line *line, int32_t row, bool had_imag return; index_type i; uint32_t run_length = 0; - // Note that all these values are base-1, zero means unknown or incorrect. - uint32_t prev_image_id = 0; + uint32_t prev_img_id_lower24bits = 0; uint32_t prev_placement_id = 0; + // Note that the following values are 1-based, zero means unknown or incorrect. + uint32_t prev_img_id_higher8bits = 0; uint32_t prev_img_row = 0; uint32_t prev_img_col = 0; for (i = 0; i < line->xnum; i++) { CPUCell *cpu_cell = line->cpu_cells + i; GPUCell *gpu_cell = line->gpu_cells + i; - uint32_t cur_image_id = 0; + uint32_t cur_img_id_lower24bits = 0; uint32_t cur_placement_id = 0; + uint32_t cur_img_id_higher8bits = 0; uint32_t cur_img_row = 0; uint32_t cur_img_col = 0; if (cpu_cell->ch == IMAGE_PLACEHOLDER_CHAR) { - // The image id is encoded in the foreground color, and the - // placement id is (optionally) in the underline color. - cur_image_id = color_to_id(gpu_cell->fg); + // The lower 24 bits of the image id are encoded in the foreground + // color, and the placement id is (optionally) in the underline color. + cur_img_id_lower24bits = color_to_id(gpu_cell->fg); cur_placement_id = color_to_id(gpu_cell->decoration_fg); // If the char has diacritics, use them as row and column indices. - if (cpu_cell->cc_idx[1]) - cur_img_col = diacritic_to_rowcolumn(cpu_cell->cc_idx[1]); if (cpu_cell->cc_idx[0]) cur_img_row = diacritic_to_rowcolumn(cpu_cell->cc_idx[0]); + if (cpu_cell->cc_idx[1]) + cur_img_col = diacritic_to_rowcolumn(cpu_cell->cc_idx[1]); + // The third diacritic is used to encode the higher 8 bits of the + // image id (optional). + if (cpu_cell->cc_idx[2]) + cur_img_id_higher8bits = diacritic_to_rowcolumn(cpu_cell->cc_idx[2]); } - if (cur_image_id == prev_image_id && cur_placement_id == prev_placement_id && + // The current run is continued if the lower 24 bits of the image id and + // the placement id are the same as in the previous cell and everything + // else is unknown or compatible with the previous cell. + if (run_length > 0 && cur_img_id_lower24bits == prev_img_id_lower24bits && + cur_placement_id == prev_placement_id && (!cur_img_row || cur_img_row == prev_img_row) && - (!cur_img_col || cur_img_col == prev_img_col + 1)) { + (!cur_img_col || cur_img_col == prev_img_col + 1) && + (!cur_img_id_higher8bits || cur_img_id_higher8bits == prev_img_id_higher8bits)) { // This cell continues the current run. run_length++; // If some values are unknown, infer them from the previous cell. - cur_img_row = prev_img_row ? prev_img_row : 1; + cur_img_row = MAX(prev_img_row, 1u); cur_img_col = prev_img_col + 1; + cur_img_id_higher8bits = MAX(prev_img_id_higher8bits, 1u); } else { - if (prev_image_id) { - // This cell breaks the current run. Render the current run. + // This cell breaks the current run. Render the current run if it + // has a non-zero length. + if (run_length > 0) { + uint32_t img_id = prev_img_id_lower24bits | (prev_img_id_higher8bits - 1) << 24; grman_put_cell_image( - self->grman, row, i - run_length, prev_image_id, + self->grman, row, i - run_length, img_id, prev_placement_id, prev_img_col - run_length, prev_img_row - 1, run_length, 1, self->cell_size); } // Start a new run. - run_length = 1; - if (!cur_img_col) cur_img_col = 1; - if (!cur_img_row) cur_img_row = 1; + if (cpu_cell->ch == IMAGE_PLACEHOLDER_CHAR) { + run_length = 1; + if (!cur_img_col) cur_img_col = 1; + if (!cur_img_row) cur_img_row = 1; + if (!cur_img_id_higher8bits) cur_img_id_higher8bits = 1; + } } - prev_image_id = cur_image_id; + prev_img_id_lower24bits = cur_img_id_lower24bits; + prev_img_id_higher8bits = cur_img_id_higher8bits; prev_placement_id = cur_placement_id; prev_img_row = cur_img_row; prev_img_col = cur_img_col; } - if (prev_image_id) { + if (run_length > 0) { // Render the last run. - grman_put_cell_image(self->grman, row, i - run_length, prev_image_id, + uint32_t img_id = prev_img_id_lower24bits | (prev_img_id_higher8bits - 1) << 24; + grman_put_cell_image(self->grman, row, i - run_length, img_id, prev_placement_id, prev_img_col - run_length, prev_img_row - 1, run_length, 1, self->cell_size); }