続・続・JavaでXMLをフォーマットする

またも、別の方法があるというコメントいただきました。(id:toolkit:20060719)

transformer.setOutputProperty(javax.xml.transform.OutputKeys.INDENT, ”yes”);
transformer.setOutputProperty(javax.xml.transform.OutputKeys.METHOD, ”xml”);
transformer.setOutputPropert(org.apache.xml.serializer.OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, ”2”);

なので、記事を書いた責任上(?)検証してみます。

実は、最初にこれと似たような事を試しました。

でも、うまく行かなかったんですねぇ。

OutputKeys.INDENT , "yes"

のみ指定していて、それでうまく行かなくてスタイルシート当てようかと思ったんです。

いただいたコメントでの、一番最後の指定がキモですねー。

org.apache.xml.serializer.OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, ”2”

うーん、これは気がつかなかった。

Xalanが必要だった

この、org.apache.xml.serializer.OutputPropertiesFactoryですが、てっきりXercesに入っているのかと思いきや、Xalanに入っているものでした。

しかも、メインのxalan.jarじゃなくて、最近切り離されたらしい(?)serializer.jarに入っています。

使用したコードたち

in.xml

<?xml version="1.0" encoding="UTF-8"?><test><hoge>ほげ</hoge><fuga>ふが</fuga></test>

動作検証用のコード

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;

public class OutputPropertiesFactoryTest01 {

	public static void main(String[] args) throws Exception {
		// フォーマットしたいXML
		File inXml = new File("in.xml");
		// フォーマットしたXML
		File outXml = new File("out.xml");

		// フォーマットしたいXMLのDOMオブジェクトを作る
		DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
		DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
		InputStream in = new FileInputStream(inXml);
		Document doc = docBuilder.parse(in);
		in.close();

		// Transformerを用意する。
		TransformerFactory transFactory = TransformerFactory.newInstance();
		Transformer transformer = transFactory.newTransformer();
		transformer.setOutputProperty(OutputKeys.INDENT, "yes");
		transformer.setOutputProperty(OutputKeys.METHOD, "xml");
		transformer.setOutputProperty(
            org.apache.xml.serializer.OutputPropertiesFactory.S_KEY_INDENT_AMOUNT,"2");
		  
		 //書き出し
		DOMSource source = new DOMSource(doc);
		OutputStream out = new FileOutputStream(outXml);
		Result result = new StreamResult(out);
		transformer.transform(source, result);
		out.close();
	}
}

実行してみました。

serializer.jarだけをクラスパスに通して実行します。

out.xml

<?xml version="1.0" encoding="UTF-8"?>
<test>
<hoge>ほげ</hoge>
<fuga>ふが</fuga>
</test>

ありゃ?改行はされたけど、インデントされてません。


しょうがないので、xalan.jarもクラスパスに含めて実行します。

out.xml

<?xml version="1.0" encoding="UTF-8"?>
<test>
  <hoge>ほげ</hoge>
  <fuga>ふが</fuga>
</test>

うまく行きました。


っつーことは、結局この方法をとる場合は、XSLTプロセッサ本体もJDK以外のものを使わないと駄目みたい。
これもLSSerializerの時の一緒で、JDKに入っているものが古い(?)のでしょうか。

OutputPropertiesFactoryなるもの

Eclipseさんに聞いてみると、JDKにはcom.sun.org.apache.xml.internal.serializer.OutputPropertiesFactoryがある事が分かりました。

試しに、こっちの方を使って実験してみました。

JDK
改行はされるけどインデントはつかない
Xalan
成功

ちょっと混乱気味(笑)

結局、Xalan本体が必要で、OutputKeys.INDENTに"yes"を指定しただけでは駄目で、OutputPropertiesFactory.S_KEY_INDENT_AMOUNTに数値を指定しなきゃ、という結論でしょうか??


これもJDKの実装がXalanの新しいものになれば、そのうち解決されるって話ですかね??

ちょっと疑問

