講談社ブルーバックスの『Q&A 火山噴火』pdfファイルが章ごとに分かれている上にファイルサイズが大きくて困ったのでgsとpdftkで処理した

御嶽山噴火によって、火山活動への関心が高まっています。
それに伴って、講談社の公式サイトではブルーバックス『Q&A 火山噴火』がpdfで配布されています。


しかし、困ったことに公開されているファイルは

  • 一つのファイルに結合されておらず
  • ファイルサイズが大きすぎる

という問題があります。
含まれている画像ファイルが適切に圧縮されていないようです。


pdfで配布されており、正しい知識を知るための素晴らしい資料なのですが、
ファイルサイズがあまりに大きい上に章ごとに分かれていて、少し扱いづらいです。
今回は、大きなpdfファイルを圧縮して、結合する方法を見ていきましょう。


まず、ファイルをダウンロードしてみます。

files="contents.pdf $(seq -f "chapter%02g.pdf" -s " " 9)"
url=http://bluebacks.kodansha.co.jp/content/bsupport/images/kazan/
for name in $files; do
  curl -OR "$url$name"
done

サイズを確認してみます。

 $ for f in *.pdf; du -k $f
34292	chapter01.pdf
10172	chapter02.pdf
3624	chapter03.pdf
24096	chapter04.pdf
28056	chapter05.pdf
5680	chapter06.pdf
26368	chapter07.pdf
9624	chapter08.pdf
8672	chapter09.pdf
640	contents.pdf

一番大きいのはchapter01.pdf、なんと34MBもあります。
このファイルは17ページしかないので、1ページ平均2MBもあることになります。


ファイルの大きさとか気にせずに、とりあえず結合してみます。

 $ pdftk contents.pdf chapter0{1..9}.pdf cat output kazan.pdf
 $ du -k kazan.pdf
151200	kazan.pdf

150MBもある、大きなpdfが出来ました。
Previewで一応開くことはできましたが、スクロールをしていると応答がなくなりました。


pdfファイルを圧縮する方法はいくつかあります。
例えば、convertコマンドを使う方法や、あるいは何らかのpdfビュワーでpdfに印刷する方法などです。
いろいろ試しましたが、結局gsコマンドを使う方法が一番良いようです。
個人的に、一番良い出力(ファイルサイズが小さくて画像が極度にぼやけない)を得られたオプションは、このような感じでした。

gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=/ebook -dNOPAUSE -dBATCH -dQUIET -sOutputFile=output.pdf input.pdf

説明を書くと、次のようになります。

gs -sDEVICE=pdfwrite \        # 出力デバイスをpdfに
   -dCompatibilityLevel=1.4 \ # Actobat Reader 5以上でサポートされている
   -dPDFSETTINGS=/ebook \     # 画像を圧縮してファイルサイズを小さく
   -dNOPAUSE \                # ページごとに中断しないように
   -dBATCH \                  # gsのインタラクティブモードは使わない
   -dQUIET \                  # ページごとにページ番号を出力しない
   -sOutputFile=output.pdf \  # 出力ファイル
   input.pdf                  # 入力ファイル


ところが、実際に150MBあるkazan.pdfをこの方法で圧縮しようとすると、gsコマンドはSegmentation faultで落ちてしまいました。
どうやらファイルサイズが大きいとダメなようです。
今回の場合、章ごとにファイルが分かれているので、章ごとに圧縮してから結合しようとしましたが、
gsコマンドはchapter01.pdf (34MB) や、chapter07.pdf (26MB) を圧縮しようとした時も落ちてしまいました。


ファイルを分割して、各々を圧縮して、最後に結合するという方法を取れば良さそうです。
もうここまで来ると、一つのコマンドにしたくなりますよね。
というわけで、pdfファイルを圧縮するコマンドを書いてみました。

#!/usr/bin/env bash

# Check if gs is installed
if ! command -v gs > /dev/null 2>&1; then
  echo 'gs is required'
  exit 1
fi

