Itext8( Java )で文字のシェイプをPDF化
Itext8を使って、Javaでいわゆる”ワードアート”的な文字シェイプを作ってPDF化しようとした際の話です。
従来(Itext5)での手法
Itext5では、PdfGraphics2Dクラスがあり、これを使うことで任意の文字のシェイプをPDFに埋め込むことができました。しかしバージョン7以降ではこのPdfGraphics2Dが存在しないため、この方法はつかえません。
ちなみにその処理は次のようなものでした。
またPdfContentByteクラスも、現行バージョンでは存在しません。
itext5(old).java
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.RenderingHints;
import java.io.FileOutputStream;
import com.itextpdf.awt.FontMapper;
import com.itextpdf.awt.PdfGraphics2D;
import com.itextpdf.text.Document;
import com.itextpdf.text.PageSize;
import com.itextpdf.text.pdf.BaseFont;
import com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.PdfWriter;
public class Itext5Test {
public static void main(String[] args) {
addShapeObject("テスト", "c:\\test.pdf");
}
public static boolean addShapeObject(String strShape, String strPdfPath) {
Document doc = new Document();
doc.setPageSize(PageSize.A4);
doc.setMargins(10,10,10,10);
PdfContentByte cb;
PdfWriter pw;
if(strShape == null || strShape.equals("")) {
return false;
}
// 出力ファイルオープン
try {
pw = PdfWriter.getInstance(doc, new FileOutputStream(strPdfPath));
} catch (Exception e) {
e.printStackTrace();
return false;
}
// ドキュメントオープン
doc.open();
// PdfContentByte取得
cb = pw.getDirectContent();
// PdfContentByteの状態を記憶
cb.saveState();
PdfGraphics2D g2 = new PdfGraphics2D(cb,doc.getPageSize().getWidth(),doc.getPageSize().getHeight(), new PdfFontMapper());
// awt.Font
Font font = new Font("MS ゴシック", Font.PLAIN, 20);
g2.setFont(font);
g2.setStroke(new BasicStroke(1));
g2.setColor(Color.BLACK);
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
//通常Itextのy軸は下が0で上に向かって数値が増えますが、このメソッドでは上が0となります
g2.drawString(strShape,100,100);
g2.dispose();
// PdfContentByteの状態を戻す
cb.restoreState();
//ドキュメントクローズ
doc.close();
return true;
}
}
class PdfFontMapper implements FontMapper{
@Override
public BaseFont awtToPdf(Font arg0) {
// AwtフォントをItextのBaseFontに変換するマップです
// ここでは全てのフォントをMSゴシックに結び付けます
try {
// ttcはファイルパスの後 ,インデックス でフォントを指定します
// インデックスはWindowsからWクリックでフォントをプレビューした時のページIndexと結びつくようです
return BaseFont.createFont("C:\\Windows\\Fonts\\msgothic.ttc,0",BaseFont.IDENTITY_H,BaseFont.EMBEDDED);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@Override
public Font pdfToAwt(BaseFont arg0, int arg1) {
return null;
}
}
PdfCanvasで直接線画可能
あとから気づいたのですが、以前 PdfContentByte と PdfGraphics2D 使って行っていた文字列出力を、新しいPdfCanvasクラスを使って処理ことができるようです。
前例において PdfGraphics2D を使って文字列を出力していた部分は (従来から)PdfContentByte だけでも行うこともでき、その機能が PdfContentByte の代替である PdfCanvas に備わっていました
PdfCanvas のインスタンスの取得の仕方ですが、たとえばコンストラクタに PdfDocument から生成した PdfPage を渡して初期化します。
pdf-canvas-sample.java
...
// PDFドキュメントの生成
PdfDocument pdfDoc = new PdfDocument(new PdfWriter("c:\\test.pdf"));
// 新しいページを作成
PdfPage newPage = pdfDoc.addNewPage(PageSize.A4.rotate());
// PDFCanvasを生成
PdfCanvas canvas = new PdfCanvas(newPage);
...
このあと紹介するコードにおいて注意が必要な点は、フォントを PdfFont でセットする必要がある点です。PdfFontFactory.createFontはフォント取得失敗するとエラーをスローします。また、java.awt に所属するクラスと同名のクラスが多く存在しますので、importを定義する際には気を付けてください。
Javaでのフォントの取り扱いに関しましては別途記事を設けましたので、そちらも参考にしていただければと思います。
変形や回転を適用させるには、com.itextpdf.kernel.geom.AffineTransformのインスタンスを PdfCanvas の concatMatrixに渡します。
canvasに設定するフォントや、色、回転等は記憶されます。色などは後から設定の上書きが可能ですが AffineTransform に関しては常に追加となります。そのため、一度生成した AffineTransform のインスタンスから createInverse メソッドを呼ぶことで打消しの変換を作成してそれを適用させて元に戻します。
また、PdfCanvas にはそのような状態を記憶する saveState と記憶した状態に戻す restoreState があるのでこちらを使うことで設定を元に戻す方法もあります。
この機能を使ってブランチでは初期状態を記憶し、戻るときに状態をレストアするのが間違いが起こりにくいと思います。
use-canvas.java
public boolean makeShape2(String str, PdfCanvas c) {
// canvasの状態を記憶
c.saveState();
// フォントとフォントサイズの設定
try {
c.setFontAndSize(PdfFontFactory.createFont("C:\\Windows\\Fonts\\hgrhr8.ttc,0",PdfEncodings.IDENTITY_H),25);
} catch (IOException e) {
e.printStackTrace();
c.restoreState();
return false;
}
// Shapeにつける色(itextpdf.kernel.colors.Color)
com.itextpdf.kernel.colors.Color colorItext = new DeviceRgb(0,0,0);
com.itextpdf.kernel.colors.Color colorItext2 = new DeviceRgb(255,255,80);
//ストロークのサイズを調整したい時
c.setLineWidth(0.5f);
// ストロークの色
c.setStrokeColor(colorItext);
// 塗りつぶしの色
c.setFillColor(colorItext2);
// テキストのFillStrokeを指定
c.setTextRenderingMode(PdfCanvasConstants.TextRenderingMode.FILL_STROKE);
// 回転
com.itextpdf.kernel.geom.AffineTransform at = com.itextpdf.kernel.geom.AffineTransform.getRotateInstance(Math.toRadians(45));
c.concatMatrix(at);
//文字列の出力
c.beginText();
//出力地点へ移動
c.setTextMatrix(300,200);
//出力
c.showText(str);
c.endText();
// 回転を戻す
try {
c.concatMatrix(at.createInverse());
} catch (NoninvertibleTransformException e) {
e.printStackTrace();
}
// 四角形の線画
c.roundRectangle(300, 200, 100, 100, 0.5);
c.fillStroke();
//状態を記憶した状態に戻す
c.restoreState();
}
PdfCanvasConstants.TextRenderingModeの値とクリッピング
テキストの線画時に利用する PdfCanvasConstants.TextRenderingMode の定数値ですが、FILLやFILL_STROKE、STROKE、FILL_CLIP、FILL_STROKE_CLIP、STROKE_CLIP、CLIP、INVISIBLEといった種類があります。
ここで説明するまでもないのかもしれませんが、STROKEはアウトラインの線画で、FILLはその内側を塗る処理です。
CLIP の接尾はクリッピングを意味するもので、canvasで任意のパスを線画した後 clip ( と endPath ) を呼んだ場合ど同様の挙動をし、文字列がクリッピングパスとして設定されます。
クリッピングパスを設定した場合そのエリアから外は線画されないので、文字の上半分だけ塗りつぶしたい場合などに利用できると思います。
CLIP のみの定数は、文字の出力はせずにクリッピングパスだけ設定します。
一度セットしたクリッピングパスを元に戻すには saveState と restoreStateを使うしかなさそうです。
話はそれますが、クリッピングを利用する際 endPath(現在のパスの破棄) を呼んだつもりで closePath(パスの始点と終点を結ぶ) を呼んでいると意図しない挙動に悩まされます。
他のグラフィックツールと同様にendPathを呼んだあと再度パスを作成しclipすると、2つのパスが重なりあった部分だけが線画対象となります。
INVISIBLE は検索や注釈、その他のギミック用に非表示のテキストを挿入できます。
アフィン変換
もう一つ補足を加えますと、PdfCanvas の concatMatrixメソッドは既存のアフィン変換を、引数として渡された変換と合成するものです。AffinTranfFormインスタンスを受け取るほか、 concatMatrix(a,b,c,d,e,f) という double 値受け取ることもできます。a~fはこれは 2Dアフィン変換の行列を意味します。
Itextにおけるa~fの引数は、アフィン変換行列に次のように当てはめられているようです。e,fの部分はe→x,f→yに置き換えられているメソッドもありました。
a | c | e |
b | d | f |
0 | 0 | 1 |
このような形状の配列になっているのは処理上、先の配列に
x |
y |
1 |
という配列の形にした入力値を掛け算をすることで、変換適用後の配列
x' |
y' |
1 |
が得られれるからです。配列の掛け算を展開して主要な部分を抽出すると「 x'= ax + cy + e 」 、「 y'= bx + dy + f 」という計算になります。
このa~fの値を指定する事で、平行移動、拡大・縮小・回転・せん断(ゆがませる)変換を指定することができます。
X軸:Tx、Y軸:Ty分の平行移動は次のように表せます。
1 | 0 | Tx |
0 | 1 | Ty |
同様に、X:Sx、Y:Syの拡大縮小。
Sx | 0 | 0 |
0 | Sy | 0 |
原点を基準にした回転は次のようになります。
cos(θ) | -sin(θ) | 0 |
sin(θ) | cos(θ) | 0 |
サイン、コサインの計算はMath.sin Math.cos メソッドがありますが、ここに渡す角度はラジアンである必要があるので、「度数」を指定したい場合は「 Math.toRadians 」で「度」を「ラジアン」に変換すると便利です。
このような配列として処理する事で複数の変換をまとめることもできます。詳しく知りたい方は、イメージングソリューション「アフィン変換」の記事がわかりやすかったです。
似たようなメソッドに setTextMatrix というメソッドも存在します。これは指定した座標に移動する引数x,yをとるもののほか、a,b,c,d,x,yのアフィン配列や、AffineTransformインスタンスを受けるものがありますが、これらにセットする変換データは beginTextの後に呼び出すもので、endTextを呼ぶとクリアされるようです。
Java(awt)で文字をShape化
PdfCanvasで直接テキストを出力できることを知らずに、java.awtでShapeを作って、Itextに渡す方法を以前には行っていたのでこちらの方法も残しておきます。
とりあえず、awtで文字列をShape化します。
make-shape.java
import java.awt.Font;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
public class Itext7Test {
public static void main(String[] args) {
System.out.println(makeShape("test"));
}
public static Shape makeShape(String str) {
// フォント
Font font = new Font("MS ゴシック", Font.PLAIN, 14);
// フォントのレンダリングコンテキストを取得
FontRenderContext frc = new FontRenderContext(null, true, true);
// テキストのレイアウトを作成
TextLayout textLayout = new TextLayout(str,font, frc);
// シェイプを取得
return textLayout.getOutline(null);
}
...
awtでShapeの生成するのはさほど難しくありませんでした。
このShapeをItextに渡す場合は、Shapeのパス情報を分解してひとつずつItextに渡す必要があるようです。chatGPTを参考に次のようなコードを作成しました。
パスにおける数値(LineToに渡す値)には単位がありません。コンテキストにより解釈され適切な単位に変換されます。
make-shape.java
...
public static void drawShapeToPdfCanvas(Shape shape, PdfCanvas c) {
// java.awt.geom.PathIterator
PathIterator pathIterator = shape.getPathIterator(null);
float[] coords = new float[6];
while (!pathIterator.isDone()) {
int type = pathIterator.currentSegment(coords);
switch (type) {
case PathIterator.SEG_MOVETO:
c.moveTo(coords[0], coords[1]);
break;
case PathIterator.SEG_LINETO:
c.lineTo(coords[0], coords[1]);
break;
case PathIterator.SEG_QUADTO:
c.curveTo(coords[0], coords[1], coords[2], coords[3]);
break;
case PathIterator.SEG_CUBICTO:
c.curveTo(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]);
break;
case PathIterator.SEG_CLOSE:
c.closePath();
break;
}
pathIterator.next();
}
// Shapeにつける色(com.itextpdf.kernel.colors.Color)
Color colorItext = new DeviceRgb(0,0,0);
// ストロークのサイズを調整したい時
// c.setLineWidth(5f);
// ストロークの色
c.setStrokeColor(colorItext);
// 塗りつぶしの色
c.setFillColor(colorItext);
//fillとstroke別々に処理すると先に実行した方しか採用されません
c.fillStroke();
//c.fill();
//c.stroke();
}
...
AffineTransform
このままでは問題が発生します。awtにおけるShapeのパスは原点が左上なのに対して、Itextでは原点が左下となります。そのためそのまま実行すると上下反転した文字が生成されてしまいます。
そこでAffineTransformを利用してShapeを上下反転させます。
make-shape.java
...
public static Shape revarseShape(Shape shape) {
AffineTransform at = AffineTransform.getTranslateInstance(0, shape.getBounds2D().getCenterY());
at.concatenate(AffineTransform.getScaleInstance(1,-1));//上下反転
at.concatenate(AffineTransform.getTranslateInstance(0, -1 * shape.getBounds2D().getCenterY()));
return at.createTransformedShape(shape);
}...
AffineTransformのscaleに1,-1を渡すと上下反転の変換を行えます。そのまま実行すると折り返しの基準は座標系の原点(0,0)になるので、一旦translateでShapeのy軸の中心を反転地点を移動させることでその場で上下の反転をします。その後位置を元に戻します。
注意点としては concatenateメソッドは後に設定した変換から適用されていくことです。
AffineTransformのcreateTransformedShapeに元のShapeを渡すことで定義した変換を適用させたあとのShapeが得られます。
さらに、指定位置に配置する変換を加え次のようにすることで無事Itext8で文字列をShape化して配置することができました。
pdf-canvas-sample.java
...
// 意図した場所へ配置
public static Shape moveShape(Shape shape,float fltX, float fltY) {
AffineTransform at = AffineTransform.getTranslateInstance(-1 * shape.getBounds2D().getX()+fltX, -1 * shape.getBounds2D().getY()+fltY);
return at.createTransformedShape(shape);
}
// PDFを作成する
public static void makePdf() throws IOException {
float fltX = 50; // X座標
float fltY = 50; // Y座標(上から)
// PDFドキュメントの生成
PdfDocument pdfDoc = new PdfDocument(new PdfWriter("d:\\test.pdf"));
// 新しいページを作成
PdfPage newPage = pdfDoc.addNewPage(PageSize.A4.rotate());
// PDFCanvasを生成
PdfCanvas canvas = new PdfCanvas(newPage);
// 基本のシェイプを生成
Shape shape = makeShape("シェイプテスト");
// 上下反転
Shape rShape = revarseShape(shape);
// 上からのY座標、Itextに合わせて下からの位置に変換
fltY = (float)(newPage.getPageSize().getHeight() - fltY - rShape.getBounds2D().getHeight());
// 配置
Shape mShape = moveShape(rShape,fltX,fltY);
// 線画
drawShapeToPdfCanvas(mShape, canvas);
// PDFをクローズ
pdfDoc.close();
}...
こちらの方法の利点は、PdfFontを利用しなくて済むことぐらいでしょうか。
トラッキングの設定
文字間隔を設定するトラッキング(カーニングは特定の文字間の文字間隔のことを言うそうです) を設定したい場合は、AttributedSringを使って次のようにするとよさそうでしたが、うまく反映されませんでした。
...
AttributedString attributedString = new AttributedString(mStrBody);
attributedString.addAttribute(TextAttribute.FONT, font);
attributedString.addAttribute(TextAttribute.TRACKING, 1);
TextLayout textLayout = new TextLayout(attributedString.getIterator(), frc);
...
結局、FontからGlyphVectorを取得して、直接X軸を操作することで実現させました。
...
//FontからGlyphVectorを取得
GlyphVector gv = font.createGlyphVector(frc, "文字列"); // 文字列からGlyphVectorを生成
//次の文字に対して指定をするので-1 gv.getNumGlyphs()-1
float fltTracking = 0;
for (int i = 0, max = gv.getNumGlyphs()-1; i < max; i++) {
fltTracking += 1;
gv.setGlyphPosition(i+1, new java.awt.geom.Point2D.Float((float) gv.getGlyphPosition(i+1).getX()+fltTracking, 0));
}...
細かな調整がしたい場合は、Path2D.Doubleに個別に生成したshapeを積み上げていく方法もあるようです。
...
Path2D.Double mixShape = new Path2D.Double();
mixShape.append(shape,false);
...
広告