JDKだけでXSLする場合、スタイルシートには

xalan:indent-amount="2"

という記述をすれば良いのですが、これって、内部的に同じ事してるんじゃないかな?と勘ぐってみたり。
でも、動かないのはなぜでしょう・・・??深追いはしませんけど(笑)


しかし、インデントをつける方法はいろいろあって、しかもノーマルなJDKにはまともに実装されていないなんて想像してませんでした。ここまでこの話題で引っ張るとも思わなかったし(笑)


ちょっとごちゃごちゃしちゃったんで、後で結果をまとめておきます。

続・JavaでXMLをフォーマットする

前回のエントリ(id:toolkit:20060719)での、id:suchiさんのコメントから、

LSSerializerのDOMConfigrationで ”format-pretty-print” をtrue

という方法を教わったので、早速検証してみます。

結果としては・・・ちょっと微妙な感じになったのでメモしておきます。*1

LSSerializerまでの長い道のり(笑)

LSSerializerはJ2SE 5.0でDOM Level3をサポートする事によって追加されたクラスのようです。
まず、LSSerializerはインターフェースです。なので、どうにかして実装を取ってこなければなりません。
DocumentBuilderFactoryみたいに、Factoryクラスがあるのかな?と思ったら、どうも見当たりません。
しょうがないので、Webをあさって、JavaDoc見て調べてみた結果、

  1. LSSerializerはDOMImplementationLS#createLSSerializer()で得られる。
  2. DOMImplementationLSはDocument#getFeature()で得られたオブジェクトをキャストする事によって得られる。

という結論になりました。

Document doc;
※docを作る
DOMImplementation domImpl = doc.getImplementation();
DOMImplementationLS domImplLS = (DOMImplementationLS) domImpl.getFeature("LS", "3.0");
LSSerializer lsSer = domImplLS.createLSSerializer();

ココまでくれば、

lsSer.getDomConfig().setParameter("format-pretty-print", Boolean.TRUE);

とすれば良い・・と思います。

コードにしてみました。

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSOutput;
import org.w3c.dom.ls.LSSerializer;

public class LSSerializerTest01 {
	public static void main(String[] args) throws Exception {
		// フォーマットしたいXML
		File inXml = new File("in.xml");
		// フォーマットしたXML
		File outXml = new File("out.xml");

		// フォーマットしたいXMLのDOMオブジェクトを作る
		DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
		DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
		InputStream in = new FileInputStream(inXml);
		Document doc = docBuilder.parse(in);
		in.close();

		// DOMImplementationLSを取得する
		DOMImplementation domImpl = doc.getImplementation();
		DOMImplementationLS domImplLS = (DOMImplementationLS) domImpl.getFeature("LS", "3.0");

		LSOutput lsOutput = domImplLS.createLSOutput();
		LSSerializer lsSer = domImplLS.createLSSerializer();
		OutputStream out = new FileOutputStream(outXml);
		lsOutput.setByteStream(out);
		
		// LSSerializerのDOMConfigurationにパラメータをセットする
		lsSer.getDomConfig().setParameter("format-pretty-print", Boolean.TRUE);
		lsSer.write(doc, lsOutput);
		out.close();
	}
}


で、実行してみると・・ありゃ、Exception発生。

Exception in thread "main" org.w3c.dom.DOMException: FEATURE_NOT_SUPPORTED: The parameter format-pretty-print is recognized but the requested value cannot be set.
at com.sun.org.apache.xml.internal.serialize.DOMSerializerImpl.setParameter(DOMSerializerImpl.java:267)


なんと!そのパラメータ(format-pretty-print)は認識できるけど、セットできませんと怒られてしまいました(笑)
うーん、今ひとつ釈然としない。メインのソースが悪いのかな・・それともOSXだから??


もう、よくわかんないので、Xercesの最新版をゲットして、パスを通して実行してみると・・・うまく行くではないですか(笑)。*2


Documentオブジェクトの実装は

org.apache.xerces.dom.CoreDOMImplementationImpl