# Check if argument is given
if [ $# -eq 0 ]; then
  echo "No argument (Usage: $(basename -- "$0") filename.pdf)"
  exit 1
fi

# Store the current time
starttime=$(date +%s)

# Source file is $1
sourcefile=./$(basename -- "$1")
dirname=$(dirname -- "$1")
outfile=${sourcefile%.*}-small.pdf

# Save the current path
save_path="$(pwd)"

# Change the working directory
cd -- "$dirname"

# Compressor
compress-pdf() { gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=/ebook -dNOPAUSE -dBATCH -dQUIET -sOutputFile="$2" "$1"; }

# Get size of a file
getsize() { du -k -- "$1" 2>/dev/null | awk '{print $1}'; }

# Echo status
echo-done() {
  sourcefile=$1
  outfile=$2
  sourcesize=$(getsize "$sourcefile")
  outsize=$(getsize "$outfile")
  enddate=$(date +%s)
  runtime="$((enddate-starttime))"
  if [ "$sourcesize" == "" ] || [ "$sourcesize" == "0" ] || [ "$outsize" == "" ] || [ "$outsize" == "0" ]; then
    echo "Compression of $sourcefile done in ${runtime}s."
  elif [ "$sourcesize" -ge "$outsize" ]; then
    echo "Compression of $sourcefile done in ${runtime}s,"\
         "filesize is reduced by $((100-100*outsize/sourcesize))% (from ${sourcesize}K to ${outsize}K)."
  else
    echo "Compression of $sourcefile done in ${runtime}s,"\
         "but filesize increased by $((100*outsize/sourcesize-100))% (from ${sourcesize}K to ${outsize}K)."
  fi
}

echo-fail() {
  sourcefile=$1
  outfile=$2
  enddate=$(date +%s)
  runtime="$((enddate-starttime))"
  echo "Compression of $sourcefile failed in ${runtime}s."
}

# Compress the pdf file
if [ -f "$sourcefile" ]; then
  echo "Compressing $sourcefile to $outfile..."
  sourcesize=$(getsize "$sourcefile")
  compress-pdf "$sourcefile" "$outfile"
  gsstatus=$?
  if [ "$gsstatus" -eq 0 ] && [ -f "$outfile" ]; then
    echo-done "$sourcefile" "$outfile"
  else
    echo-fail "$sourcefile" "$outfile"
    rm -rf -- "$outfile"
    if command -v pdftk > /dev/null 2>&1; then
      echo "Trying bursting..."
      prefix="${sourcefile%.*}-burst"
      eval "rm -rf -- $prefix-* doc_data.txt"
      if pdftk "$sourcefile" burst output "$prefix-%04d.pdf"; then
        while IFS="" read -r -d "" file; do
          burstoutfile=${file%.*}-small.pdf
          echo "Compressing $file to $burstoutfile..."
          compress-pdf "$file" "$burstoutfile"
        done < <(find . -name "$prefix-*.pdf" -print0)
        echo "Concatenating..."
        eval "pdftk $prefix-*-small.pdf cat output $prefix-concat.pdf"
        echo "Re-compression..."
        if ! compress-pdf "$prefix-concat.pdf" "$outfile"; then
          mv -f "$prefix-concat.pdf" "$outfile"
        fi
        eval "rm -rf -- $prefix-* doc_data.txt"
        echo-done "$sourcefile" "$outfile"
      fi
    fi
  fi
else
  echo "File $sourcefile not found."
fi

# Restore the directory
cd -- "$save_path"

# Next pdf
if [ $# -ge 2 ]; then
  shift
  $0 "$@"
fi

gsが落ちた場合は、pdfファイルを1ページごとに分割します。
そして各々を圧縮してから結合し、最後に再度圧縮をかけます。
本当は「いい具合のファイルサイズ」に分割したかったのですが、それを一般にやるには面倒だと思ったので1ページごとに分割してしまいました。


上のコマンドを適当にcompresspdfとかいう名前をつけて、150MBもあるkazan.pdfを圧縮してみるとこんな感じなりました。

 $ compresspdf kazan.pdf
Compressing kazan.pdf to kazan-small.pdf...
/path/to/compresspdf: line 30: 48589 Segmentation fault: 11  gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=/ebook -dNOPAUSE -dBATCH -dQUIET -sOutputFile="$2" "$1"
Compression of kazan.pdf failed in 69s.
Trying bursting...
Compressing ./kazan-burst-0001.pdf to ./kazan-burst-0001-small.pdf...
Compressing ./kazan-burst-0002.pdf to ./kazan-burst-0002-small.pdf...
Compressing ./kazan-burst-0003.pdf to ./kazan-burst-0003-small.pdf...
Compressing ./kazan-burst-0004.pdf to ./kazan-burst-0004-small.pdf...
...
Compressing ./kazan-burst-0110.pdf to ./kazan-burst-0110-small.pdf...
Compressing ./kazan-burst-0111.pdf to ./kazan-burst-0111-small.pdf...
Compressing ./kazan-burst-0112.pdf to ./kazan-burst-0112-small.pdf...
Compressing ./kazan-burst-0113.pdf to ./kazan-burst-0113-small.pdf...
Concatenating...
Re-compression...
Compression of kazan.pdf done in 564s, filesize is reduced by 92% (from 151200K to 12728K).

出来ました!
ファイルサイズは13MB程度となり、およそ92%のサイズダウンに成功しました!


gsの引数のPDFSETTINGSを変えることで、簡単に画像の圧縮度を変えることができます。
上の例では「-dPDFSETTINGS=/ebook」を使っていますが、「-dPDFSETTINGS=/screen」を使うと更に高い圧縮率が得られます。
しかも、/screenの場合はセグフォで落ちることはないようです。(じゃー上みたいなややこしいスクリプト書く必要ねーじゃん!)

 $ compresspdf kazan.pdf
Compressing kazan.pdf to kazan-small.pdf...
Compression of kazan.pdf done in 362s, filesize is reduced by 96% (from 151200K to 6992K).

ファイルサイズは7MBになりました。
もちろん、中の画像は粗くなります。
PDFSETTINGSには他に/printer、/prepress、/defaultという引数を取ることができ、結果は次のようになりました。

  • /screen 6992K (362s)
  • /ebook 12728K (564s)
  • /printer stackunderflowで出力できず
  • /prepress 21200K (959s)
  • /default 22008K (91s)

出力のpdfを比較すると、やはり/ebookも/prepressや/defaultよりは粗くなっています。
画像自体に意味がある場合は/defaultで、画像に重要な意味がなくてファイルサイズを優先させたい場合は/ebookを選ぶのが、一番いい選択だと思います。

まとめ

gsコマンドがセグフォで落ちるのには本当に困りました。
compresspdfというコマンドを作ったらとても便利になりましたた。
pdfを公開するときはきちんと画像を適切なサイズに圧縮して、ファイルを結合してほしいと思います。