set -e
_pt_is_tmux() {
case "$TERM" in
screen*|tmux*) return 0 ;;
*) return 1 ;;
esac
}
_pt_print_osc() {
if _pt_is_tmux; then
printf '\033Ptmux;\033'
fi
printf '\033]'
}
_pt_print_st() {
printf '\007'
if _pt_is_tmux; then
printf '\033\\'
fi
}
_pt_image_dims() {
_f="$1"
if [ ! -f "$_f" ]; then
return
fi
if command -v sips >/dev/null 2>&1; then
_w=$(sips --getProperty pixelWidth "$_f" 2>/dev/null | awk '/pixelWidth/{print $2}')
_h=$(sips --getProperty pixelHeight "$_f" 2>/dev/null | awk '/pixelHeight/{print $2}')
if [ -n "$_w" ] && [ -n "$_h" ]; then
printf '%s %s' "$_w" "$_h"
return
fi
fi
if command -v identify >/dev/null 2>&1; then
_dims=$(identify -format '%w %h' "$_f" 2>/dev/null)
if [ -n "$_dims" ]; then
printf '%s' "$_dims"
return
fi
fi
if command -v file >/dev/null 2>&1; then
_pair=$(file "$_f" 2>/dev/null | grep -oE '[0-9]+ x [0-9]+' | head -1)
if [ -n "$_pair" ]; then
printf '%s' "$_pair" | sed 's/ x / /'
return
fi
fi
}
_pt_tmux_advance_cursor() {
_rows_to_advance="$1"
if [ "$_rows_to_advance" -le 0 ] 2>/dev/null; then
return
fi
_i=0
while [ "$_i" -lt "$_rows_to_advance" ]; do
printf '\n'
_i=$((_i + 1))
done
}
_pt_is_png_b64() {
_b="$1"
case "$_b" in
iVBORw0KGgo*) return 0 ;;
*) return 1 ;;
esac
}
_pt_print_apc() {
if _pt_is_tmux; then
printf '\033Ptmux;\033\033_G'
else
printf '\033_G'
fi
}
_pt_print_apc_st() {
if _pt_is_tmux; then
printf '\033\033\\\033\\'
else
printf '\033\\'
fi
}
_pt_send_kitty() {
_chunk_size=4096
_first=1
printf '%s\n' "$_b64data" | fold -w "$_chunk_size" | \
awk -v sz="$_chunk_size" '
{ buf[NR] = $0 }
END {
for (i = 1; i <= NR; i++) {
if (i < NR) printf "%s\tm=1\t%s\n", (i==1 ? "first" : ""), buf[i]
else printf "%s\tm=0\t%s\n", (i==1 ? "first" : ""), buf[i]
}
}
' | while IFS=' ' read -r _tag _m _chunk; do
if [ "$_tag" = "first" ]; then
_pt_print_apc
printf '%s,%s;%s' "$_kitty_control" "$_m" "$_chunk"
_pt_print_apc_st
else
_pt_print_apc
printf '%s;%s' "$_m" "$_chunk"
_pt_print_apc_st
fi
done
printf '\n'
}
_pt_estimate_rows() {
_img_file="$1" _opt_width="$2" _opt_height="$3" _preserve="$4"
if [ -n "$_opt_height" ]; then
case "$_opt_height" in
*px) ;; *)
printf '%s' "$_opt_height"
return
;;
esac
fi
if [ -z "$_img_file" ] || [ ! -f "$_img_file" ]; then
return
fi
_dims=$(_pt_image_dims "$_img_file")
if [ -z "$_dims" ]; then
return
fi
_img_w=$(printf '%s' "$_dims" | cut -d' ' -f1)
_img_h=$(printf '%s' "$_dims" | cut -d' ' -f2)
if [ -z "$_img_w" ] || [ -z "$_img_h" ] || [ "$_img_h" -le 0 ] 2>/dev/null; then
return
fi
_term_cols=$(tput cols 2>/dev/null || echo 80)
_cell_w=8
_cell_h=16
_term_px_w=$((_term_cols * _cell_w))
if [ -n "$_opt_height" ]; then
_display_h=$(printf '%s' "$_opt_height" | sed 's/px$//')
else
if [ "$_img_w" -gt "$_term_px_w" ]; then
_display_h=$(( _img_h * _term_px_w / _img_w ))
else
_display_h="$_img_h"
fi
fi
_rows=$(( (_display_h + _cell_h - 1) / _cell_h ))
printf '%s' "$_rows"
}
_width=""
_height=""
_preserve_aspect="1"
_file=""
_format="iterm2"
while [ $# -gt 0 ]; do
case "$1" in
--format)
_format="$2"
shift 2
;;
--width)
_width="$2"
shift 2
;;
--height)
_height="$2"
shift 2
;;
--preserve-aspect-ratio)
_preserve_aspect="1"
shift
;;
--no-preserve-aspect-ratio)
_preserve_aspect="0"
shift
;;
--)
shift
_file="${1:--}"
break
;;
-)
_file="-"
shift
break
;;
-*)
echo "Unknown option: $1" >&2
echo "Usage: pt-imgcat [--format iterm2|kitty] [--width N] [--height N] <file|->" >&2
exit 1
;;
*)
_file="$1"
shift
break
;;
esac
done
case "$_format" in
iterm2|kitty) ;;
*)
echo "Error: unknown format '$_format' (use iterm2 or kitty)" >&2
exit 1
;;
esac
_from_stdin=0
if [ -z "$_file" ]; then
if [ -t 0 ]; then
echo "Usage: pt-imgcat [options] <file|->" >&2
echo " cat image.png | pt-imgcat" >&2
exit 1
fi
_from_stdin=1
_file="-"
elif [ "$_file" = "-" ]; then
_from_stdin=1
fi
if [ "$_format" = "kitty" ]; then
_stdin_tmp=""
if [ "$_from_stdin" -eq 1 ]; then
_stdin_tmp=$(mktemp /tmp/pt-imgcat-XXXXXX)
cat > "$_stdin_tmp"
_src_file="$_stdin_tmp"
else
if [ ! -f "$_file" ]; then
echo "Error: '$_file' is not a regular file" >&2
exit 1
fi
if [ ! -r "$_file" ]; then
echo "Error: '$_file' is not readable" >&2
exit 1
fi
_src_file="$_file"
fi
_b64data=$(base64 < "$_src_file" | tr -d '\n')
if ! _pt_is_png_b64 "$_b64data"; then
_converted=""
_conv_tmp=$(mktemp /tmp/pt-imgcat-XXXXXX.png)
if command -v sips >/dev/null 2>&1; then
if sips -s format png "$_src_file" --out "$_conv_tmp" >/dev/null 2>&1; then
_b64data=$(base64 < "$_conv_tmp" | tr -d '\n')
_converted="1"
fi
elif command -v convert >/dev/null 2>&1; then
if convert "$_src_file" "$_conv_tmp" 2>/dev/null; then
_b64data=$(base64 < "$_conv_tmp" | tr -d '\n')
_converted="1"
fi
fi
rm -f "$_conv_tmp"
if [ -z "$_converted" ] && ! _pt_is_png_b64 "$_b64data"; then
[ -n "$_stdin_tmp" ] && rm -f "$_stdin_tmp"
echo "Error: --format kitty requires PNG input (got non-PNG data)" >&2
echo " Install sips (macOS) or ImageMagick for auto-conversion," >&2
echo " or use --format iterm2 for JPEG/GIF/WebP" >&2
exit 1
fi
fi
[ -n "$_stdin_tmp" ] && rm -f "$_stdin_tmp"
_kitty_control="a=T,f=100,q=2"
if [ -n "$_width" ]; then
case "$_width" in
*px)
echo "Error: --format kitty does not support pixel-unit width (use cells)" >&2
exit 1
;;
*) _kitty_control="${_kitty_control},c=${_width}" ;;
esac
fi
if [ -n "$_height" ]; then
case "$_height" in
*px)
echo "Error: --format kitty does not support pixel-unit height (use cells)" >&2
exit 1
;;
*) _kitty_control="${_kitty_control},r=${_height}" ;;
esac
fi
_pt_send_kitty
if _pt_is_tmux; then
_est_rows=$(_pt_estimate_rows "$_file" "$_width" "$_height" "$_preserve_aspect")
if [ -n "$_est_rows" ] && [ "$_est_rows" -gt 0 ] 2>/dev/null; then
_pt_tmux_advance_cursor "$_est_rows"
fi
fi
else
_params="inline=1"
if [ -n "$_width" ]; then
_params="${_params};width=${_width}"
fi
if [ -n "$_height" ]; then
_params="${_params};height=${_height}"
fi
if [ "$_preserve_aspect" = "0" ]; then
_params="${_params};preserveAspectRatio=0"
fi
if [ "$_from_stdin" -eq 1 ]; then
_b64name=$(printf '%s' "image" | base64 | tr -d '\n')
_b64data=$(base64 | tr -d '\n')
_size=$(printf '%s' "$_b64data" | base64 -d 2>/dev/null | wc -c | tr -d ' ')
_pt_print_osc
printf '1337;File=name=%s;size=%s;%s:' "$_b64name" "$_size" "$_params"
printf '%s' "$_b64data"
_pt_print_st
printf '\n'
else
if [ ! -f "$_file" ]; then
echo "Error: '$_file' is not a regular file" >&2
exit 1
fi
if [ ! -r "$_file" ]; then
echo "Error: '$_file' is not readable" >&2
exit 1
fi
_basename=$(basename "$_file")
_b64name=$(printf '%s' "$_basename" | base64 | tr -d '\n')
_b64data=$(base64 < "$_file" | tr -d '\n')
_size=$(printf '%s' "$_b64data" | base64 -d 2>/dev/null | wc -c | tr -d ' ')
_pt_print_osc
printf '1337;File=name=%s;size=%s;%s:' "$_b64name" "$_size" "$_params"
printf '%s' "$_b64data"
_pt_print_st
printf '\n'
if _pt_is_tmux; then
_est_rows=$(_pt_estimate_rows "$_file" "$_width" "$_height" "$_preserve_aspect")
if [ -n "$_est_rows" ] && [ "$_est_rows" -gt 0 ] 2>/dev/null; then
_pt_tmux_advance_cursor "$_est_rows"
fi
fi
fi
fi