From a3daba20365b990630e72db5d316b1b0cc87fa51 Mon Sep 17 00:00:00 2001 From: Alexander Wuerstlein Date: Sun, 29 Jun 2025 17:24:14 +0200 Subject: [PATCH 1/2] Add inplace option -i to replace the input Add an option -i to replace the input file with the shrunk output (if smaller). This creates a temporary directory in a secure and portable manner, cleaning it up afterwards. The temporary directory is created using 'mktemp' if present, otherwise the less secure method of including the PID, current time and possibly (if available) $RANDOM is used. The directory is removed upon exit, but carefully: 'rm -fr' is only executed on a then-empty directory after output.pdf has been removed. This could leak directories, but I consider that less of a problem than possibly removing something important. --- shrinkpdf.sh | 86 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 78 insertions(+), 8 deletions(-) diff --git a/shrinkpdf.sh b/shrinkpdf.sh index add6690..bdd9137 100755 --- a/shrinkpdf.sh +++ b/shrinkpdf.sh @@ -29,6 +29,40 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. +create_tempdir () +{ + if command -v mktemp >/dev/null 2>&1; then + # Possibly unportable, but far more secure than the fallback below + mktemp -d + else + # Fallback method + temp_base="${TMPDIR:-/tmp}" + # $RANDOM might be undefined, but including a possibly empty string doesn't hurt + # shellcheck disable=SC3028 + temp_dir="$temp_base/shrinkpdf.sh.$$.$RANDOM.$(date +%s)" + + if mkdir "$temp_dir" 2>/dev/null; then + chmod 700 "$temp_dir" + echo "$temp_dir" + else + echo "could not create temporary directory $temp_dir." >&2 + exit 1 + fi + fi +} + +cleanup_tempdir () +{ + if [ -n "$1" ] ; then + rm -f "$1/output.pdf" + # ShellCheck complains about ls and weird filenames, but we only care about the number of newlines + # shellcheck disable=SC2012 + if [ "$( ls -A "$1" 2>/dev/null | wc -l )" -eq 0 ] ; then + # only rm -fr if the directory is empty to avoid recursive deletion catastrophe bugs + rm -fr "$1" + fi + fi +} shrink () { @@ -86,15 +120,17 @@ check_input_file () check_smaller () { # If $1 and $2 are regular files, we can compare file sizes to - # see if we succeeded in shrinking. If not, we copy $1 over $2: + # see if we succeeded in shrinking. if [ ! -f "$1" ] || [ ! -f "$2" ]; then - return 0; + echo 0; + return; fi ISIZE="$(wc -c "$1" | awk '{ print $1 }')" OSIZE="$(wc -c "$2" | awk '{ print $1 }')" if [ "$ISIZE" -lt "$OSIZE" ]; then - echo "Input smaller than output, doing straight copy" >&2 - cp "$1" "$2" + echo 1; + else + echo 0; fi } @@ -105,7 +141,7 @@ check_overwrite () # Unfortunately the stronger `-ef` test is not in POSIX. if [ "$1" = "$2" ]; then echo "The output file is the same as the input file. This would truncate the file." >&2 - echo "Use a temporary file as an intermediate step." >&2 + echo "Use a temporary file as an intermediate step, or use the -i flag." >&2 return 1 fi } @@ -123,6 +159,7 @@ usage () echo " -o Output file, default is standard output." echo " -r Resolution in DPI, default is 72." echo " -t Threshold multiplier for an image to qualify for downsampling, default is 1.5" + echo " -i Inplace operation: Overwrite the input file with the output if smaller. Use with caution." } # Set default option values. @@ -130,9 +167,10 @@ grayscale="" ofile="-" res="72" threshold="1.5" +inplace=0 # Parse command line options. -while getopts ':hgo:r:t:' flag; do +while getopts ':hgo:ir:t:' flag; do case $flag in h) usage "$0" @@ -142,7 +180,25 @@ while getopts ':hgo:r:t:' flag; do grayscale="YES" ;; o) - ofile="${OPTARG}" + if [ "-" = "$ofile" ] ; then + ofile="${OPTARG}" + else + echo "invalid combination of options -o and -i." + exit 1 + fi + ;; + i) + if [ "-" = "$ofile" ] ; then + temp_dir=$(create_tempdir) + # I actually want this string expanded right here. + # shellcheck disable=SC2064 + trap "cleanup_tempdir \"$temp_dir\"" EXIT INT TERM + ofile="$temp_dir/output.pdf" + inplace=1 + else + echo "invalid combination of options -o and -i." + exit 1 + fi ;; r) res="${OPTARG}" @@ -179,4 +235,18 @@ get_pdf_version "$ifile" || pdf_version="1.5" shrink "$ifile" "$ofile" "$res" "$pdf_version" "$threshold" || exit $? # Check that the output is actually smaller. -check_smaller "$ifile" "$ofile" +if [ 1 -eq "$(check_smaller "$ifile" "$ofile")" ] ; then + # file got bigger, just overwrite with the smaller original + echo "Input smaller than output, doing straight copy" >&2 + cp "$ifile" "$ofile" +else + # file has shrunk + if [ 1 -eq "$inplace" ] ; then + # user requested inplace operation, so overwrite the original + # input file with the temporary and smaller output + cp "$ofile" "$ifile" + + # if not inplace: + # noting to do, output has already been written to the right place + fi +fi From 15dec9a8ec0431a1325cd641691273eff7d2116b Mon Sep 17 00:00:00 2001 From: Alexander Wuerstlein Date: Sun, 29 Jun 2025 18:21:00 +0200 Subject: [PATCH 2/2] add test for inplace shrinking --- .github/workflows/test.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bc2dca4..30172d7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,3 +31,12 @@ jobs: chmod +x shrinkpdf.sh ./shrinkpdf.sh -g -r 72 -o new.pdf orig.pdf ls -al orig.pdf new.pdf + + # Shrink the PDF in place. + - name: Shrink the PDF in place + run: | + chmod +x shrinkpdf.sh + cp orig.pdf inplace.pdf + ls -al inplace.pdf + ./shrinkpdf.sh -g -r 72 -i inplace.pdf + ls -al inplace.pdf orig.pdf new.pdf