フォントの置換をIDMLを使って行う その2

IDMLの勉強を目的に、IDMLを使ってInDesign文書のフォントを置換するプログラムを作りました。プログラムを書いている途中で、フォント置換にIDMLを利用するのは不適切なことに気が付いたのですが、一応、プログラムが動作するところまで書き上げました。
フォント置換を行うのにIDMLを利用するのが適さない理由は、IDMLInDesignの実行環境と分離独立しているからです。例えば、あるInDesignの実行環境で利用可能なフォントを調べようと思っても、IDMLを使って調べるのは困難です。IDMLはドキュメントを構築するのに必要なオブジェクトをモデル化したものです。InDesignの実行環境に関する情報にアクセスするためのオブジェクトはモデル化されていません。フォントの置き換えという実行環境に依存する処理を行うのに、実行環境の情報にアクセスする術がないのは致命的です。

【プログラムの作成方針】
簡単に実装することを最優先し、IDMLToolsのサンプルプログラムとまったく同じ構成でプログラムを作成することにしました。具体的には、下図のようにサンプルプログラムとまったく同様のフォルダ構成で、バッチやJavaソースコードを作りました。Javaプログラムも %IDMLTOOLS_HOME%jars\idmltools.jar にアーカイブしました。

%IDMLTOOLS_HOME% -+- samples --- replacefont -+- run.bat  <- サンプルを実行するためのバッチ
                  |                           |
                  |                           +- replacefont.bat
                  |                           |
                  |                           +- missingfont.idml  <- サンプルのIDMLファイル
                  |                           |
                  |                           +- fontlist.txt <- フォントの置換を指定したCSVファイル
                  |
                  +- src - com - adobe - idml - samples - ReplaceFont.java

【 準 備 】
Javaコンパイルのために、以下の2つをインストールする必要があります。

  1. Java SE Development Kit 6
  2. Ant

Javaのコーディングのためには、Eclipseがお奨めです。

※ Antビルドファイル %IDMLTOOLS_HOME%build.xml を使用することにより、簡単に %IDMLTOOLS_HOME%jars\idmltools.jar をリビルドできます。

フォントの置換を指定したCSVファイル(fontlist.txt)を用意します。
fontlist.txt

$ID/新ゴB,小塚明朝 Pro

【 バッチコマンド 】

replacefont.bat

@echo off

if "%IDMLTOOLS_HOME%"=="" goto need_idml_home


java -classpath "%IDMLTOOLS_HOME%\jars\idmltools.jar;%IDMLTOOLS_HOME%\jing\bin\xercesImpl.jar;%IDMLTOOLS_HOME%\jing\bin\xml-apis.jar;%IDMLTOOLS_HOME%\jing\bin\saxon.jar;%IDMLTOOLS_HOME%\jing\bin\isorelax.jar" com.adobe.idml.samples.ReplaceFont %*

goto exit

:need_idml_home
echo IDMLTOOLS_HOME is not set.  Please set IDMLTOOLS_HOME.


:exit

run.bat

@echo off

cmd /C replacefont.bat missingfont.idml fontlist.txt

Javaソースコード
ReplaceFont.java

/**
 *
 * 外部ファイルの指定に従ってフォントを置換する
 *
 */
package com.adobe.idml.samples;

import com.adobe.idml.*;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import java.io.*;

import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.*;
import org.w3c.dom.*;

public class ReplaceFont
{
  /**
   * ReplaceFontのコンストラクタの引数には、IDMLファイルのパスを指定してください。
   * 指定されたIDMLファイルは、本クラスのオブジェクトを構築するときに一時フォルダへ展開されます。
   *
   * @param idmlFile IDMLファイルのパス
   * @throws IOException
   */
  public ReplaceFont(String idmlFile) throws IOException
  {
    fIDMLFile = idmlFile;   // IDMLファイルのパス
    fDeleteWorkingDir = false;  // 一時フォルダ削除のフラグ

    File f_IDML = new File(fIDMLFile);

    if( f_IDML.isDirectory())
    {
      // フォルダのパスが指定された場合
      fWorkingDir = f_IDML;
      fDeleteWorkingDir = false;
    }
    else
    {
      // UCFのパスが指定された場合
      fWorkingDir = com.adobe.idml.Package.decompress(fIDMLFile); // 一時フォルダへパッケージを解凍する
      fDeleteWorkingDir = true;
    }

    fLocator = new PackageXmlLocator(fWorkingDir);  // パッケージ内のXMLファイルにアクセスするためのクラス
  }

  /**
   * IDMLファイルの処理が完了したら、本メソッドを呼び出してください。
   * 本メソッドが呼び出されるまで、IDMLファイルは一時フォルダに展開された状態でディスクに記憶されています。
   */
  public void release()
  {
    if( fDeleteWorkingDir)
      FileUtils.DeleteDirectory(fWorkingDir);

    fIDMLFile =  null;
    fWorkingDir = null;
  }

