御嶽山噴火によって、火山活動への関心が高まっています。
それに伴って、講談社の公式サイトではブルーバックス『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を公開するときはきちんと画像を適切なサイズに圧縮して、ファイルを結合してほしいと思います。