PDF 内で使用されているフォントの表示/置換

どうもです。
オブジェクト指向プログラミングに挫折して,がっくしな感じの
whitypig です。
それはそうと,世の中をちょっと見ていると,自分たちで何かを生み出している所は,
つおいと思う。
人様が作ったモノを,管理したり,ごにょごにょしているだけで金儲けしているところは,
あやしいと思う。某某とか,某某とか。
某某クラブ思い出した。

置換するやつ

昨日言っていたヤツです。
等幅フォントの置換のみを考慮しています。
マッチしたフォントを問答無用で全部置換します。
試してないけど,TrueType のフォントじゃないとたぶんだめ。
一括で色変更するのは,気が向いたら後日。

使い方

1.まずは,使用されているフォントを調べる。

% java ReplaceFont HelloWorld.pdf
====================1====================
(F1, Times-Roman)
(F2, Helvetica-Bold)
====================2====================
(F1, Times-Roman)
(F4, Times-Italic)
(F3, Times-BoldItalic)
(F2, Helvetica-Bold)
====================3====================
(F1, Times-Roman)
(F5, Courier)
====================4====================
(F1, Times-Roman)
(F5, Courier)
(F4, Times-Italic)
# いっぱいあるので省略

2. 置換対象のフォント名にあたりをつける。
今回は,Courier を Consolas に置換したいとします。
3. フォントファイル(ノーマルとボールドの両方)を用意する。
今回は,consola.ttf, consolab.ttf とします。
4. 実行する。

% java ReplaceFont HelloWorld.pdf "Courier" consola.ttf consolab.ttf 

例外がガシガシ飛んでくることもあるので,そんなときは諦めるか,
コメントしてもらえると修正するかもしれません。
できあがった PDF は,<元々の名前から拡張子を抜いたモノ>.mod.pdf になります。
これは決め打ちしてます。


で,できあがった PDF ファイルを見て確認してみる。
ちなみに,HelloWorld.pdf は,http://webster.cs.ucr.edu/AoA/Windows/index.html
から入手できるものを使わせてもらいました。

どんな感じになるか?

見て違いがわかるところを,我らがペイントで切り抜いて貼り付けておきます。

  • 置換前

  • 置換後


ソースコード

汚いけど許して。これでもきれいにしたんだからね!
一応独習Javaで勉強しましたよっと。

import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.pdfbox.exceptions.COSVisitorException;
import org.apache.pdfbox.pdfparser.PDFStreamParser;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDFontDescriptor;
import org.apache.pdfbox.pdmodel.font.PDFontDescriptorDictionary;
import org.apache.pdfbox.pdmodel.font.PDTrueTypeFont;

/**
 * Describe class ReplaceFont here.
 *
 *
 * Created: Sun Jan 10 04:38:55 2010
 *
 * @author whitypig
 * @version 1.0
 */
public final class ReplaceFont {

  private PDDocument _doc;

  /**
   * Creates a new <code>ReplaceFont</code> instance.
   *
   */
  private ReplaceFont(String pdffilename) throws IOException {
    _doc = PDDocument.load(pdffilename);
  }

  /**
   * PDF ファイル内で使用されいてるフォントをすべて表示/置換します。
   * 使い方は,まず,java ReplaceFont pdf を実行して,
   * 置換対象のフォント名を調べてから,
   * すべての引数を渡して実行するという形になります。
   * 置換対象は,等幅フォントのみを仮定しています。
   * 他にも色々暗黙に仮定をしているので・・・。
   */
  public static void main(final String[] args) {
    try {
      if (args.length == 1) {
        ReplaceFont app = new ReplaceFont(args[0]);
        Visitor visitor = new PrintingVisitor(app);
        app.traverseEachPage(args[0], visitor);
      }
      else if (args.length == 4) {
        ReplaceFont app = new ReplaceFont(args[0]);
        Visitor visitor = new ReplacingVisitor(app, args[1], args[2], args[3]);
        app.traverseEachPage(args[0], visitor);
      }
      else {
        System.err.println("Usage: java ReplaceFont <PDF file> [<Target> <TTF> <Bold TTF>]");
        System.err.println("<PDF file>: PDF ファイル名");
        System.err.println("<Target>: 置換対象のフォント名");
        System.err.println("<TTF>: 置換後の TrueType フォントファイル");
        System.err.println("<Bold TTF>: 置換後の ボールドの TrueType フォント名");
        System.err.println("");
        System.err.println("PDF ファイルが指定されない場合は,");
        System.err.println("使用されているを各ページについて表示します。");
        System.exit(1);
      }
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }

  /**
   * PDFファイル内で使用されいてるフォントを,
   * 各ページについて,(キー, フォント名) の形式で表示する。
   */
  private void traverseEachPage(String pdffile, Visitor visitor) throws IOException, COSVisitorException {
    try {
      List pages = _doc.getDocumentCatalog().getAllPages();
      for (int i = 0, size = pages.size(); i < size; i++) {
        PDPage page = (PDPage)pages.get(i);
        visitor.visit(page);
      }
      if (visitor instanceof ReplacingVisitor) {
        // 置換実行なら,置換後のファイルを保存する
        String destName = pdffile.replace(".pdf", ".mod.pdf");
        _doc.save(destName);
      }
    }
    finally {
      if (_doc != null) {
        _doc.close();
      }
    }
  }

  public PDDocument getDoc() {
    return _doc;
  }

}

abstract class Visitor {
  protected ReplaceFont _app;
  abstract void visit(PDPage page) throws IOException;
}

class PrintingVisitor extends Visitor {
  private int _pageNo = 0;

