縦書きのための文字の変換を考える
すでにやってみて明らかですが、大部分の文字は変わりません。並べ方が縦になるだけです。均等割付で一文字ずつ場所を指定することはできているので、簡単に並びます。ただしいくつかの文字は考慮が必要です。
- ハイフォンと長音符などは90度回転が必要です。カッコやかぎかっこがこれにあたります。
- 句読点(、。)やひらがなカタカナの小文字(ゃゅょぁぃぅぇぉ)などは場所の移動が必要です。
- 漢数字にするかどうかを考えます。
今回の課題は宛名なので、一般の文章より対処の範囲は限定されると期待できます。たぶん次の例で全部だと思います。
- ハイフォンと長音符は縦になったフォントを使えばなんとかなりそうです。
- カタカナの小文字は厳密には右に寄らなければなりませんが、このままでも許容範囲のような気がします。
- 半角の隙間が全角になるのも許容できると思います。
数字はやはり微妙です。縦棒と数字の1が並ぶと見づらいですが、漢数字の一二三が縦に並ぶとなかなか苦しいものがあります。従来のワープロソフトでは2、3桁の数値は左右にはみ出しても横に並べるということをしていました。4桁もあるでしょうし半角英字との組み合わせも考えられます。B、Cのどちらかということにします。
今回の対応は、ここまでにしておきますが、もしこれが一般の文章なら、もっと考慮すべき文字は多くなります。例えば、
意外と簡単にできてしまいます。これは、90度回転したフォントがUNICODEに登録されているからです。けれども、JIS X 0213にはこの回転文字はありません。日本のコンピュータも内部の文字コードはUNICODEを採用しているとはいえ、全部の文字を登録しいるわけではありません。どこまでのフォントを収録するかの判断基準としてJIS X 0213は大きな手がかりであるはずですが、それに入っていないものは省略される可能性が高くなります。この判断はフォントごとに異なります。
Webブラウザは全世界のページを表示する可能性が高いので、複数のフォントファイルから登録してある文字を探しているようです。E の「」や()の縦の文字を表示するために、ページのソースには︶などと書いていますが、表示されていると思います。
この︶のあたりの文字コード表のスクリーンショットを出しておきます。Windows 10のものです。Officeを入れているかどうかで変わる可能性もあります。四角の中にFE1Aなどと見えるのはそこにグリフがないことを示しています。DebianのWheezy(Linux)ではFE20の行にもすべてフォントがあります。
この中でJIS X 0213にあるのは、水色の背景の2文字だけです。Unicodeでは sesami dot とう名前で、日本語では傍点で使われます。
ハイフォンと長音符の総覧
宛名に出てくる横長の線は、ハイフォンと長音符だけで間に合うはずですが、他の方からデータをいただいたりすると、全角のハイフォンと半角の長音符が混ざっている可能性があります。そこで文字コード表から横長の線に見えるものを拾ってみます。さすがに漢数字の一は省きました。長音符の違いがはっきりするように明朝体系で見てみます。先頭の四の字はフォントの位置の確認のためです。
一番上のU56DBはU+56DBのことで四のコードポイント(つまりJavaの採用しているもの)です。次のU002DつまりU+002DはASCIIの2Dにあたります。ASCIIの時代は文字数が限られていて、これがハイフォンとマイナス符号を兼ねていました。UnicodeではハイフォンがU+2010、マイナス符号がU+2212と別々に追加登録されています。en-dash、em-dashはそれぞれn幅とm幅のダッシュです。box...は日本語で罫線線素とよばれるもので、罫線を文字で構成するために用意している部品です。隣のフォントと繋がるように作っています。U+30FCが長音符です。ここまではJISX0213にあるもので、一番下にJ213Cと書いてあるのがその漢字用8ビット符号の値です。
U+FF0Dは旧の全角ハイフォンで、U+FF70は半長音符です。両方とも互換性の確保のためにUnicodeに残されていますが、JISX0213にはないのです。現在、全角のハイフォンはJISの8ビット符号で213Dで、対応するUnicodeの文字はem_dashです。半長音符は長音符と区別せずに213Cです。半角カナが使われなくなることでこれも使われなくなったはずですが、ASCIIのハイフォンと間違えて入力されているものが多いと考えられます。たとえ間違って入力されたとしても、縦の線として印刷されることを期待されていると考えられますから、考慮します。
これらには描画される高さの差はありますが、縦にするときには長いものと短いものの2つがあれば十分と考えられます。
縦線を表す文字の総覧
では次に縦の線に見える文字です。数はそう多くない上に、フォントにより、ないものもあります。
一応説明しておきますと、
- U+007C はASCIIの縦棒(vartical line)です
- U+2502とU+2503は罫線線素の縦棒です。隣のフォントと繋がるように作っていて長いです
- U+4E28 は漢字の扱いでserifもついていて、縦の長音符だろうと思います
JIS X 0213に登録されているのはここまでです。
- U+01C0 はlatin letter dental clickとあります。短めの縦棒を探して見つけたものですが、登録されているフォントは少なめです。後で名前の由来を調べましたら、南部アフリカで使われる舌打ちに近い音を表す文字のうちの一つだそうです
- U+FE31 em-dashの縦版です。U+FE32 は en-dashの縦版です。これは名前からして多くのフォントに登録されていてもいいと思うのですが、ないものもたくさんあります。JISにないので仕方がないかもしれません
- U+FE33 vertical low line です。アンダーラインなのでこれは位置が真中でありません
JISにない縦書き用の文字は印字する時に90度回転させて使うことを考えているようです。Javaでもできないことはありませんが、位置合わせが煩雑になりそうなので今回は採用しません。
ハイフォンと長音符の変換の結論
ということで、Linux, Windows共、IPAex明朝フォントをインストールして、短いものは Vertical En Dash、長いものは Vertical Em Dash 、長音符は CJK UNIFIED IDEOGRAPH U+4E28 を使うことにします。
| 変換前の文字 | 変換先の文字 | 安全策 | |||||
|---|---|---|---|---|---|---|---|
| ASCIIハイフォン | U+002D | - | ︲ | U+FE32 | ver. en_dash | | | U+007C (ASCIIの 縦棒) |
| ハイフォン | U+2010 | ‐ | |||||
| en_dash | U+2013 | – | |||||
| マイナス記号 | U+2212 | − | |||||
| em_dash | U+2014 | — | ︱ | U+FE31 | ver. em_dash | ||
| 全角 - | U+FF0D | - | |||||
| 半角長音符 | U+FF70 | ー | U+FE32 または U+30FC | ||||
| 長音符 | U+30FC | ー | 丨 | U+4E28 | CJK IDEOG. | 丨 | U+4E28(同左) |
IPAex明朝フォントがない時や毛筆体を使いたいなど、短い線の文字がない場合の安全策としてはASCIIの縦棒をつかうように書きましたが、実際に使ってみると長すぎて美しくありません。下の「グリフがない時の対策」でASCIIの縦棒を小さいフォントにして代用するようにさらに改良します。
文字を置換するプログラム
staticなメソッドだけのクラスにします。privateのコンストラクタでインスタンスを作れないようにし、static初期化ブロックを使って<String,String>のMapを定義しています。今の所Charしか扱っていませんが、サロゲートペアでも使えるようにStringにしました。hyphmapはハイフォンや長音符のみで、numtoomapはそれに数字を漢数字にするデータを加えています。
数字は十や百について触らずに、0を〇にすれば意外とうまく変換できます。
import java.util.*;
public final class ExchChar {
private ExchChar() {}
private static Map<String, String> hyphmap = new HashMap<>();
static {
String ASCII_HYP = "\u002D"; //"-" HYPHEN-MINUS(半角-) 3-2231
String HYPHEN = "\u2010"; //"‐" HYPHEN 3-213E
String EN_DASH = "\u2013"; //"–" EN DASH 3-237C
String EM_DASH = "\u2014"; //"—" EM DASH 3-213D
String MINUS_SIGN = "\u2212"; //"−" MINUS SIGN 3-215D
String BOX_LT_H = "\u2500"; //"─" BOX DRAWINGS LIGHT HORIZONTAL 3-2821
String BOX_HV_H = "\u2501"; //"━" BOX DRAWINGS HEAVY HORIZONTAL 3-282C
String CHOONFU = "\u30FC"; //"ー" PROLONGED SOUND MARK(長音符) 3-213C
String ZEN_HYP = "\uFF0D"; //"-" FULLWIDTH HYPHEN-MINUS(全角マイナス)
String HAN_CHO = "\uFF70"; //"ー" HALFWIDTH PROLONGED SOUND MARK(半角長音)
String ASCII_VER = "\u007C"; //"|" VERTICAL LINE(半角|) 3-2143
String BOX_LT_V = "\u2502"; //"│" BOX DRAWINGS LIGHT VERTICAL 3-2822
String BOX_HV_V = "\u2503"; //"┃" BOX DRAWINGS HEAVY VERTICAL 3-282D
String CJK_4E28 = "\u4E28"; //"丨" CJK UNIFIED IDEOGRAPH 3-2E24(第3水準)
String DENT_CLK = "\u01C0"; //"ǀ" LATIN LETTER DENTAL CLICK (noJIS)
String V_EM = "\uFE31"; //"︱" Vertical Em Dash
String V_EN = "\uFE32"; //"︲" Vertical En Dash
String V_LOWLIN = "\uFE33"; //"︳" Vertical Low Line
String vshort = V_EN;//DENT_CLK;
hyphmap.put(ASCII_HYP,vshort);
hyphmap.put(HYPHEN,vshort);
hyphmap.put(EN_DASH,V_EN);
hyphmap.put(MINUS_SIGN,vshort);
hyphmap.put(EM_DASH,V_EM);
hyphmap.put(ZEN_HYP,V_EM);
hyphmap.put(HAN_CHO,vshort);
hyphmap.put(CHOONFU,CJK_4E28);
hyphmap.put(BOX_LT_H,BOX_LT_V);
hyphmap.put(BOX_HV_H,BOX_HV_V);
}
private static Map<String, String> numtoomap = new HashMap<>(hyphmap);
static {
numtoomap.put("1","一");
numtoomap.put("2","二");
numtoomap.put("3","三");
numtoomap.put("4","四");
numtoomap.put("5","五");
numtoomap.put("6","六");
numtoomap.put("7","七");
numtoomap.put("8","八");
numtoomap.put("9","九");
numtoomap.put("0","〇");
numtoomap.put("1","一");
numtoomap.put("2","二");
numtoomap.put("3","三");
numtoomap.put("4","四");
numtoomap.put("5","五");
numtoomap.put("6","六");
numtoomap.put("7","七");
numtoomap.put("8","八");
numtoomap.put("9","九");
numtoomap.put("0","〇");
}
/**文字列中のハイフォンや長音符を縦のものに置換する*/
public static String hyphen(String smoto){
return exchange(smoto,hyphmap);
}
/**文字列中のハイフォンや長音符を縦のものに、数字を漢数字に置換する*/
public static String numToo(String smoto){
return exchange(smoto,numtoomap);
}
private static String exchange(String smoto,Map<String, String> map){
StringBuffer sb = new StringBuffer(smoto);
int i=0;
int nexti = 0;
int cpct = 0;
String moji,xmoji;
while (sb.length()>i){
nexti = sb.offsetByCodePoints(i,1);
moji = sb.substring(i,nexti);
xmoji = map.get(moji);
if (xmoji!=null){
sb.replace(i,nexti,xmoji);
nexti = sb.offsetByCodePoints(i,1);
}
i=nexti;
cpct++;
}
return sb.toString();
}
}
staticメソッドが2つだけあります。使い方は次のとおりです。
public final class ExchCharTest {
public static void main(String[] args) {
String s ="ラフォーレ桜ヶ丘 B-120 サロゲートペアでも—叱吉—大丈夫"; //注3
System.out.println("before:"+s);
String x = ExchChar.hyphen(s);
System.out.println("hyphen:"+x);
x = ExchChar.numToo(s);
System.out.println("numToo:"+x);
}
}
あらたなサロゲートペア文字を使ってLinuxで確認しました。ここに示したプログラムはCodeZineの制約とWindows 10での実行を考えてサロゲートペアを使わないMS932(≒Shift_JIS)で表現できる似た文字に替えてあります。"叱"はU+53F1ですが、Linuxで試したのはU+20B98です。旁の部分がヒでなく七になっているものです。"吉"はU+5409ですが、Linuxで試したのはU+20BB7です。士ではなく土の"つちよし"と呼ばれるものです。
~$ java ExchCharTest before:ラフォーレ桜ヶ丘 B-120 サロゲートペアでも—叱吉—大丈夫 hyphen:ラフォ丨レ桜ヶ丘 B︲120 サロゲ丨トペアでも︱叱吉︱大丈夫 numToo:ラフォ丨レ桜ヶ丘 B︲一二〇 サロゲ丨トペアでも︱叱吉︱大丈夫
ハイフォンと長音符だけ
では、「PostcardsJushoVer.java」で使ってみましょう。Windows 10でフォントを指定せずに、ハイフォンと長音符だけを置換したのが次の図です。ォとヶが真中ですが許容範囲でしょう。長音符はOKですが、Bと12の間の四角はVertical En Dashのグリフがないことに依る文字化けです。Linuxでは問題なしだったのですが、Windows 10のデフォルトのフォントではだめです。
プログラムは、「PostcardsJushoVer.java」のdrawPage()メソッドの最初の部分に次の変更をしただけです。元々の文字列をExchChar.hyphen()で置換しています。
String addr = hitos.get(idx).addr; ⇩ String tmp = hitos.get(idx).addr; String addr = ExchChar.hyphen(tmp);
フォントをIPAex明朝にすると
IPA明朝ではだめですが、IPAex明朝にするとWindows 10でもうまくいきます。ただし、WindowsもLinuxもこのフォントは追加で入れなければなりません。とはいえ、オープンソースで無料で使用できるので入れやすいフォントです。
プログラムの変更はフォントの指定のところだけです。
g2.setFont(new Font(Font.SERIF, Font.PLAIN, 16));
⇩
g2.setFont(new Font("IPAex明朝", Font.PLAIN, 16));
IPAex明朝にしたことで、文字の品質が落ちたように感じられるかもしれません。フォントによっては小さな字が不得意なものもあります。画面とプリンタでは解像度が異なります。同じサイズでも文字を構成するピクセル数が違うので画面では線が失われたりするようです。低解像度の時のためのビットマップデータを持っているフォントもありますが、IPAではいつのころからかその手法を使わなくなったと聞いています。
このプレビューは拡大すると、品質は上がります。スクリーンショットを掲載する関係で小さいままにしているので、消えている線が確認できます。
一方、SERIFのような論理フォントで指定すると画面では画面用の低解像度で見やすいフォントが割り当てられて、それなりの品質になります。良さそうではありますが、厳密に言うと画面とプリンタで異なるグリフを使っていることになりますから、そこはデメリットかもしれません。
数字も変えてみる
アラビア数字を漢数字にしてみます。
文字列の置換メソッドを替えるだけです。
String addr = ExchChar.hyphen(tmp); ⇩ String addr = ExchChar.numtoo(tmp);
毛筆フォントにしたい
年賀状などでは、毛筆体などを使いたくなるかもしれません。Windows 10ではMicrosoft Office に同梱されてHG行書体という毛筆フォントが入っていることがあります。Linuxでは青柳衡山フォントTなどが使えます。
Bと12の間の黒い四角は再びVertical En Dashのグリフがないことに依る文字化けです。
グリフがない時の対策
FontのメソッドにcanDisplayUpTo(String s)というものがあります。引数の文字列の中にそのフォントにグリフのない最初の文字の位置を返します。なければ-1です。
Vertical En Dash がないときにはASCII縦棒にするということが可能ですが、ASCII縦棒は長すぎてバランスを崩します。
あまりスマートな方法ではないですが、この文字だけ文字の大きさを小さくして対処するとこうなります。常時使う機能ではないので、AfureStringVクラスに新しいメソッドを作って拡張機能としています。
/**drawBottom()のverticalEnDashのグリフのない時の対処版*/
//str,fm, fnt ,g2,mm2pt,mojih,ascent
public void drawBottomEnx(float hm, float vm) {
int strlen = str.length();
int i=0;
int cpcti = 0;
int nexti;
float fontwd;
float smfsize;
float smf;
String s;
float vbase = (vm+remm)*mm2pt+ascent;
while (strlen>i){
nexti = str.offsetByCodePoints(i,1);
s = str.substring(i,nexti); //①
if( fnt.canDisplayUpTo(s)>=0 &&
(s.equals("\uFE32")|| s.equals("\uFE31"))){ //②
smf = (s.equals("\uFE32"))? 0.5f :0.8f; //③
smfsize = fnt.getSize()*smf; //④
g2.setFont(fnt.deriveFont(smfsize)); //⑤
s = "\u007C"; //⑥
fontwd = g2.getFontMetrics().stringWidth(s); //⑦
g2.drawString(s,
hm*mm2pt-fontwd/2,
vbase+mojih*(cpcti-0.5f)+smfsize/2); //⑧
g2.setFont(fnt); //⑨
}else{
fontwd = fm.stringWidth(s);
g2.drawString(s,
hm*mm2pt-fontwd/2,
vbase+mojih*cpcti);
}
i=nexti;
cpcti++;
}
}
- ①で1文字取り出します。charでなくSringなのはサロゲートペア対策です
- ②グリフがなくて、しかも縦EnDashか縦EmDashであるときだけ対処します
- ③Enなら0.5倍、Emなら0.8倍にします
- ④現在のフォントサイズに掛けて使う文字のサイズを決めます
- ⑤現在の文字を引き継いで、サイズだけ異なるフォントを作り、現在のGraphics2Dのインスタンスg2に設定します
- ⑥文字をASCII縦棒にします
- ⑦g2から新たにFontMetricsをgetして縦棒文字の印字幅を求めます。ここはグローバルになっているFontMetricsであるfmを使ってしまうと、元のフォントの幅になってしまいます
- ⑧小さくなったフォントに合わせて縦位置を調整します
- ⑨フォントを元に戻します
AfureStringVのインスタンスを作成した時に、引数になったGraphics2Dのインスタンスg2からFontMetrics、Font、文字サイズ、アセントをgetしてグローバル変数として保持しています。g2のフォントを設定し直してもすでにgetしたFontMetricsには影響が及ばないようです。
ここで用意した宛名ではdrawBottom()だけに対策が必要ですが、Top,TtoB,Midにも用意しておきます。メソッド名はそれぞれEnxを追加しています。グリフがなくて、しかも縦EnDashか縦EmDashであるときだけの対処なので、常時これを使っても問題ありません。ただし何か問題が起こったときに戻せるように、従来のEnxなしのメソッドも残しておきます。これについては後でEnxを追加した「AfureStringV.java」を再掲します。
