はじめに
前回は、葉書の宛名を横書きで改行候補で自動改行する方法で印刷しました。また、複数のベージを切り替えて確認でき、印刷位置の微調整もするプレビューのクラス「MultiPageDialog.java」も作りました。今回はこのプレビューを使って縦書きの印刷をします。
宛名を縦書きにするには、次の4つを考えます。4つ目は好みかと思います。
- 縦書きのための均等割付をつくる
- 縦書きのための自動改行を作る
- ハイフォンなど横書き縦書きで字形の異なる文字を自動で変換する
- 算用数字を漢数字に入れ替える
縦書きのための均等割付
横書きの左寄せ、センタリング、右寄せ・均等割付に対応するものをつくります。
横書きの時には、対象となる文字列を現在のフォントで描くときに必要な幅を求めるメソッドを使いましたが、縦書きにはありません。ただ、漢字やカナではすべての文字の高さが同じと仮定しても問題ないので指定の高さを文字数で割って切り捨てすれば、何文字描けるかはすぐに分かります。
英小文字ではaとbでは高さに違いがありますが、これは考慮しません。これはまた別の問題をはらんでいます。英文の住所なら、素直に横書きを使いましょう。数字の問題については漢数字にする部分で考察します。
クラス名は末尾にVerticalの意味でVを付けることにします。
/**
指定範囲に文字列を均等割付、上寄せ、下寄せ、センタリングなどの調整をして描画する
入り切らない場合は、入りきる分だけの部分文字列を保持して
均等割付、上寄せなどの指示により部分文字列を描画する
(2017-6-26 version 2.0)
(2019-11-28 version 3.0)
@author Adachi
@version 3.0
*/
import java.awt.Graphics2D;
import java.awt.FontMetrics;
public class AdjustStringV {
String str;
int nxtbgncpi;
float wdmm;
float remm;
float gapmm;
Graphics2D g2;
FontMetrics fm;
float mm2pt = 72/25.4f;
float pt2mm = 25.4f/72;
float ascent, mojih;
boolean debug = false;
/** areawidth に string を入れる時の余り remm を計算 */
public AdjustStringV(Graphics2D g2, String string, float areawidth) {
this(g2,string,areawidth,0);
}
/** areawidth に string を入れる時の余り remm を計算。
ただし str の begincpindexから */
public AdjustStringV(Graphics2D g2, String string, float areawidth,
int begincpindex){
this.g2 = g2;
fm = g2.getFontMetrics();
String zstr = (string!=null) ? string : ""; //nullなら""
wdmm = (areawidth>=0f) ? areawidth : 0f; //負なら0
int bgncpi = (begincpindex>=0) ? begincpindex : 0; //負なら0
int zcpct = zstr.codePointCount(0,zstr.length()); //zstrのcp数
if (zcpct>bgncpi){ //開始位置をindexに換算
int idxbgn = zstr.offsetByCodePoints(0,bgncpi);
str = zstr.substring(idxbgn);
}else{ //開始位置が文字列をはみ出していたら
str = "";
}
mojih = fm.getFont().getSize(); //①
ascent = mojih * 0.9f;
int maxcpct = (int)Math.floor(wdmm/(mojih*pt2mm));//②
int cpct = str.codePointCount(0,str.length());
if(maxcpct>cpct)maxcpct=cpct;
remm = wdmm - mojih*pt2mm*maxcpct;
int maxidx = str.offsetByCodePoints(0, maxcpct); //③
str = str.substring(0,maxidx);
nxtbgncpi = bgncpi + maxcpct; //④
if (nxtbgncpi>=zcpct) nxtbgncpi = -1;
gapmm = (maxcpct>1)? remm/(maxcpct-1):0;
if(debug){
String fmt = "str=%s remm=%f next=%d gap=%f\n";
System.out.printf(fmt,str,remm,nxtbgncpi,gapmm);
}
}
/** zstr のなかで書ききれなかった残りがあるかを答える */
public boolean hasNext(){
return nxtbgncpi>0;
}
/** 書ききれなかった残りの文字の開始位置(コードポイントで)を答える */
public int getNextPt(){
return nxtbgncpi;
}
/** strがareawidth のなかに収まるかを答える */
public boolean isFitInto(){
return remm>=0;
}
/** 文字列の幅を引いたあまりをmmで返す */
public float getRemain(){
return remm;
}
/** 文字間の隙間をmmで返す */
public float getGap(){
return gapmm;
}
/** 現在の文字列を返す */
public String getCurrentString(){
return str;
}
/**縦書きを左寄せのように上に詰める。hmを中心線にする
str,fm,g2,mm2pt,mojih,ascent*/
public void drawTop(float hm, float vm) {
int strlen = str.length();
int i=0;
int cpcti = 0;
int nexti;
float fontwd;
while (strlen>i){
nexti = str.offsetByCodePoints(i,1);
fontwd = fm.stringWidth(str.substring(i,nexti));
g2.drawString(str.substring(i,nexti),
hm*mm2pt-fontwd/2,
vm*mm2pt+mojih*cpcti+ascent);
i=nexti;
cpcti++;
}
}
/**縦書きを右寄せのように下に詰める。hmを中心線にする
str,fm,g2,mm2pt,mojih,ascent*/
public void drawBottom(float hm, float vm) {
int strlen = str.length();
int i=0;
int cpcti = 0;
int nexti;
float fontwd;
while (strlen>i){
nexti = str.offsetByCodePoints(i,1);
fontwd = fm.stringWidth(str.substring(i,nexti));
g2.drawString(str.substring(i,nexti),
hm*mm2pt-fontwd/2,
(vm+remm)*mm2pt+mojih*cpcti+ascent);
i=nexti;
cpcti++;
}
}
/**縦の均等割付 hmを中心線にする */
//str,fm,g2,mm2pt,mojih,ascent,gapmm
public void drawTtoB(float hm, float vm) {
int strlen = str.length();
int cpct = str.codePointCount(0,strlen);
if (cpct>1){
int i=0;
int cpcti = 0;
int nexti;
float fontwd;
while (strlen>i){
nexti = str.offsetByCodePoints(i,1);
fontwd = fm.stringWidth(str.substring(i,nexti));
g2.drawString(str.substring(i,nexti),
hm*mm2pt-fontwd/2,
(vm+gapmm*cpcti)*mm2pt+mojih*cpcti+ascent);
i=nexti;
cpcti++;
}
}else if(cpct==1){
g2.drawString(str,hm*mm2pt-fm.stringWidth(str)/2,
(vm+remm/2)*mm2pt+ascent);
}
}
/**縦の中央寄せ hmを中心線にする */
//str,fm,g2,mm2pt,mojih,ascent
public void drawMid(float hm, float vm) {
int strlen = str.length();
int i=0;
int cpcti = 0;
int nexti;
float fontwd;
while (strlen>i){
nexti = str.offsetByCodePoints(i,1);
fontwd = fm.stringWidth(str.substring(i,nexti));
g2.drawString(str.substring(i,nexti),
hm*mm2pt-fontwd/2,
(vm+remm/2)*mm2pt+mojih*cpcti+ascent);
i=nexti;
cpcti++;
}
}
}
何文字入るかの部分について若干の説明
何文字入るかの部分についてプログラム解説をしておきます。丸付き数字はプログラム中の部分を指します。
① 文字の高さを取得します
なぜこれを使うかは、後述の「文字の高さにまつわる問題」で詳しく述べます。
② 指定された幅と文字の高さから最大文字数を計算します
mojihをmm単位に換算して指定幅を割り、端数を切り捨てして最大文字数を計算します。もし元の文字列の文字数が小さければ、それを最大数とします。最大文字数から余る高さを計算しておきます。
③ 印字する文字列をstrに格納します
サロゲートペアを考慮して最大文字数の文字列を切り出してstrに代入しています
④ 書ききれなかった文字列の先頭番号を記憶します
3文字書いたときは3です。たとえば、"東西南北"で"東西南"まで書ける時は
str:"東西南" nxtbgncpi:3
となります。
先頭の文字の番号は0なので一文字も書けなかったときは0、全部書けて次に書くものがないときは-1です。
サロゲートペアが含まれていても文字数は同じです。ここはStringのindexとは異なります。
縦書きのための均等割付動作確認
このプログラムにはmain()を設けず、別に確認プログラムを作りました。
まずは、「何文字描けるか」の計算の部分です。横書きの場合と考え方が違いますから、動作確認は念入りに行います。中の文字列の"【丈`】"と"【魚花】"(注1)がサロゲートペアを使用する文字です。
注1
【丈`】は実際には一文字でU+2000bのことです。丈の上に犬のように点がついた文字ですが、それを丈にバッククォート文字を追加して2文字で表しています。【魚花】も実際には一文字でU+29e3dのことです。偏と旁をそれぞれ一文字で表して組み合わせています。普通にUTF-8でウェブページを書けば問題なく使用できる文字ですが、このCodeZineのサイトではCMSの仕様でサロゲートペアの文字が扱えないため、致し方なくこのような回避策をとっています。次のプログラムと実行結果は、正しい文字に訂正しないと書いたとおりにはなりません。
/**
AdjustStringV.java のCUI用動作確認プログラム
コンストラクタの部分のみ確認します。印刷は行いません。
@author Adachi
@version 1.0
*/
import java.awt.Graphics2D;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.image.BufferedImage;
public class TestASV {
public static void main( String[] args ) {
BufferedImage buffimg = new BufferedImage(400,300,BufferedImage.TYPE_INT_RGB);
Graphics2D g = buffimg.createGraphics();
g.setFont(new Font(Font.SERIF, Font.PLAIN, 11));
float wakuhaba = (args.length>0) ? Float.parseFloat(args[0]):10f;
int kaishiichi = (args.length>1) ? Integer.parseInt(args[1]):0;
String mojitachi="東西南北【丈`】【魚花】鱈鮭鰯AB"; //(注1)
System.out.println("文字列="+mojitachi+
" /枠幅(mm)="+wakuhaba+" /開始番号(0-)="+kaishiichi);
AdjustStringV asv = new AdjustStringV(g, mojitachi,wakuhaba,kaishiichi);
report(asv);
while(asv.hasNext()){
asv = new AdjustStringV(g, mojitachi,wakuhaba,asv.getNextPt());
report(asv);
}
}
public static void report(AdjustStringV asv ){
String str = asv.getCurrentString();
float remm = asv.getRemain();
int nxtbgncpi = asv.getNextPt();
float gapmm = asv.getGap();
String fmt = "str=%s remm=%f next=%d gap=%f\n";
System.out.printf(fmt,str,remm,nxtbgncpi,gapmm);
}
}
実行結果
実行時の第一引数が設定幅、第二引数が開始文字の番号です。0が最初からです。省略すると 10 0 と指定したことになります。
nextは文字列(charの配列)のindexではなく、見た目の文字数での次の開始番号で、全部書いてしまうと-1になります。
引数なしで10mm幅だと2文字ずつしか描けません。
注1 debian64:~$ java TestASV 文字列=東西南北【丈`】【魚花】鱈鮭鰯AB /枠幅(mm)=10.0 /開始番号(0-)=0 str=東西 remm=2.238889 next=2 gap=2.238889 str=南北 remm=2.238889 next=4 gap=2.238889 str=【丈`】【魚花】 remm=2.238889 next=6 gap=2.238889 str=鱈鮭 remm=2.238889 next=8 gap=2.238889 str=鰯A remm=2.238889 next=10 gap=2.238889 str=B remm=6.119444 next=-1 gap=0.000000
15mmにしてスタートを2からにします。南からの開始です。今度は3文字入ります。
注1 debian64:~$ java TestASV 15 2 文字列=東西南北【丈`】【魚花】鱈鮭鰯AB /枠幅(mm)=15.0 /開始番号(0-)=2 str=南北【丈`】 remm=3.358334 next=5 gap=1.679167 str=【魚花】鱈鮭 remm=3.358334 next=8 gap=1.679167 str=鰯AB remm=3.358334 next=-1 gap=1.679167
18mmで5文字目から。4文字ずつ入ります。
注1 debian64:~$ java TestASV 18 5 文字列=東西南北【丈`】【魚花】鱈鮭鰯AB /枠幅(mm)=18.0 /開始番号(0-)=5 str=【魚花】鱈鮭鰯 remm=2.477777 next=9 gap=0.825926 str=AB remm=10.238889 next=-1 gap=10.238889
Shift_JIS環境でも確認できるGUI版
この連載第4回でも紹介しましたGUI版を今回も作っておきました。Shift_JIS環境で保存してコンパイルできます。
/**
AdjustStringV.java のGUI用動作確認プログラム
コンストラクタの部分のみ確認します。印刷は行いません。
Shift_JIS環境でもGUI出力なら可能です。
Windowsの端末出力はShift_JIS出力のためうまくいきません。
@author Adachi
@version 1.0
*/
import java.awt.Graphics2D;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.image.BufferedImage;
import java.util.List;
import java.util.ArrayList;
public class TestASVonGUI {
public static void main( String[] args ) {
List<String> lines = new ArrayList<>();
BufferedImage buffimg = new BufferedImage(400,300,BufferedImage.TYPE_INT_RGB);
Graphics2D g = buffimg.createGraphics();
g.setFont(new Font(Font.SERIF, Font.PLAIN, 11));
float wakuhaba = (args.length>0) ? Float.parseFloat(args[0]):10f;
int kaishiichi = (args.length>1) ? Integer.parseInt(args[1]):0;
String jou = (new String(new int[]{0x2000b},0,1));
String hokke = (new String(new int[]{0x29e3d},0,1));
String mojitachi="東西南北"+jou+hokke+"鱈鮭鰯AB"; //(注1)
String s = "文字列="+mojitachi+" /枠幅(mm)="+wakuhaba+" /開始番号(0-)="+kaishiichi;
System.out.println(s);
lines.add(s);
AdjustStringV asv = new AdjustStringV(g, mojitachi,wakuhaba,kaishiichi);
s = report(asv);
System.out.println(s);
lines.add(s);
while(asv.hasNext()){
asv = new AdjustStringV(g, mojitachi,wakuhaba,asv.getNextPt());
s = report(asv);
System.out.println(s);
lines.add(s);
}
javax.swing.JOptionPane.showMessageDialog(
null,
String.join("\n",lines),
"一文字ずつ",
javax.swing.JOptionPane.PLAIN_MESSAGE
);
}
public static String report(AdjustStringV asv ){
String str = asv.getCurrentString();
float remm = asv.getRemain();
int nxtbgncpi = asv.getNextPt();
float gapmm = asv.getGap();
String fmt = "str=%s remm=%f next=%d gap=%f";
return String.format(fmt,str,remm,nxtbgncpi,gapmm);
}
}
実行すると端末のテキスト表示はサロケートペア文字は「?」となりますが、ダイアログでは正しい文字になります。表示内容はTestASV.javaと同一です。
印刷位置の指定方法
横書きで左寄せ・右寄せは縦書きでは上・下に寄せることに相当します。メソッド名を変えました。均等割付とセンタリングも合わせて変えてあります。
| 配置 (横書きでは) |
AjustString のメソッド |
AjustStringV のメソッド |
|---|---|---|
| 左寄せ | drawLeft() | drawTop() |
| 右寄せ | drawRight() | drawBottom() |
| 均等割付 | drawKintou() | drawTtoB() |
| センタリング | drawCenter() | drawMid() |
縦書きの場合は均等割付以外でも一文字ずつ書いていく必要があります。横書きでは自然にベースラインに沿って文字が並びますが、縦書きの場合は一文字ずつ文字の横幅の中心を求めてそれを揃えています。これをしないと、幅が異なる文字では右側が凸凹になります。
各メソッドとも引数には印字開始点のx,y座標(単位はmm)をいれます。横書きではベースラインの左端を指定し、そのままの位置から印字しました。縦書きの場合はx座標は文字の中心線、y座標は枠の上辺となるように指定し、実際の印字はxを文字幅の半分左、yをAscent分下に印字することになります。
Javaの文字の高さは英字中心に設計されていて、y,p,gなどの小文字はベースラインから下に出ます。ベースラインの上部分をAscent、下部分をDescentと呼んでいます。漢字は本来はAscent部分に配置されればよかったのかもしれません。しかし、当時の英字に合わせたシステムに高い解像度を必要とする漢字を入れるためにはAscent+Descentを使わざるを得なかったのだと思います。今は全体の解像度が高くなっているので、Ascent部分に配置することもできるはずですが、実際には中途半端な合わせ方になっています。ただし、フォントの設計思想に依ります。
Ascent、Descent、漢字の関係は図のようになっています。しかも英字のみを書く時と、漢字混じりで英字を書くときでは位置が微妙に変わる可能性があります。1つめのbyと2つめのbyが微妙に違っているのがそれです。
Ascent分だけ下げる必要があるというのは次の図です。枠の範囲に入るかどうかを計算するのは文字の高さですが、上下の調整は英字でなくてもAscentを使う必要があります。
文字の高さにまつわる問題
文字の高さはgetFont().getSize()の値を使うことにしました。その理由を説明しておきます。
まず、文字の高さはgetHeight()の値ではありません。getHeight()は隣接するテキスト行のベースラインの間の距離なので、行間が含まれてしまいます。つまり、Height=Ascent+Descent+Leadingなのです。そこで文字の高さはAscent+Descentで求められると考えました。これらの値は、FontMetricsクラスのメソッドで求めることができます。getAscent()とgetDescent()です。
fm = g2.getFontMetrics(); ... mojih = fm.getAscent()+fm.getDescent(); ascent = fm.getAscent();
ただ、これだと2つの問題があって、実際には次のようにしています。
fm = g2.getFontMetrics(); ... mojih = fm.getFont().getSize(); ascent = mojih * 0.9f;
つまり、フォントのサイズを流用しているというわけです。これはワープロで親しんでいるポイントの値になります。Ascentはフォントサイズを0.9倍して求めています。これはフォントにより多少異なるのですが、経験値です。
2つの問題のうち1つは、ある環境でLANDSCAPEにして印刷した時にgetHeight()やgetAscent()、getDescent()がどれも0になってしまうという不具合でした。プレビューではPORTRAITとLANDSCAPEの区別はないので問題は起こらず、印刷して初めて出現するという不具合です。
もう一つの問題は、Ascent+Descentが大きめに報告されるフォントがあることです。このために、狭い所に均等割付けで入れる場合、見た目で文字の上下に隙間があって3文字入りそうなのに2文字しか入らないということが起こります。
Ascent+Descentがフォントサイズと一致すると思いたいところですが、実際に調べると、Ascentだけでもフォントサイズより大きい場合があります。改行した時に前の行と少し離れるように大きくしている可能性もありますが、それはLeadingの役割だったはずです。これも調べてみると、Leadingの値はほとんどの場合0です。getSize()ではフォントによっては実際の高さより小さめになる可能性がありますが、字が密着してもいいから詰め込みたい場合もありますし、多用する均等割付けでは幅を十分にとれば文字は均等に離れるので良しとします。ただし均等割付以外では文字間隔が狭くなってしまうかもしれません。
