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 を作って公開してくれている人たちにありがとう!
オープンソースはすげぇなぁとほんとに思います。