sent-tools

A small toolkit of shell scripts for compiling suckless sent slideshows.
git clone https://scm.kuandu.systems/git-raw/sent-tools.git
Log | Files | Refs | README

compile-sentspec.sh (10031B)


      1 #!/bin/sh
      2 #
      3 # compile-sent.sh - Compile a .sentspec meta-format file into a sent-compatible
      4 # slide deck, supporting download/local images and source bars.
      5 #
      6 
      7 set -eu
      8 
      9 VERBOSE=0
     10 readonly DOWNLOADED_DIR="./images/downloaded"
     11 readonly COMPILED_DIR="./images/compiled"
     12 readonly HASHES_FILE="./images.hashes"
     13 
     14 usage() {
     15   cat << EOF
     16 Usage: $0 [-b bgcolor] [-f fontcolor] [-F fontname] [-W width] [-H height] [-v] [-h] <file.sentspec>
     17 
     18 Compile a .sentspec meta-format file into a sent-compatible slide deck.
     19 
     20 Options:
     21   -b bgcolor   Set background color (default: white)
     22   -f fontcolor Set font color (default: black)
     23   -F fontname  Set font family (default: DejaVu-Sans-Mono)
     24   -W width     Set output width in pixels (default: 1920)
     25   -H height    Set output height in pixels (default: 1080)
     26   -v           Verbose output
     27   -h           Show this help and exit
     28 
     29 Lines starting with 'L|', 'R|', or 'C|' are interpreted as:
     30 
     31   pos|mode|file|url|title|subtitle
     32 
     33   pos      L for image left, R for image right, C for image center
     34   mode     D for download, F for local file
     35   file     Local filename for the image (or to save download as)
     36   url      Image URL (optional, if present adds a source bar)
     37   title    Title text (L|R only)
     38   subtitle Subtitle text (L|R only, optional)
     39 
     40 All other lines are passed through unchanged.
     41 EOF
     42 }
     43 
     44 error() {
     45   printf "Error: %s\n" "$1" >&2
     46   exit 1
     47 }
     48 
     49 status() {
     50   if [ "$VERBOSE" = 1 ]; then
     51     printf "[*] %s\n" "$1" >&2
     52   fi
     53 }
     54 
     55 check_dependencies() {
     56   for cmd in curl convert identify mktemp cut grep awk sha256 stat; do
     57     command -v "$cmd" > /dev/null 2>&1 \
     58       || error "Required command '$cmd' not found."
     59   done
     60 }
     61 
     62 check_font() {
     63   font="$1"
     64   if ! convert -list font | grep -qi "^ *Font: *$font\$"; then
     65     status "Font '$font' not found in ImageMagick. Using anyway, but rendering may fail."
     66   fi
     67 }
     68 
     69 download_image() {
     70   url="$1"
     71   filename="$2"
     72   status "Downloading $url"
     73   curl -sSL "$url" -o "$filename" || error "Failed to download $url"
     74 }
     75 
     76 scale_image() {
     77   input="$1"
     78   output="$2"
     79   max_width="$3"
     80   max_height="$4"
     81 
     82   img_w=$(identify -format "%w" "$input")
     83   img_h=$(identify -format "%h" "$input")
     84 
     85   scale_w=$(awk -v mw="$max_width" -v iw="$img_w" 'BEGIN {print mw/iw}')
     86   scale_h=$(awk -v mh="$max_height" -v ih="$img_h" 'BEGIN {print mh/ih}')
     87   scale=$(awk -v sw="$scale_w" -v sh="$scale_h" \
     88     'BEGIN {if (sw < sh) print sw; else print sh}')
     89   new_w=$(awk -v iw="$img_w" -v s="$scale" 'BEGIN {printf "%d", iw*s}')
     90   new_h=$(awk -v ih="$img_h" -v s="$scale" 'BEGIN {printf "%d", ih*s}')
     91 
     92   convert "$input" -background white -flatten -resize "${new_w}x${new_h}" \
     93     "$output"
     94 }
     95 
     96 combine_image_text() {
     97   pos="$1"
     98   imgfile="$2"
     99   outfile="$3"
    100   title="$4"
    101   subtitle="$5"
    102   screen_w="$6"
    103   screen_h="$7"
    104   bgcolor="$8"
    105   fontcolor="$9"
    106   font="${10}"
    107   tmpdir="${11}"
    108 
    109   half_w=$((screen_w / 2))
    110   title_pt=68
    111   subtitle_pt=52
    112   title_y=$((-1 * (screen_h / 10)))
    113   subtitle_y=$((title_y + 200))
    114 
    115   text_img=$(mktemp "$tmpdir/textimg.XXXXXX.png")
    116 
    117   if [ -n "$subtitle" ]; then
    118     convert -size "${half_w}x${screen_h}" xc:"$bgcolor" \
    119       -font "$font" -pointsize $title_pt -fill "$fontcolor" \
    120       -gravity Center -annotate +0+$title_y "$title" \
    121       -font "$font" -pointsize $subtitle_pt \
    122       -gravity Center -annotate +0+$subtitle_y "$subtitle" \
    123       "$text_img"
    124   else
    125     convert -size "${half_w}x${screen_h}" xc:"$bgcolor" \
    126       -font "$font" -pointsize $title_pt -fill "$fontcolor" \
    127       -gravity North -annotate +0+$title_y "$title" \
    128       "$text_img"
    129   fi
    130   cp "$text_img" test.jpg
    131 
    132   canvas=$(mktemp "$tmpdir/canvas.XXXXXX.png")
    133   convert -size "${screen_w}x${screen_h}" xc:"$bgcolor" \
    134     -colorspace sRGB "$canvas"
    135   convert "$canvas" -fill "#00ff00" -draw "point 0,0" "$canvas"
    136 
    137   img_w=$(identify -format "%w" "$imgfile")
    138   half_w=$((screen_w / 2))
    139 
    140   if [ "$img_w" -lt "$half_w" ]; then
    141     offset=$((half_w - img_w))
    142   else
    143     offset=0
    144   fi
    145 
    146   if [ "$pos" = "L" ]; then
    147     convert "$canvas" \
    148       \( "$imgfile" -geometry +${offset}+0 -gravity west \) -composite \
    149       \( "$text_img" -gravity east -geometry +0+0 \) -composite \
    150       "$outfile"
    151   else
    152     convert "$canvas" \
    153       \( "$text_img" -gravity west -geometry +0+0 \) -composite \
    154       \( "$imgfile" -geometry +${offset}+0 -gravity east \) -composite \
    155       "$outfile"
    156   fi
    157 }
    158 
    159 add_source_bar() {
    160   input_img="$1"
    161   output_img="$2"
    162   url="$3"
    163   date="$4"
    164 	source_bar_font="$5"
    165   source_bar_font_size="$6"
    166   source_bar_height="$7"
    167 
    168   img_w=$(identify -format "%w" "$input_img")
    169   source_text="Source: $url, $date"
    170   source_text_escaped=$(printf '%s' "$source_text" | sed 's/%/%%/g')
    171 
    172   bar_img=$(mktemp "$TMPDIR/bar.XXXXXX.png")
    173   convert -size "${img_w}x${source_bar_height}" xc:"rgba(255,255,255,0.8)" \
    174     -font "$source_bar_font" -pointsize "$source_bar_font_size" -fill "black" \
    175     -gravity northwest -annotate +10+0 "$source_text_escaped" \
    176     "$bar_img"
    177 
    178   convert "$input_img" "$bar_img" -gravity south -composite "$output_img"
    179 }
    180 
    181 compute_slide_hash() {
    182   imgfile="${1:-}"
    183   line="${2:-}"
    184   font="${3:-}"
    185   fontcolor="${4:-}"
    186   bgcolor="${5:-}"
    187   {
    188     sha256 < "${imgfile:-/dev/null}"
    189     printf "%s\n%s\n%s\n%s\n" "$line" "$font" "$fontcolor" "$bgcolor"
    190   } | sha256 | awk '{print $1}'
    191 }
    192 
    193 get_stored_hash() {
    194   [ -f "$HASHES_FILE" ] || return 1
    195   grep -F -- "$1 " "$HASHES_FILE" 2> /dev/null | awk '{print $2}'
    196 }
    197 
    198 update_hash() {
    199   tmpf=$(mktemp "$TMPDIR/hashupdate.XXXXXX")
    200   if [ -f "$HASHES_FILE" ]; then
    201     grep -v -F -- "$1 " "$HASHES_FILE" > "$tmpf" || true
    202   fi
    203   printf "%s %s\n" "$1" "$2" >> "$tmpf"
    204   mv "$tmpf" "$HASHES_FILE"
    205 }
    206 
    207 cleanup() {
    208   [ -n "${TMPDIR:-}" ] && [ -d "$TMPDIR" ] && rm -rf "$TMPDIR"
    209 }
    210 
    211 main() {
    212   bgcolor="white"
    213   fontcolor="black"
    214   font="DejaVu-Sans-Mono"
    215   screen_w=1920
    216   screen_h=1080
    217   source_bar_font="DejaVu-Sans-Condensed"
    218   source_bar_font_size=12
    219   source_bar_height=18
    220 
    221   while getopts "b:f:F:W:H:vh" opt; do
    222     case "$opt" in
    223       b) bgcolor="$OPTARG" ;;
    224       f) fontcolor="$OPTARG" ;;
    225       F) font="$OPTARG" ;;
    226       W) screen_w="$OPTARG" ;;
    227       H) screen_h="$OPTARG" ;;
    228       v) VERBOSE=1 ;;
    229       h)
    230         usage
    231         exit 0
    232         ;;
    233       *)
    234         usage
    235         exit 1
    236         ;;
    237     esac
    238   done
    239   shift $((OPTIND - 1))
    240 
    241   if [ $# -ne 1 ]; then
    242     usage
    243     exit 1
    244   fi
    245 
    246   input_file="$1"
    247   [ -f "$input_file" ] || error "Input file '$input_file' not found"
    248 
    249   check_dependencies
    250   check_font "$font"
    251 
    252 
    253   status "Using background color: $bgcolor"
    254   status "Using font color: $fontcolor"
    255   status "Using font: $font"
    256   status "Output size: ${screen_w}x${screen_h}"
    257 
    258   TMPDIR=$(mktemp -d "/tmp/compile-sent.XXXXXX")
    259   readonly TMPDIR
    260   trap cleanup EXIT INT TERM
    261 
    262   for d in "$DOWNLOADED_DIR" "$COMPILED_DIR"; do
    263     [ -d "$d" ] || mkdir -p "$d"
    264   done
    265 
    266   if [ ! -f "$HASHES_FILE" ]; then
    267     : > "$HASHES_FILE"
    268   fi
    269 
    270   while IFS= read -r line || [ -n "$line" ]; do
    271     case "$line" in
    272       L\|* | R\|* | C\|*)
    273         pos=$(printf '%s\n' "$line" | cut -d'|' -f1)
    274         mode=$(printf '%s\n' "$line" | cut -d'|' -f2)
    275         filename=$(printf '%s\n' "$line" | cut -d'|' -f3)
    276         url=$(printf '%s\n' "$line" | cut -d'|' -f4)
    277         title=""
    278         subtitle=""
    279         if [ "$pos" = "L" ] || [ "$pos" = "R" ]; then
    280           title=$(printf '%s\n' "$line" | cut -d'|' -f5)
    281           subtitle=$(printf '%s\n' "$line" | cut -d'|' -f6-)
    282         fi
    283 
    284         if [ -z "$filename" ]; then
    285           status "Malformed meta line, skipping: $line"
    286           continue
    287         fi
    288 
    289         if [ "$mode" = "F" ]; then
    290           imgfile="$filename"
    291           basefile=$(basename "$filename")
    292           if [ ! -f "$imgfile" ]; then
    293             status "Local image not found: $filename"
    294             continue
    295           fi
    296         elif [ "$mode" = "D" ]; then
    297           basefile=$(basename "$filename")
    298           imgfile="$DOWNLOADED_DIR/$basefile"
    299           if [ -f "$imgfile" ]; then
    300 						#status "Already downloaded: $imgfile"
    301             :
    302 					else
    303             if [ -z "$url" ]; then
    304               status "No URL for download mode, skipping: $line"
    305               continue
    306             fi
    307             download_image "$url" "$imgfile"
    308 						
    309           fi
    310         else
    311           status "Unknown mode: $mode"
    312           continue
    313         fi
    314 
    315         outimg="$COMPILED_DIR/${basefile%.*}.jpg"
    316 
    317         slide_hash=$(compute_slide_hash "$imgfile" "$line" "$font" "$fontcolor" "$bgcolor")
    318         stored_hash=$(get_stored_hash "$outimg" || true)
    319 
    320         if [ "$slide_hash" = "$stored_hash" ] && [ -f "$outimg" ]; then
    321           status "Slide unchanged, skipping processing."
    322           printf "@%s\n" "$outimg"
    323           continue
    324         fi
    325 
    326         status "Compiling: $outimg"
    327 
    328         scaled_img="$imgfile"
    329         max_img_w="$screen_w"
    330         max_img_h="$screen_h"
    331         if [ "$pos" = "L" ] || [ "$pos" = "R" ]; then
    332   				max_img_w=$((screen_w / 2))
    333   				max_img_h=$screen_h
    334         fi
    335         scaled_img=$(mktemp "$TMPDIR/scaledimg.XXXXXX.jpg")
    336         scale_image "$imgfile" "$scaled_img" "$max_img_w" "$max_img_h"
    337 
    338         img_with_bar="$scaled_img"
    339         if [ -n "$url" ]; then
    340           if stat -f %B "$imgfile" 2> /dev/null | grep -qv '^0$'; then
    341             date=$(stat -f "%SB" -t "%Y-%m-%d %H:%M:%S" "$imgfile")
    342           else
    343             date=$(stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$imgfile")
    344           fi
    345           img_with_bar=$(mktemp "$TMPDIR/withbar.XXXXXX.jpg")
    346           add_source_bar "$scaled_img" "$img_with_bar" "$url" "$date" \
    347 						"$source_bar_font" "$source_bar_font_size"  "$source_bar_height"
    348         fi
    349 
    350         if [ "$pos" = "L" ] || [ "$pos" = "R" ]; then
    351           combine_image_text "$pos" "$img_with_bar" "$outimg" "$title" "$subtitle" \
    352             "$screen_w" "$screen_h" "$bgcolor" "$fontcolor" "$font" "$TMPDIR"
    353         else
    354           cp "$img_with_bar" "$outimg"
    355         fi
    356 
    357         printf "@%s\n" "$outimg"
    358         update_hash "$outimg" "$slide_hash"
    359         ;;
    360       *)
    361         printf "%s\n" "$line"
    362         ;;
    363     esac
    364   done < "$input_file"
    365 }
    366 
    367 main "$@"