  /**
   * フォントの対応を記述したCSVファイルを読み込みます。
   *
   * @param inFile 旧・新フォントの対応を記述したCSVファイルのパス
   * @throws Exception
   */
  public void readFontReplaceFile(String inFile) throws Exception
  {
    File f_fontReplaceFile = new File(inFile);
    BufferedReader reader = null;
    try
    {
      reader = new BufferedReader(
            new InputStreamReader (
            new FileInputStream(f_fontReplaceFile), "SJIS"));
      String oneLine = null;
      while ((oneLine = reader.readLine()) != null)
      {
        //System.out.println(oneLine);
        String tmpArray[] = oneLine.split(",");
        fontReplaceList.put(tmpArray[0], tmpArray[1]);
      }
    }
    catch (FileNotFoundException e)
    {
      e.printStackTrace();
    }
    catch (IOException e)
    {
      e.printStackTrace();
    }
    finally
    {
      if (reader != null)
      {
        try
        {
          reader.close();
        }
        catch (IOException e)
        {
          e.printStackTrace();
        }
      }
    }

  }

  /**
   * 新フォントがFonts.xmlに登録されていることを確認します。
   *
   * @throws Exception
   * @return true:登録されている,false:登録されていない
   */
  public boolean fontExistenceCheck() throws Exception
  {
    // CSVファイルで指定されたフォントがFonts.xmlファイルの中にあるかどうか確認する
    ArrayList<String> fontFamilies = XmlUtils.getAttributes(fLocator.getResourcesXmlFiles(), "//FontFamily/@Name");
    Set<String> keys = fontReplaceList.keySet();
    Iterator<String> iter = keys.iterator();
    boolean notRegisteredFontExist = false;
    while (iter.hasNext())
    {
      String oldFontName = iter.next(); // 旧フォント名
      String newFontName = fontReplaceList.get(oldFontName);  // 新フォント名
      if (fontFamilies.contains(newFontName))
      {
        System.out.println("「" + oldFontName + "」を「" + newFontName + "」に置換します");
      } else
      {
        System.out.println("「" + newFontName + "」は、Fonts.xmlに登録されていません");
        notRegisteredFontExist = true;
      }
    }
    if (notRegisteredFontExist)
    {
      System.out.println("\nError: CSVファイルで指定されたフォントは、Fonts.xmlに登録されていません。");
      return(false);
    }
    return(true);
  }

  /**
   * すべてのストーリーファイルの中の、AppliedFontの指定を確認し、対象のフォント指定を書き換えます。
   * @throws Exception
   */
  public void doReplace() throws Exception
  {
    // すべてのストーリーファイルのAppliedFontを調べて、指定されたフォントなら書き換える
    ArrayList<String> storyFiles = fLocator.getStoriesXmlFiles(); // すべてのストーリーファイルのパスを取得
    ListIterator<String> itr = storyFiles.listIterator();
    DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
    Document document = null;
    InputStream is = null;
    boolean isModified;
    while (itr.hasNext())
    {
      String fPath = itr.next();  // ストーリーファイルのパス
      //System.out.println(fPath);
      File f_story = new File(fPath);
      isModified = false;
      document = builder.parse(f_story);
      if (document != null)
      {
          XPathFactory factory = XPathFactory.newInstance();
        XPath xpath = factory.newXPath();
        XPathExpression expr = xpath.compile("//AppliedFont/text()");
        Object result = expr.evaluate(document, XPathConstants.NODESET);
          NodeList appliedFontNodes = (NodeList) result;
        for (int i = 0, length = appliedFontNodes.getLength(); i < length; i++)
        {
          Text appliedFontTextNode = (Text)appliedFontNodes.item(i);
          String fontName = appliedFontTextNode.getNodeValue();
          //System.out.println("*** fontName = " + fontName);
          if (fontReplaceList.containsKey(fontName))
          {
            appliedFontTextNode.setNodeValue(fontReplaceList.get(fontName));
            isModified = true;
            //System.out.println("*** isModified");
          }
        }
        if (isModified)
        {
          boolean ret = saveDocument(f_story, document);  // 変更済みのDOMをストーリーファイルに保存する
          if (ret == false)
          {
            System.out.println("Error: ファイルの書き込みに失敗しました。\nファイル = " + fPath);
          }
        }
      }
      document = null;
      is = null;
    }
  }

  /**
   * Documentを指定ファイルに保存する
   * @param aFile 保存先ファイル
   * @param aDocument Documentインスタンス
   * @return true:成功, false:失敗
   */
  public boolean saveDocument( File aFile, Document aDocument )
  {
    //---------------------------------
    // step1. Transformerの準備
    //---------------------------------
    Transformer transformer = null;
    try {
      TransformerFactory factory = TransformerFactory.newInstance();
      transformer = factory.newTransformer();
    } catch (TransformerConfigurationException e) {
      // 通常はありえない。
      e.printStackTrace();
      return false;
    }

    //---------------------------------
    // step2. Transformerの動作設定
    //---------------------------------
    transformer.setOutputProperty("indent",   "no");
    transformer.setOutputProperty("encoding", "UTF-8");

    //-------------------------------------
    // step3. Documentをファイルに書き出す
    //------------------------------------
    try {
            transformer.transform(new DOMSource( aDocument ), new StreamResult( aFile ) );
    } catch (TransformerException e) {
      // 書き出しエラー発生
      e.printStackTrace();
      return false;
    }

    // 終了
    return true;
  }

