icat-mini.sh (19230B)
1 #!/bin/sh 2 3 # vim: shiftwidth=4 4 5 script_name="$(basename "$0")" 6 7 short_help="Usage: $script_name [OPTIONS] <image_file> 8 9 This is a script to display images in the terminal using the kitty graphics 10 protocol with Unicode placeholders. It is very basic, please use something else 11 if you have alternatives. 12 13 Options: 14 -h Show this help. 15 -s SCALE The scale of the image, may be floating point. 16 -c N, --cols N The number of columns. 17 -r N, --rows N The number of rows. 18 --max-cols N The maximum number of columns. 19 --max-rows N The maximum number of rows. 20 --cell-size WxH The cell size in pixels. 21 -m METHOD The uploading method, may be 'file', 'direct' or 'auto'. 22 --speed SPEED The multiplier for the animation speed (float). 23 " 24 25 # Exit the script on keyboard interrupt 26 trap "echo 'icat-mini was interrupted' >&2; exit 1" INT 27 28 cols="" 29 rows="" 30 file="" 31 tty="/dev/tty" 32 uploading_method="auto" 33 cell_size="" 34 scale=1 35 max_cols="" 36 max_rows="" 37 speed="" 38 39 # Parse the command line. 40 while [ $# -gt 0 ]; do 41 case "$1" in 42 -c|--columns|--cols) 43 cols="$2" 44 shift 2 45 ;; 46 -r|--rows|-l|--lines) 47 rows="$2" 48 shift 2 49 ;; 50 -s|--scale) 51 scale="$2" 52 shift 2 53 ;; 54 -h|--help) 55 echo "$short_help" 56 exit 0 57 ;; 58 -m|--upload-method|--uploading-method) 59 uploading_method="$2" 60 shift 2 61 ;; 62 --cell-size) 63 cell_size="$2" 64 shift 2 65 ;; 66 --max-cols) 67 max_cols="$2" 68 shift 2 69 ;; 70 --max-rows) 71 max_rows="$2" 72 shift 2 73 ;; 74 --speed) 75 speed="$2" 76 shift 2 77 ;; 78 --) 79 file="$2" 80 shift 2 81 ;; 82 -*) 83 echo "Unknown option: $1" >&2 84 exit 1 85 ;; 86 *) 87 if [ -n "$file" ]; then 88 echo "Multiple image files are not supported: $file and $1" >&2 89 exit 1 90 fi 91 file="$1" 92 shift 93 ;; 94 esac 95 done 96 97 file="$(realpath "$file")" 98 99 ##################################################################### 100 # Adjust the terminal state 101 ##################################################################### 102 103 stty_orig="$(stty -g < "$tty")" 104 stty -echo < "$tty" 105 # Disable ctrl-z. Pressing ctrl-z during image uploading may cause some 106 # horrible issues otherwise. 107 stty susp undef < "$tty" 108 stty -icanon < "$tty" 109 110 restore_echo() { 111 [ -n "$stty_orig" ] || return 112 stty $stty_orig < "$tty" 113 } 114 115 trap restore_echo EXIT TERM 116 117 ##################################################################### 118 # Detect imagemagick 119 ##################################################################### 120 121 # If there is the 'magick' command, use it instead of separate 'convert' and 122 # 'identify' commands. 123 if command -v magick > /dev/null; then 124 identify="magick identify" 125 convert="magick" 126 else 127 identify="identify" 128 convert="convert" 129 fi 130 131 ##################################################################### 132 # Detect tmux 133 ##################################################################### 134 135 # Check if we are inside tmux. 136 inside_tmux="" 137 if [ -n "$TMUX" ]; then 138 case "$TERM" in 139 *tmux*|*screen*) 140 inside_tmux=1 141 ;; 142 esac 143 fi 144 145 ##################################################################### 146 # Compute the number of rows and columns 147 ##################################################################### 148 149 is_pos_int() { 150 if [ -z "$1" ]; then 151 return 1 # false 152 fi 153 if [ -z "$(printf '%s' "$1" | tr -d '[:digit:]')" ]; then 154 if [ "$1" -gt 0 ]; then 155 return 0 # true 156 fi 157 fi 158 return 1 # false 159 } 160 161 if [ -n "$cols" ] || [ -n "$rows" ]; then 162 if [ -n "$max_cols" ] || [ -n "$max_rows" ]; then 163 echo "You can't specify both max-cols/rows and cols/rows" >&2 164 exit 1 165 fi 166 fi 167 168 # Get the max number of cols and rows. 169 [ -n "$max_cols" ] || max_cols="$(tput cols)" 170 [ -n "$max_rows" ] || max_rows="$(tput lines)" 171 if [ "$max_rows" -gt 255 ]; then 172 max_rows=255 173 fi 174 175 python_ioctl_command="import array, fcntl, termios 176 buf = array.array('H', [0, 0, 0, 0]) 177 fcntl.ioctl(0, termios.TIOCGWINSZ, buf) 178 print(int(buf[2]/buf[1]), int(buf[3]/buf[0]))" 179 180 # Get the cell size in pixels if either cols or rows are not specified. 181 if [ -z "$cols" ] || [ -z "$rows" ]; then 182 cell_width="" 183 cell_height="" 184 # If the cell size is specified, use it. 185 if [ -n "$cell_size" ]; then 186 cell_width="${cell_size%x*}" 187 cell_height="${cell_size#*x}" 188 if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then 189 echo "Invalid cell size: $cell_size" >&2 190 exit 1 191 fi 192 fi 193 # Otherwise try to use TIOCGWINSZ ioctl via python. 194 if [ -z "$cell_width" ] || [ -z "$cell_height" ]; then 195 cell_size_ioctl="$(python3 -c "$python_ioctl_command" < "$tty" 2> /dev/null)" 196 cell_width="${cell_size_ioctl% *}" 197 cell_height="${cell_size_ioctl#* }" 198 if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then 199 cell_width="" 200 cell_height="" 201 fi 202 fi 203 # If it didn't work, try to use csi XTWINOPS. 204 if [ -z "$cell_width" ] || [ -z "$cell_height" ]; then 205 if [ -n "$inside_tmux" ]; then 206 printf '\ePtmux;\e\e[16t\e\\' >> "$tty" 207 else 208 printf '\e[16t' >> "$tty" 209 fi 210 # The expected response will look like ^[[6;<height>;<width>t 211 term_response="" 212 while true; do 213 char=$(dd bs=1 count=1 <"$tty" 2>/dev/null) 214 if [ "$char" = "t" ]; then 215 break 216 fi 217 term_response="$term_response$char" 218 done 219 cell_height="$(printf '%s' "$term_response" | cut -d ';' -f 2)" 220 cell_width="$(printf '%s' "$term_response" | cut -d ';' -f 3)" 221 if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then 222 cell_width=8 223 cell_height=16 224 fi 225 fi 226 fi 227 228 # Compute a formula with bc and round to the nearest integer. 229 bc_round() { 230 LC_NUMERIC=C printf '%.0f' "$(printf '%s\n' "scale=2;($1) + 0.5" | bc)" 231 } 232 233 # Compute the number of rows and columns of the image. 234 if [ -z "$cols" ] || [ -z "$rows" ]; then 235 # Get the size of the image and its resolution. If it's an animation, use 236 # the first frame. 237 format_output="$($identify -format '%w %h\n' "$file" | head -1)" 238 img_width="$(printf '%s' "$format_output" | cut -d ' ' -f 1)" 239 img_height="$(printf '%s' "$format_output" | cut -d ' ' -f 2)" 240 if ! is_pos_int "$img_width" || ! is_pos_int "$img_height"; then 241 echo "Couldn't get image size from identify: $format_output" >&2 242 echo >&2 243 exit 1 244 fi 245 opt_cols_expr="(${scale}*${img_width}/${cell_width})" 246 opt_rows_expr="(${scale}*${img_height}/${cell_height})" 247 if [ -z "$cols" ] && [ -z "$rows" ]; then 248 # If columns and rows are not specified, compute the optimal values 249 # using the information about rows and columns per inch. 250 cols="$(bc_round "$opt_cols_expr")" 251 rows="$(bc_round "$opt_rows_expr")" 252 # Make sure that automatically computed rows and columns are within some 253 # sane limits 254 if [ "$cols" -gt "$max_cols" ]; then 255 rows="$(bc_round "$rows * $max_cols / $cols")" 256 cols="$max_cols" 257 fi 258 if [ "$rows" -gt "$max_rows" ]; then 259 cols="$(bc_round "$cols * $max_rows / $rows")" 260 rows="$max_rows" 261 fi 262 elif [ -z "$cols" ]; then 263 # If only one dimension is specified, compute the other one to match the 264 # aspect ratio as close as possible. 265 cols="$(bc_round "${opt_cols_expr}*${rows}/${opt_rows_expr}")" 266 elif [ -z "$rows" ]; then 267 rows="$(bc_round "${opt_rows_expr}*${cols}/${opt_cols_expr}")" 268 fi 269 270 if [ "$cols" -lt 1 ]; then 271 cols=1 272 fi 273 if [ "$rows" -lt 1 ]; then 274 rows=1 275 fi 276 fi 277 278 ##################################################################### 279 # Generate an image id 280 ##################################################################### 281 282 image_id="" 283 while [ -z "$image_id" ]; do 284 image_id="$(shuf -i 16777217-4294967295 -n 1)" 285 # Check that the id requires 24-bit fg colors. 286 if [ "$(expr \( "$image_id" / 256 \) % 65536)" -eq 0 ]; then 287 image_id="" 288 fi 289 done 290 291 ##################################################################### 292 # Uploading the image 293 ##################################################################### 294 295 # Choose the uploading method 296 if [ "$uploading_method" = "auto" ]; then 297 if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ] || [ -n "$SSH_CONNECTION" ]; then 298 uploading_method="direct" 299 else 300 uploading_method="file" 301 fi 302 fi 303 304 # Functions to emit the start and the end of a graphics command. 305 if [ -n "$inside_tmux" ]; then 306 # If we are in tmux we have to wrap the command in Ptmux. 307 graphics_command_start='\ePtmux;\e\e_G' 308 graphics_command_end='\e\e\\\e\\' 309 else 310 graphics_command_start='\e_G' 311 graphics_command_end='\e\\' 312 fi 313 314 start_gr_command() { 315 printf "$graphics_command_start" >> "$tty" 316 } 317 end_gr_command() { 318 printf "$graphics_command_end" >> "$tty" 319 } 320 321 # Send a graphics command with the correct start and end 322 gr_command() { 323 start_gr_command 324 printf '%s' "$1" >> "$tty" 325 end_gr_command 326 } 327 328 # Send an uploading command. Usage: gr_upload <action> <command> <file> 329 # Where <action> is a part of command that specifies the action, it will be 330 # repeated for every chunk (if the method is direct), and <command> is the rest 331 # of the command that specifies the image parameters. <action> and <command> 332 # must not include the transmission method or ';'. 333 # Example: 334 # gr_upload "a=T,q=2" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file" 335 gr_upload() { 336 arg_action="$1" 337 arg_command="$2" 338 arg_file="$3" 339 if [ "$uploading_method" = "file" ]; then 340 # base64-encode the filename 341 encoded_filename=$(printf '%s' "$arg_file" | base64 -w0) 342 gr_command "${arg_action},${arg_command},t=f;${encoded_filename}" 343 fi 344 if [ "$uploading_method" = "direct" ]; then 345 # Create a temporary directory to store the chunked image. 346 chunkdir="$(mktemp -d)" 347 if [ ! "$chunkdir" ] || [ ! -d "$chunkdir" ]; then 348 echo "Can't create a temp dir" >&2 349 exit 1 350 fi 351 # base64-encode the file and split it into chunks. The size of each 352 # graphics command shouldn't be more than 4096, so we set the size of an 353 # encoded chunk to be 3968, slightly less than that. 354 chunk_size=3968 355 cat "$arg_file" | base64 -w0 | split -b "$chunk_size" - "$chunkdir/chunk_" 356 357 # Issue a command indicating that we want to start data transmission for 358 # a new image. 359 gr_command "${arg_action},${arg_command},t=d,m=1" 360 361 # Transmit chunks. 362 for chunk in "$chunkdir/chunk_"*; do 363 start_gr_command 364 printf '%s' "${arg_action},i=${image_id},m=1;" >> "$tty" 365 cat "$chunk" >> "$tty" 366 end_gr_command 367 rm "$chunk" 368 done 369 370 # Tell the terminal that we are done. 371 gr_command "${arg_action},i=$image_id,m=0" 372 373 # Remove the temporary directory. 374 rmdir "$chunkdir" 375 fi 376 } 377 378 delayed_frame_dir_cleanup() { 379 arg_frame_dir="$1" 380 sleep 2 381 if [ -n "$arg_frame_dir" ]; then 382 for frame in "$arg_frame_dir"/frame_*.png; do 383 rm "$frame" 384 done 385 rmdir "$arg_frame_dir" 386 fi 387 } 388 389 upload_image_and_print_placeholder() { 390 # Check if the file is an animation. 391 frame_count=$($identify -format '%n\n' "$file" | head -n 1) 392 if [ "$frame_count" -gt 1 ]; then 393 # The file is an animation, decompose into frames and upload each frame. 394 frame_dir="$(mktemp -d)" 395 frame_dir="$HOME/temp/frames${frame_dir}" 396 mkdir -p "$frame_dir" 397 if [ ! "$frame_dir" ] || [ ! -d "$frame_dir" ]; then 398 echo "Can't create a temp dir for frames" >&2 399 exit 1 400 fi 401 402 # Decompose the animation into separate frames. 403 $convert "$file" -coalesce "$frame_dir/frame_%06d.png" 404 405 # Get all frame delays at once, in centiseconds, as a space-separated 406 # string. 407 delays=$($identify -format "%T " "$file") 408 409 frame_number=1 410 for frame in "$frame_dir"/frame_*.png; do 411 # Read the delay for the current frame and convert it from 412 # centiseconds to milliseconds. 413 delay=$(printf '%s' "$delays" | cut -d ' ' -f "$frame_number") 414 delay=$((delay * 10)) 415 # If the delay is 0, set it to 100ms. 416 if [ "$delay" -eq 0 ]; then 417 delay=100 418 fi 419 420 if [ -n "$speed" ]; then 421 delay=$(bc_round "$delay / $speed") 422 fi 423 424 if [ "$frame_number" -eq 1 ]; then 425 # Upload the first frame with a=T 426 gr_upload "q=2,a=T" "f=100,U=1,i=${image_id},c=${cols},r=${rows}" "$frame" 427 # Set the delay for the first frame and also play the animation 428 # in loading mode (s=2). 429 gr_command "a=a,v=1,s=2,r=${frame_number},z=${delay},i=${image_id}" 430 # Print the placeholder after the first frame to reduce the wait 431 # time. 432 print_placeholder 433 else 434 # Upload subsequent frames with a=f 435 gr_upload "q=2,a=f" "f=100,i=${image_id},z=${delay}" "$frame" 436 fi 437 438 frame_number=$((frame_number + 1)) 439 done 440 441 # Play the animation in loop mode (s=3). 442 gr_command "a=a,v=1,s=3,i=${image_id}" 443 444 # Remove the temporary directory, but do it in the background with a 445 # delay to avoid removing files before they are loaded by the terminal. 446 delayed_frame_dir_cleanup "$frame_dir" 2> /dev/null & 447 else 448 # The file is not an animation, upload it directly 449 gr_upload "q=2,a=T" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file" 450 # Print the placeholder 451 print_placeholder 452 fi 453 } 454 455 ##################################################################### 456 # Printing the image placeholder 457 ##################################################################### 458 459 print_placeholder() { 460 # Each line starts with the escape sequence to set the foreground color to 461 # the image id. 462 blue="$(expr "$image_id" % 256 )" 463 green="$(expr \( "$image_id" / 256 \) % 256 )" 464 red="$(expr \( "$image_id" / 65536 \) % 256 )" 465 line_start="$(printf "\e[38;2;%d;%d;%dm" "$red" "$green" "$blue")" 466 line_end="$(printf "\e[39;m")" 467 468 id4th="$(expr \( "$image_id" / 16777216 \) % 256 )" 469 eval "id_diacritic=\$d${id4th}" 470 471 # Reset the brush state, mostly to reset the underline color. 472 printf "\e[0m" 473 474 # Fill the output with characters representing the image 475 for y in $(seq 0 "$(expr "$rows" - 1)"); do 476 eval "row_diacritic=\$d${y}" 477 printf '%s' "$line_start" 478 for x in $(seq 0 "$(expr "$cols" - 1)"); do 479 eval "col_diacritic=\$d${x}" 480 # Note that when $x is out of bounds, the column diacritic will 481 # be empty, meaning that the column should be guessed by the 482 # terminal. 483 if [ "$x" -ge "$num_diacritics" ]; then 484 printf '%s' "${placeholder}${row_diacritic}" 485 else 486 printf '%s' "${placeholder}${row_diacritic}${col_diacritic}${id_diacritic}" 487 fi 488 done 489 printf '%s\n' "$line_end" 490 done 491 492 printf "\e[0m" 493 } 494 495 d0="̅" 496 d1="̍" 497 d2="̎" 498 d3="̐" 499 d4="̒" 500 d5="̽" 501 d6="̾" 502 d7="̿" 503 d8="͆" 504 d9="͊" 505 d10="͋" 506 d11="͌" 507 d12="͐" 508 d13="͑" 509 d14="͒" 510 d15="͗" 511 d16="͛" 512 d17="ͣ" 513 d18="ͤ" 514 d19="ͥ" 515 d20="ͦ" 516 d21="ͧ" 517 d22="ͨ" 518 d23="ͩ" 519 d24="ͪ" 520 d25="ͫ" 521 d26="ͬ" 522 d27="ͭ" 523 d28="ͮ" 524 d29="ͯ" 525 d30="҃" 526 d31="҄" 527 d32="҅" 528 d33="҆" 529 d34="҇" 530 d35="֒" 531 d36="֓" 532 d37="֔" 533 d38="֕" 534 d39="֗" 535 d40="֘" 536 d41="֙" 537 d42="֜" 538 d43="֝" 539 d44="֞" 540 d45="֟" 541 d46="֠" 542 d47="֡" 543 d48="֨" 544 d49="֩" 545 d50="֫" 546 d51="֬" 547 d52="֯" 548 d53="ׄ" 549 d54="ؐ" 550 d55="ؑ" 551 d56="ؒ" 552 d57="ؓ" 553 d58="ؔ" 554 d59="ؕ" 555 d60="ؖ" 556 d61="ؗ" 557 d62="ٗ" 558 d63="٘" 559 d64="ٙ" 560 d65="ٚ" 561 d66="ٛ" 562 d67="ٝ" 563 d68="ٞ" 564 d69="ۖ" 565 d70="ۗ" 566 d71="ۘ" 567 d72="ۙ" 568 d73="ۚ" 569 d74="ۛ" 570 d75="ۜ" 571 d76="۟" 572 d77="۠" 573 d78="ۡ" 574 d79="ۢ" 575 d80="ۤ" 576 d81="ۧ" 577 d82="ۨ" 578 d83="۫" 579 d84="۬" 580 d85="ܰ" 581 d86="ܲ" 582 d87="ܳ" 583 d88="ܵ" 584 d89="ܶ" 585 d90="ܺ" 586 d91="ܽ" 587 d92="ܿ" 588 d93="݀" 589 d94="݁" 590 d95="݃" 591 d96="݅" 592 d97="݇" 593 d98="݉" 594 d99="݊" 595 d100="߫" 596 d101="߬" 597 d102="߭" 598 d103="߮" 599 d104="߯" 600 d105="߰" 601 d106="߱" 602 d107="߳" 603 d108="ࠖ" 604 d109="ࠗ" 605 d110="࠘" 606 d111="࠙" 607 d112="ࠛ" 608 d113="ࠜ" 609 d114="ࠝ" 610 d115="ࠞ" 611 d116="ࠟ" 612 d117="ࠠ" 613 d118="ࠡ" 614 d119="ࠢ" 615 d120="ࠣ" 616 d121="ࠥ" 617 d122="ࠦ" 618 d123="ࠧ" 619 d124="ࠩ" 620 d125="ࠪ" 621 d126="ࠫ" 622 d127="ࠬ" 623 d128="࠭" 624 d129="॑" 625 d130="॓" 626 d131="॔" 627 d132="ྂ" 628 d133="ྃ" 629 d134="྆" 630 d135="྇" 631 d136="፝" 632 d137="፞" 633 d138="፟" 634 d139="៝" 635 d140="᤺" 636 d141="ᨗ" 637 d142="᩵" 638 d143="᩶" 639 d144="᩷" 640 d145="᩸" 641 d146="᩹" 642 d147="᩺" 643 d148="᩻" 644 d149="᩼" 645 d150="᭫" 646 d151="᭭" 647 d152="᭮" 648 d153="᭯" 649 d154="᭰" 650 d155="᭱" 651 d156="᭲" 652 d157="᭳" 653 d158="᳐" 654 d159="᳑" 655 d160="᳒" 656 d161="᳚" 657 d162="᳛" 658 d163="᳠" 659 d164="᷀" 660 d165="᷁" 661 d166="᷃" 662 d167="᷄" 663 d168="᷅" 664 d169="᷆" 665 d170="᷇" 666 d171="᷈" 667 d172="᷉" 668 d173="᷋" 669 d174="᷌" 670 d175="᷑" 671 d176="᷒" 672 d177="ᷓ" 673 d178="ᷔ" 674 d179="ᷕ" 675 d180="ᷖ" 676 d181="ᷗ" 677 d182="ᷘ" 678 d183="ᷙ" 679 d184="ᷚ" 680 d185="ᷛ" 681 d186="ᷜ" 682 d187="ᷝ" 683 d188="ᷞ" 684 d189="ᷟ" 685 d190="ᷠ" 686 d191="ᷡ" 687 d192="ᷢ" 688 d193="ᷣ" 689 d194="ᷤ" 690 d195="ᷥ" 691 d196="ᷦ" 692 d197="᷾" 693 d198="⃐" 694 d199="⃑" 695 d200="⃔" 696 d201="⃕" 697 d202="⃖" 698 d203="⃗" 699 d204="⃛" 700 d205="⃜" 701 d206="⃡" 702 d207="⃧" 703 d208="⃩" 704 d209="⃰" 705 d210="⳯" 706 d211="⳰" 707 d212="⳱" 708 d213="ⷠ" 709 d214="ⷡ" 710 d215="ⷢ" 711 d216="ⷣ" 712 d217="ⷤ" 713 d218="ⷥ" 714 d219="ⷦ" 715 d220="ⷧ" 716 d221="ⷨ" 717 d222="ⷩ" 718 d223="ⷪ" 719 d224="ⷫ" 720 d225="ⷬ" 721 d226="ⷭ" 722 d227="ⷮ" 723 d228="ⷯ" 724 d229="ⷰ" 725 d230="ⷱ" 726 d231="ⷲ" 727 d232="ⷳ" 728 d233="ⷴ" 729 d234="ⷵ" 730 d235="ⷶ" 731 d236="ⷷ" 732 d237="ⷸ" 733 d238="ⷹ" 734 d239="ⷺ" 735 d240="ⷻ" 736 d241="ⷼ" 737 d242="ⷽ" 738 d243="ⷾ" 739 d244="ⷿ" 740 d245="꙯" 741 d246="꙼" 742 d247="꙽" 743 d248="꛰" 744 d249="꛱" 745 d250="꣠" 746 d251="꣡" 747 d252="꣢" 748 d253="꣣" 749 d254="꣤" 750 d255="꣥" 751 d256="꣦" 752 d257="꣧" 753 d258="꣨" 754 d259="꣩" 755 d260="꣪" 756 d261="꣫" 757 d262="꣬" 758 d263="꣭" 759 d264="꣮" 760 d265="꣯" 761 d266="꣰" 762 d267="꣱" 763 d268="ꪰ" 764 d269="ꪲ" 765 d270="ꪳ" 766 d271="ꪷ" 767 d272="ꪸ" 768 d273="ꪾ" 769 d274="꪿" 770 d275="꫁" 771 d276="︠" 772 d277="︡" 773 d278="︢" 774 d279="︣" 775 d280="︤" 776 d281="︥" 777 d282="︦" 778 d283="𐨏" 779 d284="𐨸" 780 d285="𝆅" 781 d286="𝆆" 782 d287="𝆇" 783 d288="𝆈" 784 d289="𝆉" 785 d290="𝆪" 786 d291="𝆫" 787 d292="𝆬" 788 d293="𝆭" 789 d294="𝉂" 790 d295="𝉃" 791 d296="𝉄" 792 793 num_diacritics="297" 794 795 placeholder="" 796 797 ##################################################################### 798 # Upload the image and print the placeholder 799 ##################################################################### 800 801 upload_image_and_print_placeholder