  PrintingVisitor(ReplaceFont app) {
    _pageNo = 0;
    _app = app;
  }

  void visit(PDPage page) throws IOException {
    System.out.println("====================" + (_pageNo + 1) + "====================");
    _pageNo++;
    PDResources resources = page.getResources();
    Map fontMap = resources.getFonts();
    Set keySet = fontMap.keySet();
    Iterator it = keySet.iterator();
    while (it.hasNext()) {
      String key = (String)it.next();
      PDFont font = (PDFont)fontMap.get(key);
      System.out.println("(" + key + ", " + font.getBaseFont().toString() + ")");
    }
  }
}

class ReplacingVisitor extends Visitor {
  private String _targetName;
  private PDTrueTypeFont _font;
  private PDTrueTypeFont _bfont;

  ReplacingVisitor(ReplaceFont app, String targetname, String fontname, String bfontname) throws IOException {
    _app = app;
    _targetName = targetname;
    _font = prepareFixedPitchTTF(fontname);
    _bfont = prepareFixedPitchTTF(bfontname);
  }

  void visit(PDPage page) throws IOException {
    PDResources resources = page.getResources();
    Map fontMap = resources.getFonts();
    Set keySet = fontMap.keySet();
    Iterator it = keySet.iterator();
    String key1 = null;
    String key2 = null;
    // fontMap を走査して置換対象フォントを探す
    while (it.hasNext()) {
      String key = (String)it.next();
      PDFont value = (PDFont)fontMap.get(key);
      String valString = value.getBaseFont();
      // この fontMap 内に置換対象のフォントが含まれているか調べる。
      if (valString.contains(_targetName)) {
        if (valString.contains("Bold")) {
          // 置換対象のボールドフォントのキー
          key2 = key;
        }
        else {
          // 置換対象のフォントのキー
          key1 = key;
        }
      }
    }
    // fontMap 内の値を入れ替えてしまう。
    if (key1 != null) {
      fontMap.remove(key1);
      fontMap.put(key1, _font);
    }
    if (key2 != null) {
      fontMap.remove(key2);
      fontMap.put(key2, _bfont);
    }
    if (key1 != null || key2 != null) {
      resources.setFonts(fontMap);
      page.setResources(resources);
      key1 = key2 = null;
    }
  }

  private PDTrueTypeFont prepareFixedPitchTTF(String fontname) throws IOException {
    PDTrueTypeFont ttf = PDTrueTypeFont.loadTTF(_app.getDoc(), fontname);
    PDFontDescriptorDictionary dict = (PDFontDescriptorDictionary)ttf.getFontDescriptor();
    dict.setAverageWidth(600);
    dict.setMissingWidth(600);
    dict.setMaxWidth(600);
    dict.setFlags(32);
    ttf.setFontDescriptor(dict);
    // FirstChar から LastChar の範囲の文字には,Widthが,
    // それ以外の範囲の文字には,MissingWidth が使われる。
    // だもんで範囲内の文字には,Widths として配列を指定する必要があるけど,
    // 面倒なので,以下の様に手抜きを。
    ttf.setFirstChar(0);
    ttf.setLastChar(1);

    return ttf;
  }
}

まとめ

これが正しい方法かは知りません。
Web で情報を探していたら,PDF はそもそも編集用ではないので,
うんたらかんたら,という意見もありました。
あと,元々の PDF に変更を加えたりするのが法的にどうなのかも知りません。
ちゅうわけで,お約束のすべて自己責任でよろしこ。
さらに日本語の場合は試してないので,どうなるかは未知です。


あと,PDFBOX を作って公開してくれている人たちにありがとう!
オープンソースはすげぇなぁとほんとに思います。