です。


JDKの場合は

com.sun.org.apache.xerces.internal.dom.CoreDOMImplementationImpl

でした。

ってことは、JDKに入っているDOMの実装が古い(?)のかも。

ちなみにJAXPのパッチがあがっていました。

2006/07/13のコメントで、

this will be fixed in Mustang b92.

とあるから、JDK6でサポートされるのかな??

Xercesでもサポートされたのは最近みたい。(2.8からか?)

Implemented the DOM Level 3 Load and Save format-pretty-print parameter.

*1:検証するにあたって、http://jx-study.net/D3pF/D3p.htmlを参考にさせてもらいました。

*2:XMLパーサの指定にXercesを使うという事は特にやっていなくて、単にパスを通しただけです。

JavaでXMLをフォーマットする

とある仕事で、XMLにインデントをつけてを整形する必要があった。
多分、書き出す時にでもオプションがあるんだろう・・とタカをくくっていたが、それは大間違いであった(笑)

いろいろ調べてみて、一番手っ取り早い方法は、XSLTを使えと、そういう事らしい。

まずは、こんなXSLを作る。

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	xmlns:xalan="http://xml.apache.org/xslt">
	<xsl:output method="xml" encoding="UTF-8"
		indent="yes" xalan:indent-amount="2"/>
	<xsl:template match="/">
		<xsl:copy-of select="."/>
	</xsl:template>
</xsl:stylesheet>


xmlns:xalanとかあって、何となく気持ち悪いけど、気にしない(笑)
あとは、これをXMLに適用するだけ。簡単です。
ちなみに、OSX 10.4 で、java version "1.5.0_06"です。J2SE1.4でもOKだと思いますし、J2SE1.3でもXMLパーサが入っていればOKだと思います。(いい加減ですんません。)

public static void main(String[] args) throws Exception {
	// フォーマットしたいXML
	File targetXml = new File("target.xml");
	// インデントをつけるスタイルシート
	File xsl = new File("style.xsl");
	// フォーマットしたXML
	File outXml = new File("out.xml");

	// フォーマットしたいXMLのDOMオブジェクトを作る
	DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
	DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
	Document doc = docBuilder.parse(new FileInputStream(targetXml));

	// Transformerを用意する。
	StreamSource xslSource = new StreamSource(xsl);
	TransformerFactory transFactory = TransformerFactory.newInstance();
	Transformer transformer = transFactory.newTransformer(xslSource);

	//XSLを適用してoutXmlに書き出す。
	DOMSource source = new DOMSource(doc);
	Result result = new StreamResult(new FileOutputStream(outXml));
	transformer.transform(source, result);
}

XSLを適用するだけですが、こんなにめんどくさいコードになります。(笑)

すべてのページを変換する

ページを指定して描画するためには、

context = CGBitmapContextCreateWithColor ( width, height, colorSpace, ( 0, 0, 0, 0 ) )
context.drawPDFDocument ( r, pdf, page )

のように、drawPDFDocumentでページ数を渡してあげればOKです。

すべてのページに対してこのような処理をするためには、PDFの総ページ数を拾ってくる必要があります。

numberOfPages = pdf.getNumberOfPages()

これをループでまわしてあげるとこうなります。

004test.py

from CoreGraphics import *

# 入力ファイル
input = "in.pdf"
# 出力ファイル
output = "out%03d.jpg"

# 拡大率を設定
ratio = 3.0
transform = CGAffineTransformMakeScale( ratio , ratio )

# カラースペースはRGBで指定
colorSpace = CGColorSpaceCreateDeviceRGB ()

# PDFを読み込み
pdf = CGPDFDocumentCreateWithProvider ( CGDataProviderCreateWithFilename ( input ) )
# 総ページ数を取得
numberOfPages = pdf.getNumberOfPages()

