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 "$@"