Javaでフォント名を取得
以前、JavaでPdfを作成する際に文字列のShapeを取得しました。
その際、itext8を利用したのですが、Itext用のフォントクラスPdfFontを使うか、標準的なJavaのFontを使うか迷いました。
Shapeにして線画するので、PdfFontにする必要性がないのですが、標準ライブラリでFontを使用しようとした場合にひとつ問題になるのが、ttcファイルの読み込みでした。
どうしようか考えている中でいろいろとフォント名を取得する方法を知ることができましたので、忘れないように書き留めておきます。
これらのコードはChatGPTから得られたコードを参考にしていますが、自身の環境で実行可能な状態に修正しています。
システムのフォントを取得する方法
システムに登録されているフォントを使う場合は、GraphicsEnvironmentのgetAllFontsメソッドが用意されていますので非常に簡単です。
public static ArrayList<String> getFonts(boolean blnJPOnly, boolean blnSystem) {
Set<String> set = new HashSet<String>();
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
Font[] allFonts = ge.getAllFonts();
for (Font font : allFonts) {
if (blnJPOnly) {
//日本語が表示できるかの簡易判定
if (font.canDisplay('漢')) {
set.add(font.getFamily());
}
} else {
set.add(font.getFamily());
}
// Setで重複を除去してListにしてソートして返す
List<String> lst = new ArrayList<String>(set);
lst.sort(null);
return lst;
}
Itext8(7以降)を使う方法
次にItext8での方法です。
ttcのインデックスの最大値を取得する事ができないので、例外が発生するまでループさせるというところが若干気持ちが悪いです。
PdfFontのインスタンスから名前を取得しますが、ここで得られた名前を使って標準のFontのインスタンスを生成する事ができます。
public static List<String> getPdfFonts(boolean blnJPOnly, String strTargetDir) {
Set<String> set = new HashSet<String>();
File dir = new File(strTargetDir);
//日本語の表示が可能か否かを「漢」という字のコードポイントが存在するかで判定します
int intCodePoint = "漢".codePointAt(0);
int intIndex = 0;
for(File font: dir.listFiles()) {
if (font.isFile()) {
//ttcだった場合は
if (font.getName().toLowerCase().endsWith(".ttc")) {
intIndex = 0;
while(true) {
try {
PdfFont pFont = PdfFontFactory.createFont(font.getAbsolutePath()+","+String.valueOf(intIndex++));
if (blnJPOnly) {
try {
if (pFont.containsGlyph(intCodePoint)) {
set.add(pFont.getFontProgram().getFontNames().getFontName());
}
} catch(UnsupportedCharsetException e) {
//存在しないコードポイントをチェックしようとすると例外が発生するようです。
//e.printStackTrace(); 無視
}
} else {
set.add(pFont.getFontProgram().getFontNames().getFontName());
}
//廃棄
pFont=null;
} catch(Exception e) {
break;
}
}
} else if (font.getName().toLowerCase().endsWith(".ttf")) {
try {
PdfFont pFont = PdfFontFactory.createFont(font.getAbsolutePath());
if (blnJPOnly) {
try {
if (pFont.containsGlyph(intCodePoint)) {
set.add(pFont.getFontProgram().getFontNames().getFontName());
}
} catch(UnsupportedCharsetException e) {
//存在しないコードポイントをチェックしようとすると例外が発生するようです。
//e.printStackTrace(); 無視
}
} else {
set.add(pFont.getFontProgram().getFontNames().getFontName());
}
//廃棄
pFont=null;
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//名前順にソートしてリストで返す
List<String> lst = new ArrayList<String>(set);
lst.sort(null);
return lst;
}
上記のコードでは対象となるフォントが多すぎるとヒープ不足でエラーになります。その際は、PdfFontFactory.createFontでPdfFontを生成してフォント名を取得する部分を別に関数化します。そうすればGCがフォント取得中にも働きます。
Itext8で日本語のフォント名を取得したい
先のようにフォント名を取得すると、おそらくほとんどローマ字表記のフォント名となると思います。もし、日本語のフォント名を取得したい場合は、getFontNames の getFullName メソッドを使います。
getFullNameはString[][]を返します。ここで得られるのはフォント情報の配列となります。フォント情報もまた配列となっており、公式にはドキュメントを見つけられませんでしたが、その構造は[プラットフォームID,プラットフォームエンコーディングID、言語ID、フォント名」となっているようです。次のPDFBoxでの値で得られた値と似た構造だったためそう判断しました(Itext8はPDFBoxで定義された定数と同じ値を利用しているようです)。
PDFBoxで案内されている定数値では、プラットフォームIDは1がMac、3がWindowsとなっています。エンコーディングはWindows環境のフォントではほとんどが1でした。言語IDが1033だとLANGUGAE_WINDOWS_EN_US=米英語を意味します。こちらはPDFBoxの定数にもリストされていませんでしたが、1041が日本語のようです。
これらの情報をまとめると、Windowsの日本語環境のの場合は、プラットフォームIDが3、言語IDが1041のフォント名を取得すれば日本語名で取得できることが多いです。この際フォールバックとして1041が存在しなかったら1033も取得するようにするといいと思います。
コードのサンプルは次のようになります。
String[][] row = pdfFont.getFontProgram().getFontNames().getFullName();
for(int i =0; i < row.length; i++) {
if (4 <= row[i].length) {
if (row[i][0].equals("3")) {
if (row[i][2].equals("1041")) {
strFontNameJp = row[i][3];
} else if(row[i][2].equals("1033")) {
strFontNameEnUs=row[i][3];
}
}
}
}
照会したコードの中では日本語対応フォントかどうかをコードポイントに対するマップが存在するかで判定しましたが、言語IDが1041かどうかで判定するのも方法だと思います。
フォント名ではなくファミリ名を使いたい場合はgetFullName のかわりに getFamilyName を使います。
PDFBoxを利用する方法
以前取り上げたことのある Apache PDFBoxでも、ttcファイルの中からフォント名を取得することができます。
バージョン3.0がリリースされているのを今回知ったので、使ってみました。
肝になるのは TrueTypeFontProcessor クラスで、Fontファイルをコンストラクタに渡してイニシャルした TrueTypeCollectionクラスの processAllFontsメソッドにそれを渡すことで、ttcファイル内のすべてのttfフォントに対して処理が実行されます。
public static List<String> getFonts(boolean blnJpOnly, String strTargetDir) {
Set<String> set = new HashSet<String>();
File dir = new File(strTargetDir);
TTFParser parser = new TTFParser();
//PDFBoxではこの方法は意図したように稼働しないようです
//int intCodePoint = "漢".codePointAt(0);
TrueTypeFontProcessor ttfp = new TrueTypeFontProcessor() {
@Override
public void process(TrueTypeFont ttf) throws IOException {
String strFontName = ttf.getName();
if (strFontName!=null) {
if (blnJpOnly) {
if(ttf.getGlyph().getGlyph(intCodePoint)!=null) {
set.add(ttf.getName());
}
} else {
set.add(ttf.getName());
}
}
}
};
for(File font: dir.listFiles()) {
if (font.isFile()) {
try {
if (font.getName().toLowerCase().endsWith(".ttc")) {
//ttcだった場合は
TrueTypeCollection ttc = new TrueTypeCollection(font);
ttc.processAllFonts(ttfp);
ttc.close();
} else if (font.getName().toLowerCase().endsWith(".ttf")) {
//ttfの場合は
TrueTypeFont ttf = parser.parseEmbedded(new FileInputStream(font));
if (blnJpOnly) {
//if(ttf.getGlyph().getGlyph(intCodePoint)!=null) {
set.add(ttf.getName());
//}
} else {
set.add(ttf.getName());
}
ttf.close();
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
List<String> lst = new ArrayList<String>(set);
lst.sort(null);
return lst;
}
PDFBoxで日本語フォントの判定と日本語名の取得
PDFBoxではフォント情報の明細を得るためにはTrueTypeFontのインスタンスから、一旦 getNaming を呼んで取得したオブジェクトから getNameRecords メソッドを呼びます。
Itextの場合は文字列の配列で戻りましたが、こちらは NameRecord クラスのリストが返ります。
ここで利用するNameRecordeクラスのメソッドを簡単に説明すると次のようになります。
- getNameId
取得対象の情報を指定します。フルフォント名の場合は4(NAME_FULL_FONT_NAME)、ファミリ名の場合は1(NAME_FONT_FAMILY_NAME)を指定します。
- getPlatformId
Macなら1(PLATFORM_MACINTOSH)、Windowsなら3(PLATFORM_WINDOWS)です。
- getLanguageId
米英語なら1033(LANGUGAE_WINDOWS_EN_US)、日本語だと1041のようです。
- getString
NameRecordの文字列値を取得します。NameIDが1ならフォントファミリ名、4ならフォント名が取得できます。他には0でコピーライトなども取得可能です。
稼働の確認はバージョン3.0.0で行いましたが、ここで使われている定数値のリストは、3系のものが見つけられなかったので、バージョン2系のPDFBox Constant Field Valuesを参考にしています。
フォント名を取得する際のサンプルコードは次のような感じになると思います。
recordList = ttf.getNaming().getNameRecords();
for(NameRecord r : recordList) {
if (r.getNameId() == 4 && r.getPlatformId()==3) {
if (r.getLanguageId()==1041) {
//日本語フォント名を取得
strFontNameJp=r.getString();
} else if(r.getLanguageId()==1033) {
strFontNameEnUs = r.getString();
}
}
}
ttfだけならJavaの標準ライブラリでも可能
ttfファイルまたは、ttcの最初のひとつだけでよければJavaの標準機能からもフォント名を取得できます。
public static List<String> getTTFFonts(boolean blnJpOnly, String strTargetDir) {
Set<String> set = new HashSet<String>();
File dir = new File(strTargetDir);
for(File font: dir.listFiles()) {
if (font.isFile()) {
if (font.getName().toLowerCase().endsWith(".ttf") || font.getName().toLowerCase().endsWith(".ttc")) {
//ttcの場合も最初のひとつだけは名前が取得可能
try {
// TTFファイルを読み込む
Font ttf = Font.createFont(Font.TRUETYPE_FONT, font);
if (blnJpOnly) {
if (ttf.canDisplay('漢')) {
set.add(ttf.getFontName());
}
} else {
set.add(ttf.getFontName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
List<String> lst = new ArrayList<String>(set);
lst.sort(null);
return lst;
}
Java.awt.Fontのフォールバック
Java.awt.Fontでは、Fontコンストラクタに渡したフォント名が実際には存在しないものだった場合に、例外をスローしたりnullを返したりすることはせずに、フォールバックとしてデフォルトのシステムフォントを返します。
これは一長一短で、フォントを強制したい場合は意図したフォントが利用できているかどうかのチェックが必要になります。
java.awt.Fontのインスタンスには、getName というメソッドがありますが、これはフォントが存在しない場合もコンストラクタに渡した値を保持するようです。
他に、getFontName メソッドからもフォント名を取得できます。こちらはエラー時にはデフォルトフォント名が設定されます。ただこの値は、BOLD等やイタリック等のパラメータが反映されていたり、フォント名が日本語で得られることがあったりと、単純には比較できません。
BOLDやイタリック等の情報を排除するために、getFamily メソッドでファミリ名を取得しても、ローマ字のフォント名で初期化したのに日本語のフォントファミリー名を返されてしまうと比較できません。
そこで、少々すっきりしないコードですが、java.awt.Font でフォールバックが発生した時には getFontNameは「 Dialog 」という論理フォント名を返すようでしたのでそれを使って判定することにしました。
Font font = new Font("Yu Mincho Light",Font.PLAIN,12);
if (font.getFamily().eqluals("Dialog")) {
//フォント取得失敗
}
余談:動的なPDFBoxライブラリの読み込み
ItextとPDFBoxはどちらもPDFを扱うライブラリで、どちらも依存関係として登録しておくのは無駄な気がしたので、PDFBoxは使いたい場合に動的に使えるようにコードを書けないかと調べてみました。
public static void main(String[] argsbat) {
//PDFBoxのjar
String strPDFBoxJarPath = "c:\\pdfbox-app-3.0.0.jar";
//ttcファイル
String strTtcPath = "c:\\windows\\fonts\\HGRSGU.TTC";
//結果を保持するセット
Set<String> setFontName = new HashSet<String>();
try {
// URLオブジェクトを作成
File fPDFBox = new File(strPDFBoxJarPath);
URL jarFileUrl = fPDFBox.toURI().toURL();
// ※1.クラスローダーを作成
URLClassLoader classLoader = new URLClassLoader(new URL[]{jarFileUrl}, コードを書いているクラス.class.getClassLoader());
// クラスの定義 static部のイニシャルもしています
Class<?> trueTypeCollectionClass = Class.forName("org.apache.fontbox.ttf.TrueTypeCollection", true, classLoader);
// $はインナークラスを意味します
Class<?> fontProcessorClass = Class.forName("org.apache.fontbox.ttf.TrueTypeCollection$TrueTypeFontProcessor", true, classLoader);
// ※2.TrueTypeCollection のインスタンスを生成
Object ttcInstance = trueTypeCollectionClass.getConstructor(File.class).newInstance(new File(strTtcPath));
// ※3.Proxyを使ってFontProcessorの動的な実装をする
Object fontProcessorInstance = Proxy.newProxyInstance(classLoader, new Class[]{fontProcessorClass}, (proxy, method, args) -> {
//Processメソッドの実装
if (method.getName().equals("process")) {
// 第一引き数にフォントが入ってくる(ttfクラス)
Object ttf = args[0];
// ttfクラスからgetNameメソッドを取得して、呼び出し
Method getNameMethod = ttf.getClass().getMethod("getName");
String name = (String) getNameMethod.invoke(ttf);
// 目的のフォント名取得
//System.out.println("Font Name: " + name);
setFontName.add(name);
}
return null;
});
// ※4. 実装したクラスを引き数に渡して実行
Method processAllFontsMethod = trueTypeCollectionClass.getMethod("processAllFonts", fontProcessorClass);
processAllFontsMethod.invoke(ttcInstance, fontProcessorInstance);
//フォントを閉じる
Method ttcCloseMethod = ttcInstance.getClass().getMethod("close");
ttcCloseMethod.invoke(ttcInstance);
} catch(Exception e) {
e.printStackTrace();
}
}
- ※1
Class.forNameを使って動的にライブラリを読み込んでいます。Class.forNameメソッドを使用すると、クラスは自動的に初期化(static部が読み込まれる)されます。
Class.forName以外に、動的読み込みを行う方法に、ClassLoader.loadClassもありますが、こちらは呼び出し時に初期化されずに初回利用時に初期化されます。
- ※2
クラスを動的に読みだしたら、getConstructor(Class...).newInstance()メソッドでそのインスタンスを生成します。
getConstructorの引数にはコンストラクタに定義してあるクラスを渡し、newInstanceでは引数の実体を渡します。
- ※3
Proxyクラスは、任意のクラスをラップして代理的に処理を行う為のクラスです。たとえば、特定のメソッド利用時にログをとりたい時などに使いますが、ここではProxyの機能を使うことで動的に関数の実装をしています。
実装部は「(proxy, method, args) ->」という形でラムダ式で省略しています。
ラムダ式は関数型インターフェース(抽象メソッドをひとつだけ持つ)に場合に利用可能で、それらの引数の型を省略することもできます(記述した場合はもとの定義と違っていたらエラーになります)。
このラムダ式を通常のクラスに戻すなら次のようになると思います。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals("process")) { Object ttf = args[0]; // The TrueTypeFont object Method getNameMethod = ttf.getClass().getMethod("getName"); String name = (String) getNameMethod.invoke(ttf); System.out.println("Font Name: " + name); } return null; }
これは先のTrueTypeFontProcessorのインスタンスの部分を抽象化したもので、第1引数にプロキシインスタンス、第2引数に対象の関数(ここではprocess)、第3引数に関数に渡される引数(ここではttfフォント)が入ります。
- ※4.
インスタンスから関数名を指定してメソッドを取得し、そのインスタンスからinvokeメソッドを呼ぶことで元の関数を実行します。
ちなみにここまでのようにインスタンスから、クラスやインターフェース、メソッドなどの情報を提供する機能のことを「 リフレクション 」と呼びます。