# すべてのページに対して処理
for page in range ( 1, numberOfPages + 1 ):
    # ページのRectを取得
    rect = pdf.getMediaBox( page )
    # 拡大処理
    rect = rect.applyAffineTransform( transform )
    # ページの幅と高さを取得
    width = long( rect.size.width )
    height = long( rect.size.height )
    # ビットマップコンテキストを用意
    context= CGBitmapContextCreateWithColor ( width, height, colorSpace, ( 0, 0, 0, 0 ) )
    # 指定ページを描画
    context.drawPDFDocument ( rect, pdf, page )
    # ファイルに書き出し
    context.writeToFile ( output%page , kCGImageFormatJPEG )

コメントをつけて、変数名もすこしマトモなものにしました(笑)。

一番最後に

output%page

としていますが、こうする事によって

  • out001.jpg
  • out002.jpg

の様に連番が振られたファイルが生成されます。この辺はJavaと違って便利ですね。

forループの書き方とかPython初心者なんで見よう見まねで書いたんですがこんなもんでしょうか??

とりあえず、ここまででPDFをJPEGに変換すると言う野望(?)は果たされました。あとはこのスクリプトをもう少し汎用的に使いやすくする事をやってみたいと思います。

大きさを変えてみる

前回のエントリでJPEGのサイズを決めうちにしてました。これじゃまずいので、PDFのページサイズを取得してJPEGのサイズを決めたいと思います。

ページサイズは

getMediaBox(page)

というメソッドで取得できます。
とりあえず、こんなスクリプトになりました。

002test.py

from CoreGraphics import *

colorSpace = CGColorSpaceCreateDeviceRGB ()
pdf = CGPDFDocumentCreateWithProvider ( CGDataProviderCreateWithFilename ( "in.pdf" ) )
r = pdf.getMediaBox( 1 )

width=long( r.size.width )
height=long( r.size.height )

ctx = CGBitmapContextCreateWithColor ( width, height, colorSpace, ( 0, 0, 0, 0 ) )
ctx.drawPDFDocument ( r, pdf, 1 )
ctx.writeToFile ( "out.jpg", kCGImageFormatJPEG )

pdf.getMediaBox(1)は1ページ目のサイズをCGRectとして取得しています。これは矩形を表現するもののようです。
縦/横の実際の長さは、size.width、size.heightとして取得すればOKです。

上のスクリプトではwidth、heightをlongにキャストしていますが、これはCGBitmapContextCreateWithColorの引数としてはlongの必要があるからです。(たぶんね)

ここまで解ってくると、次は拡大/縮小なんかがやりたくなってきますよね?(笑)これを実現するためには、CGAffineTransformを使います。

transform = CGAffineTransformMakeScale( xratio, yratio )

2倍の大きさに拡大したい時は、xratioとyratioに2を指定します。縦に2倍に引き延ばしたい場合はxratioに1を、yratioに2を指定します。お気軽ですねー。

で、こいつをさっきのスクリプトに加えてやるとこうなります。これは大きさを3倍に拡大したJPEGを生成します。

003test.py

from CoreGraphics import *

colorSpace = CGColorSpaceCreateDeviceRGB ()
pdf = CGPDFDocumentCreateWithProvider ( CGDataProviderCreateWithFilename ( "in.pdf" ) )
r = pdf.getMediaBox( 1 )

transform = CGAffineTransformMakeScale( 3.0 , 3.0 )
r = r.applyAffineTransform( transform )

width=long( r.size.width )
height=long( r.size.height )

ctx = CGBitmapContextCreateWithColor ( width, height, colorSpace, ( 0, 0, 0, 0 ) )
ctx.drawPDFDocument ( r, pdf, 1 )
ctx.writeToFile ( "out.jpg", kCGImageFormatJPEG )

いい感じです。ただこれでは1ページ目しか変換できませんので、次はすべてのページに対して処理してみたいと思います。

PyDEV

Pythonで開発を行うには、テキストエディタ一つあればそれだけでOKですが、せっかく普段Eclipseを使っているのでここは一つ、PyDEVというプラグインを使ってみます。(ミーハーですみません)
http://pydev.sourceforge.net/