  /**
   * 一時フォルダの内容をIDMLパッケージにアーカイブします。
   * あるいは、他のフォルダにコピーします(引数にフォルダが指定された場合)。
   *
   * @param output IDMLパッケージファイルのパス、あるいはコピー先のフォルダパス
   * @throws Exception
   */
  public void writeTempFiles(String output) throws Exception
  {
    File outFile = new File(output);
    if( outFile.isDirectory() && outFile != fWorkingDir)
    {
      FileUtils.CopyDir(fWorkingDir, outFile);  // フォルダパスが指定された場合は、ディレクトリの内容をコピーする
    }
    else if(outFile != fWorkingDir)
    {
      com.adobe.idml.Package.compress(fWorkingDir, output);   // ファイルパスが指定された場合は、一時フォルダをパッケージにする
    }
  }

  /**
   * メンバ変数の宣言
   */
  String fIDMLFile;
  File fWorkingDir;
  boolean fDeleteWorkingDir;
  PackageXmlLocator fLocator;
  HashMap<String, String> fontReplaceList = new HashMap<String, String>();  // 旧・新フォント名を記憶するハッシュ

  /**
   * mainメソッドの使い方を表示します。
   */
  private static void usage()
  {
    System.out.println("【使い方】");
    System.out.println("ReplaceFont idmlPackage fontListFile");
    System.out.println("\n【機能】");
    System.out.println("ReplaceFontは、外部ファイルの指定に従ってフォントを置換します。");
    System.out.println("fontListFileには、旧・新フォント名をカンマ区切りで記述します。");
    System.out.println("\n【制限】");
    System.out.println("新フォントは、文書中で使われているフォントのみが指定可。");
    System.out.println("\n【使用例】");
    System.out.println("ReplaceFont SampleDoc.idml fontlist.txt");
    System.exit(-1);
  }

  /**
   * 本クラスを使うと、コマンドラインからフォンtの置換が行えるようになります。
   *
   * @param args
   */
  public static void main(String[] args)
  {
    ReplaceFont rs = null;

    try
    {
      if(args.length < 2 || args[0].equalsIgnoreCase("-h"))
      {
        usage();
      }

      String idmlFile = args[0];
      String fontListFile = args[1];

      if(com.adobe.idml.Package.isAPackage(idmlFile))
      {
        rs = new ReplaceFont(idmlFile);
        rs.readFontReplaceFile(fontListFile);   // フォントの対応を記述したCSVファイルを読み込む
        if (rs.fontExistenceCheck())
        {
          rs.doReplace();             // CSVファイルの指定に従ってフォントを置換する
          idmlFile = "NEW-" + idmlFile;     // IDMLファイルのファイル名に"NEW-"の接頭辞を付ける
          rs.writeTempFiles(idmlFile);      // 一時フォルダに展開したXMLファイルをパッケージに圧縮する
          rs.release();             // 一時フォルダを破棄する
        }
      }
      else
      {
        System.err.println("Error:  第一引数にはIDMLパッケージのパスを指定してください。");
      }
    }
    catch(Exception e)
    {
      if(rs != null)
        rs.release();

      String err = "フォントの置換に失敗しました。\nエラー・メッセージ:\t%s\nスタック・トレース:\t%s\n";
      System.err.printf(err, e.getMessage(), e.getStackTrace());
    }
  }
}

【 課 題 】
このプログラムでは、置換後の新しいフォントとして指定できるのは、Fonts.xmlに登録済みのフォントだけに制限しています。システムにインストールされている任意のフォントを指定可能にするには、必要に応じてFonts.xmlにFontFamily要素やFont要素を追加するようにしなければいけません。結構、めんどうな処理になると思います。

【 免 責 】
上記スクリプトの使用により発生する、データの破損などのあらゆる不具合・不利益については、一切の責任を負いかねますのでご了解ください。

【 参考にしたページ 】

【 雑 感 】
Javaのプログラミングはあまり経験がなかったので、コーディング完了までにだいぶ時間がかかってしまいました。XMLファイルの書き換えには、他のサンプルプログラムと同様にXSLを使う方法を考えましたが、結局、Javaの中でDOMを構築する方法を選択。結果、この選択が正解だったようです。XSLを使う場合、Javaとの間のパラメータのやり取りがネックになります。を使うしか方法がないので、複雑な構造のデータをやり取りすることができません。
さて、IDMLを活用したら、どんな面白いことができるかなぁ。。。
ここからが知恵の使いどころ。