├── .github └── workflows │ ├── shellcheck.yml │ └── test.yml ├── README.md └── shrinkpdf.sh /.github/workflows/shellcheck.yml: -------------------------------------------------------------------------------- 1 | name: Shellcheck 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | Shellcheck: 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | # Checkout the code. 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | 14 | # Install the dependencies. 15 | - name: Install dependencies 16 | run: | 17 | sudo apt update 18 | sudo apt install shellcheck 19 | 20 | # Run Shellcheck. 21 | - name: Run Shellcheck 22 | run: shellcheck shrinkpdf.sh 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | Test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | # Checkout the code. 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | 14 | # Install the test dependencies. 15 | - name: Install dependencies 16 | run: | 17 | sudo apt-get update 18 | sudo apt install ghostscript 19 | sudo apt install librsvg2-bin 20 | sudo apt install texlive-latex-recommended 21 | sudo apt install texlive-latex-extra 22 | sudo apt install pandoc 23 | 24 | # Generate a test PDF by using the README as a convenient source. 25 | - name: Generate test PDF 26 | run: pandoc -s -o orig.pdf README.md 27 | 28 | # Shrink the PDF. 29 | - name: Shrink the PDF 30 | run: | 31 | chmod +x shrinkpdf.sh 32 | ./shrinkpdf.sh -g -r 72 -o new.pdf orig.pdf 33 | ls -al orig.pdf new.pdf 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shrinkpdf: shrink PDF files with Ghostscript 2 | 3 | [![Shellcheck](https://github.com/aklomp/shrinkpdf/actions/workflows/shellcheck.yml/badge.svg)](https://github.com/aklomp/shrinkpdf/actions/workflows/shellcheck.yml) 4 | [![Test](https://github.com/aklomp/shrinkpdf/actions/workflows/test.yml/badge.svg)](https://github.com/aklomp/shrinkpdf/actions/workflows/test.yml) 5 | 6 | A simple wrapper around Ghostscript to shrink PDFs (as in reduce filesize) 7 | under Linux. Inspired by some code I found in an OpenOffice Python script (I 8 | think). The script feeds a PDF through Ghostscript, which performs lossy 9 | recompression by such methods as downsampling the images to 72dpi. The result 10 | should be (but not always is) a much smaller file. 11 | 12 | ## Usage 13 | 14 | Download the script and make it executable: 15 | 16 | ```sh 17 | chmod +x shrinkpdf.sh 18 | ``` 19 | 20 | If you run it with no arguments, it prints a usage summary. If you run it with a 21 | single argument -- the name of the pdf to shrink -- it writes the result to 22 | `stdout`: 23 | 24 | ```sh 25 | ./shrinkpdf.sh in.pdf > out.pdf 26 | ``` 27 | 28 | You can provide an output file with the `-o` option: 29 | 30 | ```sh 31 | ./shrinkpdf.sh -o out.pdf in.pdf 32 | ``` 33 | 34 | And an output resolution in DPI (default is 72 DPI) with the `-r` option: 35 | 36 | ```sh 37 | ./shrinkpdf.sh -r 90 -o out.pdf in.pdf 38 | ``` 39 | 40 | Color-to-grayscale conversion can be enabled with the `-g` flag. This can 41 | sometimes further reduce the output size: 42 | 43 | ``` 44 | ./shrinkpdf.sh -g -r 90 -o out.pdf in.pdf 45 | ``` 46 | 47 | Set the threshold at which an image would be downsampled with the `-t` flag. 48 | The default of 1.5 means that images which are already less than 1.5x the 49 | desired dpi will not be resized. (Using `-r 300` and `-t 1.5` would not resize 50 | images unless they were > 300 * 1.5 dpi, or 450 dpi.) Use lower numbers for 51 | less leniency and higher numbers for more leniency. 52 | ```sh 53 | ./shrinkpdf.sh -r 300 -t 1.1 -o out.pdf in.pdf 54 | ``` 55 | 56 | Due to limitations of shell option handling, options must always come before 57 | the input file. 58 | 59 | If both the input and the output are regular files, the script checks if the 60 | output is actually smaller. If not, it writes a message to `stderr` and copies 61 | the input over the output. 62 | 63 | Sorry, Windows users; this one is Linux only. A Windows adaptation of this 64 | script can be found [on this blog](http://dcm684.us/wp/2013/10/pdf-shrink/). 65 | It's a bit more user-friendly than my barebones version and also supports 66 | drag-and-drop. 67 | 68 | ## License and acknowledgements 69 | 70 | The script is licensed under the 71 | [BSD 3-clause](http://opensource.org/licenses/BSD-3-Clause) license. 72 | 73 | I didn't invent the wheel, just packaged it nicely. All credits go to the 74 | [Ghostscript](http://www.ghostscript.com) team. 75 | -------------------------------------------------------------------------------- /shrinkpdf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # https://github.com/aklomp/shrinkpdf 4 | # Licensed under the 3-clause BSD license: 5 | # 6 | # Copyright (c) 2014-2025, Alfred Klomp 7 | # All rights reserved. 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 1. Redistributions of source code must retain the above copyright notice, 12 | # this list of conditions and the following disclaimer. 13 | # 2. Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # 3. Neither the name of the copyright holder nor the names of its contributors 17 | # may be used to endorse or promote products derived from this software 18 | # without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | # POSSIBILITY OF SUCH DAMAGE. 31 | 32 | 33 | shrink () 34 | { 35 | if [ "$grayscale" = "YES" ]; then 36 | gray_params="-sProcessColorModel=DeviceGray \ 37 | -sColorConversionStrategy=Gray \ 38 | -dOverrideICC" 39 | else 40 | gray_params="" 41 | fi 42 | 43 | # Allow unquoted variables; we want word splitting for $gray_params. 44 | # shellcheck disable=SC2086 45 | gs \ 46 | -q -dNOPAUSE -dBATCH -dSAFER \ 47 | -sDEVICE=pdfwrite \ 48 | -dCompatibilityLevel="$4" \ 49 | -dPDFSETTINGS=/screen \ 50 | -dEmbedAllFonts=true \ 51 | -dSubsetFonts=true \ 52 | -dAutoRotatePages=/None \ 53 | -dColorImageDownsampleType=/Bicubic \ 54 | -dColorImageResolution="$3" \ 55 | -dColorImageDownsampleThreshold="$5" \ 56 | -dGrayImageDownsampleType=/Bicubic \ 57 | -dGrayImageResolution="$3" \ 58 | -dGrayImageDownsampleThreshold="$5" \ 59 | -dMonoImageDownsampleType=/Subsample \ 60 | -dMonoImageResolution="$3" \ 61 | -dMonoImageDownsampleThreshold="$5" \ 62 | -dPreserveAnnots=false \ 63 | -sOutputFile="$2" \ 64 | ${gray_params} \ 65 | "$1" 66 | } 67 | 68 | get_pdf_version () 69 | { 70 | # $1 is the input file. The PDF version is contained in the 71 | # first 1024 bytes and will be extracted from the PDF file. 72 | pdf_version=$(head -c 1024 "$1" | LC_ALL=C awk 'BEGIN { found=0 }{ if (match($0, "%PDF-[0-9]\\.[0-9]") && ! found) { print substr($0, RSTART + 5, 3); found=1 } }') 73 | if [ -z "$pdf_version" ] || [ "${#pdf_version}" != "3" ]; then 74 | return 1 75 | fi 76 | } 77 | 78 | check_input_file () 79 | { 80 | # Check if the given file exists. 81 | if [ ! -f "$1" ]; then 82 | echo "Error: Input file does not exist." >&2 83 | return 1 84 | fi 85 | } 86 | 87 | check_smaller () 88 | { 89 | # If $1 and $2 are regular files, we can compare file sizes to 90 | # see if we succeeded in shrinking. If not, we copy $1 over $2: 91 | if [ ! -f "$1" ] || [ ! -f "$2" ]; then 92 | return 0; 93 | fi 94 | ISIZE="$(wc -c "$1" | awk '{ print $1 }')" 95 | OSIZE="$(wc -c "$2" | awk '{ print $1 }')" 96 | if [ "$ISIZE" -lt "$OSIZE" ]; then 97 | echo "Input smaller than output, doing straight copy" >&2 98 | cp "$1" "$2" 99 | fi 100 | } 101 | 102 | check_overwrite () 103 | { 104 | # If $1 and $2 refer to the same file, then the file would get 105 | # truncated to zero, which is unexpected. Abort the operation. 106 | # Unfortunately the stronger `-ef` test is not in POSIX. 107 | if [ "$1" = "$2" ]; then 108 | echo "The output file is the same as the input file. This would truncate the file." >&2 109 | echo "Use a temporary file as an intermediate step." >&2 110 | return 1 111 | fi 112 | } 113 | 114 | usage () 115 | { 116 | echo "Reduces PDF filesize by lossy recompressing with Ghostscript." 117 | echo "Not guaranteed to succeed, but usually works." 118 | echo 119 | echo "Usage: $1 [-g] [-h] [-o output] [-r res] [-t threshold] infile" 120 | echo 121 | echo "Options:" 122 | echo " -g Enable grayscale conversion which can further reduce output size." 123 | echo " -h Show this help text." 124 | echo " -o Output file, default is standard output." 125 | echo " -r Resolution in DPI, default is 72." 126 | echo " -t Threshold multiplier for an image to qualify for downsampling, default is 1.5" 127 | } 128 | 129 | # Set default option values. 130 | grayscale="" 131 | ofile="-" 132 | res="72" 133 | threshold="1.5" 134 | 135 | # Parse command line options. 136 | while getopts ':hgo:r:t:' flag; do 137 | case $flag in 138 | h) 139 | usage "$0" 140 | exit 0 141 | ;; 142 | g) 143 | grayscale="YES" 144 | ;; 145 | o) 146 | ofile="${OPTARG}" 147 | ;; 148 | r) 149 | res="${OPTARG}" 150 | ;; 151 | t) 152 | threshold="${OPTARG}" 153 | ;; 154 | \?) 155 | echo "invalid option (use -h for help)" 156 | exit 1 157 | ;; 158 | esac 159 | done 160 | shift $((OPTIND - 1)) 161 | 162 | # An input file is required. 163 | if [ -z "$1" ]; then 164 | usage "$0" 165 | exit 1 166 | else 167 | ifile="$1" 168 | fi 169 | 170 | # Check if input file exists 171 | check_input_file "$ifile" || exit $? 172 | 173 | # Check that the output file is not the same as the input file. 174 | check_overwrite "$ifile" "$ofile" || exit $? 175 | 176 | # Get the PDF version of the input file. 177 | get_pdf_version "$ifile" || pdf_version="1.5" 178 | 179 | # Shrink the PDF. 180 | shrink "$ifile" "$ofile" "$res" "$pdf_version" "$threshold" || exit $? 181 | 182 | # Check that the output is actually smaller. 183 | check_smaller "$ifile" "$ofile" 184 | --------------------------------------------------------------------------------