ありがたい事に、EclipseのUpdate Managerからインストールできます。
http://pydev.sourceforge.net/download.html
インストールの手順等はあちこちで述べられているので、省略します。
自分の環境はOSX 10.4.2+Eclipse 3.1という構成です。

無事インストールとセットアップが終わると、PyDevのパースペクティブに切り替え・・・って、ないみたいです(笑)。
単に、拡張子が.pyで終わるファイルを右クリックすると、Run As メニューに Run Python が追加されます。これを選ぶとスクリプトが走ると言う事みたいです。


あとはEclipseなんで、ソースコード補完とか効きますけど、Quartz2Dに関してはパスが通っていない(?)のか、いまいち動きません。まぁ、補完が必要なほど大層なスクリプトは書きませんので、気にしない事にします。

前回のエントリに書いた、サンプルスクリプトをコピーして、実行してみます。

001test.py

# Example:
from CoreGraphics import *

ctx = CGBitmapContextCreateWithColor (612, 792,
CGColorSpaceCreateDeviceRGB (), (0, 0, 0, 0))

pdf = CGPDFDocumentCreateWithProvider (CGDataProviderCreateWithFilename ("in.pdf"))
ctx.drawPDFDocument (CGRectMake (0, 0, 612, 792), pdf, 1)
ctx.writeToFile ("out.png", kCGImageFormatPNG)

in.pdfという適当なPDFを用意して、Run AsからRun Pythonを選びます・・・

一瞬で終わり、in.pdfがout.pngというファイルにめでたく変換されました。


一応動いて一安心ですが、なんだかあっけなくてつまらないですねー。

では、試しに上記のスクリプトの一番に

ctx.writeToFile ("out.jpg", kCGImageFormatJPEG)

を追加して実行してみましょう。

またまた一瞬で終わり、out.jpgというファイルができました。


と言う訳で、もう目的はほぼ果たされちゃいました(笑)。

ただ、これだけではぜんぜん実用的じゃないので、大きさを変えてみたり、ページを指定して変換したりといろいろやってみたいと思います。

API-SUMMARY

ドキュメントのなかに、Python Bindings for Quartz 2Dでは、次のような感じでラップされていると書かれています。*1

For example, in Quartz 2D the code to draw an image is:
CGContextDrawImage (ctx, rect, image);
Using Python, the code is:
ctx.drawImage (rect, image);

なるほどー。わかったような、わからないような・・・(笑)

「サンプルがあるので見てね」と書いてあるので、そいつを見た方が早いでしょう。場所は/Developer/Examples/Quartz/Python/になります。

ざっとみて10個くらいサンプルがありますが、PDFをJPEGに変換するサンプルは残念ながらありません(あったらそもそもこんな日記は書かない・・ )。おしいのが、

pict2pdf.py

で、PICTファイルをPDFに変換すると言う、逆の処理です(笑)。

ところで、API-SUMMARYというファイルがサンプルのディレクトリの中に入っています。これはそのものずばり、PythonでのQuartz2D APIのまとめで、このファイルの最初の方に重要なコメントがあります。

# Example:
# from CoreGraphics import *
#
# ctx = CGBitmapContextCreateWithColor (612, 792,
# CGColorSpaceCreateDeviceRGB (), (0, 0, 0, 0))
#
# pdf = CGPDFDocumentCreateWithProvider (
# CGDataProviderCreateWithFilename ("in.pdf"))
#
# ctx.drawPDFDocument (CGRectMake (0, 0, 612, 792), pdf, 1)
# ctx.writeToFile ("out.png", kCGImageFormatPNG)

やってることは、BitmapContextを作って、PDFを読み込んで、BitmapContextにdrawPdfDocumentしてあげて、最後にwriteToFileでPNG形式で書き出してます。これをベースにスクリプトを書いていけば良い感じです。最初からこいつを見ときゃ良かった・・というわけで、このスクリプトを手がかりに進めていきたいと思います。