#!/bin/bash # vi: et sts=4 sw=4 ts=4 USAGE() { printf 'Usage: %s [OPTIONS] [--] FILE...\n' \ "${0##*/}" } HELP_MESSAGE() { USAGE cat <&2 USAGE >&2 exit 2 ;; esac else FILES+=("$ARG") fi done TEMP_DIR= if [[ ${#FILES[@]} -eq 0 ]]; then USAGE >&2 exit 2 fi check_required_binaries() { local BIN MISSING=() for BIN; do if ! type -t "$BIN" &>/dev/null; then MISSING+=("$BIN") fi done if [[ ${#MISSING[@]} -gt 0 ]]; then printf 'Error: You are missing required programs:\n' >&2 for BIN in "${MISSING[@]}"; do printf -- '- %s\n' "$BIN" >&2 done exit 2 fi } file_size() { stat \ --format='%s' \ --dereference \ -- \ "$@" \ 2>/dev/null } # produces a human-readable size from the byte count passed to it hr_size() ( numfmt \ --to=iec \ --suffix=B \ "$@" ) setup_tempdir() { if [[ -z $TEMP_DIR ]]; then TEMP_DIR=$(mktemp -d -t "${0##*/}.XXXXXX") cleanup() { rm -fr -- "$TEMP_DIR" } trap 'cleanup' EXIT fi } # copies $2 over to $1 if $2 is smaller than $1 use_smaller() { # if `$TEMP' isn't empty and it's of a smaller size than `$FILE', # preserve every attribute and replace `$FILE' with `$TEMP' local \ FILE=$1 \ TEMP=$2 \ ORIGSIZE \ TEMPSIZE \ MV_ARGS=('-v') if [[ $FORCE_OVERWRITE -eq 0 ]]; then MV_ARGS+=('-i') fi ORIGSIZE=$(file_size "$FILE") TEMPSIZE=$(file_size "$TEMP") if [[ -f $TEMP && $TEMPSIZE -gt 0 && $TEMPSIZE -lt $ORIGSIZE ]]; then # Preserve attributes by copying them from the original file to the # temporary one chmod \ --reference="$FILE" \ -- \ "$TEMP" && if [[ $PRESERVE_TIMESTAMP -ne 0 ]]; then touch \ --reference="$FILE" \ -- \ "$TEMP" fi && if [[ $UID -eq 0 ]]; then # We are root, so we can chown(1) things chown \ --reference="$FILE" \ -- \ "$TEMP" fi && if [[ -n $KEEP_BACKUP_SUFFIX ]]; then mv "${MV_ARGS[@]}" -- "$FILE" "$FILE$KEEP_BACKUP_SUFFIX" fi && cp \ --preserve=mode,ownership,timestamps \ -- \ "$TEMP" \ "$FILE" if [[ $? -ne 0 ]]; then printf 'Failed to optimize "%s"!\n' \ "$FILE" \ >&2 fi fi # Protect against unsuccessful following file writes to our TEMP file rm -f -- "$TEMP" } check_required_binaries \ qpdf QPDF_ARGS=( --compression-level=9 --deterministic-id --flatten-rotation --object-streams=generate --recompress-flate --stream-data=compress ) if [[ $LOSSY -ne 0 ]]; then QPDF_ARGS+=( --optimize-images ) fi if [[ $ENCODE_THRU_WARNINGS -ne 0 ]]; then QPDF_ARGS+=( --warning-exit-0 ) fi ERRORS=0 FREED_TOTAL=0 for FILE in "${FILES[@]}"; do setup_tempdir TEMP=$(mktemp -p "$TEMP_DIR" 'file.XXXXXX') rm -f -- "$TEMP" BEGIN_FILESIZE=$(file_size "$FILE") if qpdf "${QPDF_ARGS[@]}" \ -- \ "$FILE" "$TEMP" && use_smaller "$FILE" "$TEMP"; then END_FILESIZE=$(file_size "$FILE") FREED=$(( BEGIN_FILESIZE - END_FILESIZE )) FREED_HR=$(hr_size "$FREED") (( FREED_TOTAL += FREED )) printf '%s: freed %d bytes (%s)\n' \ "$FILE" \ "$FREED" \ "$FREED_HR" else (( ++ERRORS )) fi done FREED_TOTAL_HR=$(hr_size "$FREED_TOTAL") printf 'all: freed %d bytes (%s)\n' "$FREED_TOTAL" "$FREED_TOTAL_HR" if [[ $ERRORS -gt 0 ]]; then exit 1 fi