ドキュメントスキャナで紙の本をPDF化(いわゆる自炊)するとき、私はどの本も一律カラー300dpiでスキャンしています。紙の色なども含めて、なるべくオリジナルに近い状態で電子化しておきたいためです。

ただ、紙に色がついていると画像のサイズは大きくなります。特に、経年劣化で紙焼けしているとそれが顕著です。例えば、これは古い文庫本の1ページですが、オリジナルはこれだけで275KBあります。色ムラのためにJPEGの圧縮率が下がってしまうのだと思われます。

img

ImageMagickを使って画像をiPad Air用に縮小最適化するとき、赤のチャンネルだけを取り出してグレースケール化してやると、紙焼けは消えて背景がほとんど白になり、画像サイズも下がります。例えばさきほどのページを「convert -quality 50 -resize “1024x1536>” sample.jpg result1.jpg」で縮小すると181KBになりますが、「convert -quality 50 -resize “1024x1536>” -channel Red -separate sample.jpg result2.jpg」でグレースケール化すると170KBになります。

img

さらに明度とコントラストをちょっとあげてやると、裏うつりや汚れなど色の薄い部分が消えて、サイズはもっと小さくなります。「convert -quality 50 -resize “1024x1536>” -channel Red -separate -modulate 110 +contrast sample.jpg result3.jpg」で変換すると、146KBになりました。オリジナルの半分ですね。

img

(このあたりは「スキャナで自炊した画像を電子書籍(キンドルなど)リーダー向けにImageMagickで最適化してみる」(NETBUFFALO)を参考にさせていただきました。)

ただし、明度とコントラストをあげると薄い網掛けなどは飛んでしまうので、状況に応じて使い分ける必要がありそうです。

img

さて、以前「自炊PDFを第3世代iPadに最適化する その4」でiTextを使ってPDFの画像のみを置き換えるJavaプログラムの例を紹介しましたが、上記の方法で画像をグレースケール化した場合、すこしだけ考慮が必要になります。PRStreamに新しいJPEGデータをsetData()するとき、「/ColorSpace」に「DeviceGray」という値をセットしてやらないと、PDFが正しく表示できなくなります。画像がグレースケールかどうかはBufferedImageのgetType()がTYPE_BYTE_GRAYかどうで判断できます。この考慮を入れた「iPad最適化の紙焼け除去版」を載せておきます。

import java.awt.image.BufferedImage;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import javax.imageio.ImageIO;

import com.itextpdf.text.pdf.PRStream;
import com.itextpdf.text.pdf.PdfDictionary;
import com.itextpdf.text.pdf.PdfIndirectReference;
import com.itextpdf.text.pdf.PdfName;
import com.itextpdf.text.pdf.PdfNumber;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfStamper;
import com.itextpdf.text.pdf.parser.PdfImageObject;

public class OptiPadWithBleaching {
    public static void main(String[] args) throws Exception {
        PdfReader reader = new PdfReader(args[0]);
        for (int page = 1; page <= reader.getNumberOfPages(); page++) {
            PdfDictionary res = reader.getPageResources(page);
            PdfDictionary xobj = res.getAsDict(PdfName.XOBJECT);
            PdfIndirectReference im0 = xobj.getAsIndirectObject(new PdfName("Im0"));
            PRStream stream = (PRStream) reader.getPdfObject(im0.getNumber());

            Path tmpJpg = Paths.get("tmp.jpg");
            PdfImageObject imgObj = new PdfImageObject(stream);
            Files.write(tmpJpg, imgObj.getImageAsBytes());

            int width = ((PdfNumber) stream.get(PdfName.WIDTH)).intValue();
            int height = ((PdfNumber) stream.get(PdfName.HEIGHT)).intValue();
            String resizeOpt = null;
            if (width > 2539 || height > 2539) {
                resizeOpt = width < height ? "1536x2048>" : "2048x1536>";
            } else {
                resizeOpt = width < height ? "1024x1536>" : "1536x1024>";
            }
            String cmd = "mogrify -quality 50 -resize " + resizeOpt +
                " -channel Red -separate -modulate 110 +contrast " + tmpJpg;
            Process proc = Runtime.getRuntime().exec(cmd);
            proc.waitFor();

            stream.setData(Files.readAllBytes(tmpJpg), false, PRStream.NO_COMPRESSION);
            BufferedImage newImg = ImageIO.read(tmpJpg.toFile());
            stream.put(PdfName.WIDTH, new PdfNumber(newImg.getWidth()));
            stream.put(PdfName.HEIGHT, new PdfNumber(newImg.getHeight()));
            stream.put(PdfName.FILTER, PdfName.DCTDECODE);
            if (newImg.getType() == BufferedImage.TYPE_BYTE_GRAY) {
                stream.put(PdfName.COLORSPACE, PdfName.DEVICEGRAY);
            }
            Files.delete(tmpJpg);
        }
        FileOutputStream out = new FileOutputStream("out.pdf");
        PdfStamper stamper = new PdfStamper(reader, out);
        stamper.close();
        reader.close();
    }
}

※バージョンメモ

  • JDK 1.8.0_111
  • iText 5.5.10
  • ImageMagick 7.0.3