初歩のシェルスクリプトで遊ぶ[ぬかみそフォントの制作サポート(20)]

TTEditのグリフcsvファイルをsvgファイルへ変換する

<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Musashi System TTEdit -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="1024px" height="1024px" viewBox="0 0 1024 1024">
<path fill="#000000" d="M617,971 Q570,971 567,929 Q567,886 610,884 Q638,885 675.5,880.5 713,876 761,848 Q819,807 837,741 855,675 833.0,608.5 811,542 750,494 Q729,478 702,467 Q633,642 564,729 Q433,894 307,907 Q205,921 146,868 Q86,816 90.0,713.5 94,611 159.0,530.5 224,450 335,407 Q348,403 361,398 Q361,331 362,269 L157,269 Q115,269 115,229 Q115,189 157,189 L366,189 Q369,141 373,96 Q375,73 385.5,64.5 396,56 420,58 Q464,63 460,98 Q455,143 452,189 L867,189 Q909,189 909,229 Q909,269 867,269 L448,269 Q446,322 446,377 Q510,366 581,367 Q616,368 647,372 Q650,362 653,353 Q666,310 706,324 Q750,342 737,375 Q734,383 731,392 Q769,406 801,427 Q885,485 917,579 949,673 925.5,766.0 902,859 820,913 Q767,947 709.0,960.5 651,974 617,971 z M570,444 Q505,444 447,455 Q454,599 491,695 Q500,682 510,668 Q565,592 620,447 Q596,445 570,444 z M359,482 Q266,522 220,583 174,644 170,708 166,772 201,802 Q238,831 296,825 Q362,817 424,765 Q422,762 420,758 Q372,644 364,481 Q361,482 359,482 z "/>
</svg>

「あ」をTTEditで「svgファイルにエクスポート」で、svgファイルを作れます。
今回は、TTEditで書き出したcsvファイルを、シェルスクリプトで上記と同様のsvgに書き換えます。

いちおうスクリプトは置いておきますが、実行は控えたほうがいいかも。
もしスクリプトにミスがあれば、既存のファイルを上書きして壊してしまうかもしれませんし。


3パターン作ったんですが、いちばん高速で、シェル関数も含めてひとつのファイルにまとめたものだけ貼っておきます。
説明はコメントに書いたものが既にありまして、そこからコピペで。

ファイルひとつにまとめたスクリプト

#!/bin/sh
#set -x
# ======================================================================================
# ■ nukamiso専用 TTEdit csv をsvg へ変換 一時ファイル書き出し無し 外部ファイル不要
# 
# 
# stdin 変換するTTEdit csvファイルパス ファイルを UTF8,LF に変換してから入力すること
# ${1}  仮想ボディ値 デフォルト1024(nukamisoの設定値)
# ${2}  ascent値 フォントファイル全体に共通の設定値 デフォルト901(nukamisoの設定値)
# 
# TTEditのcsvファイル専用。OTEditのcsvファイルは使えない。
# OTEditのcsvを入力しても、エラー報告も停止もしない。誤った変換結果を出力する。
# 
# 出力先はmktempが自動作成するディレクトリ
# 
# 
# 
# ======================================================================================



# stdinが無いなら終了
if ! test -p /dev/stdin  ; then
  echo "ls *.csv | ${0##*/} 仮想ボディ値 アセント値" 1>&2
  exit 0
fi

# シェル関数を外部に分離するなら
# ./Path 以下にシェル関数ファイルを入れておく
# 
#scriptDir="$( cd -- "$(dirname -- "`readlink -f "${0}"`")" && pwd )"
#PATH="${scriptDir}/Path:${PATH}"
# 
#  if ! . shellFuncLib.func   ; then exit 2
#else true
#fi
#


# 仮想ボディ値 1024
ttfBody="${1:-1024}"
# アセント 901
ttfAscent="${2:-901}"


# 出力先ディレクトリ 設定
# グリフパーツcsv 一時保存ディレクトリ 設定

if test -d '/dev/shm' ; then
  outputDir="`mktemp -d -p '/dev/shm' --suffix="_${ttfBody}-${ttfAscent}"`"
else
  outputDir="`mktemp -d --suffix="_${ttfBody}-${ttfAscent}"`"
fi

# ---------------------------------------------------------------------------------
# シェル関数 外部ファイルに置いて読み込んでもよい
# ---------------------------------------------------------------------------------

glyphPartLineFunc(){
# csv座標データを、パーツごとに1行にまとめる。
# 座標の区切りは半角空白
# 半角空白を改行に入れ替えると、元のcsvデータに戻せる

# glyphLineFunc CSV "${_csv}"
# ${1} 代入する変数名
# ${2} csvデータそのもの


local VarName LINE csvTemp

VarName="${1:?}"

IFS=' 	
'

for LINE in ${2:?} ; do
case "${LINE}" in
( '1,'* )
csvTemp="${csvTemp}
${LINE}"

;;
( '2,'* | '3,'* )
csvTemp="${csvTemp} ${LINE}"

;;
( * )
csvTemp="${csvTemp}
${LINE}"

;;
esac
done

# 先頭の余分な改行(1行目の空行)を削除
csvTemp="${csvTemp#?}"

# 指定されたシェル変数にデータを入れて返す
eval ${VarName}'="${csvTemp}"'
return 0
}

tteCsvPartData2svgPathSh(){
# ■ nukamiso専用 TTEdit csv の単独グリフパーツデータをsvg path 形式へ変換
# 
# ${1} ascent値 フォントファイル全体に共通の設定値 デフォルト901(nukamisoの設定値)
# ${2} 変換するTTEdit csvデータ(UTF8 LFに変換してから)
local ttfAscent
ttfAscent=${1:-901}

# もし前行がオフカーブ点「3」ならば true
local offCurveFlag
offCurveFlag='false'

local onCurveStartX onCurveStartY
local decimalX decimalY

while read -r LINE ; do

IFS=",${IFS}"
set -- ${LINE}
IFS="${IFS#?}"

case "${1}" in
# ---------------------------------------------------
( 1 )
# ---------------------------------------------------
printf '%s' "M${2},$(( ${ttfAscent} - ${3} ))"

onCurveStartX=${2}
onCurveStartY=${3}

offCurveFlag='false' # 不要

;;
# ---------------------------------------------------
( 2 )
# ---------------------------------------------------
case "${offCurveFlag}" in
( 'false' ) # 「1 2」「2 2」のとき
# Lコマンド
printf '%s' " L${2},$(( ${ttfAscent} - ${3} ))"

;;
( 'true' ) # 「3 2」と連続したとき
# Qコマンドのパラメータ
printf '%s' " ${2},$(( ${ttfAscent} - ${3} ))"

;;
esac
offCurveFlag='false'

;;
# ---------------------------------------------------
( 3 )
# ---------------------------------------------------
case "${offCurveFlag}" in
( 'false' )
# 「Q」は省略できない
printf '%s' " Q${2},$(( ${ttfAscent} - ${3} ))"

# もし次が「3」だったならば、暗黙の「2」を補完するために必要
offCurvePreX=${2}
offCurvePreY=${3}
offCurveFlag='true'

;;
( 'true' ) # 「3 3」と連続したとき

# 暗黙の「2」を補完するため、前回の「3」の座標との中点をとる
# 

# 小数点を作成 X
case $(( ${offCurvePreX} + ${2} )) in
( *[13579] )
  decimalX='.5'
;;
( * )
  decimalX='.0'
;;
esac
# 小数点を作成 Y
case $(( ${ttfAscent} * 2 - ${3} - ${offCurvePreY} )) in
( *[13579] )
  decimalY='.5'
;;
( * )
  decimalY='.0'
;;
esac
# X,Yともに割り切れるとき小数点を省略
case "${decimalX}_${decimalY}" in
( '.0_.0' )
  decimalX=''
  decimalY=''
;;
esac

printf '%s' " $(( ( ${offCurvePreX} + ${2} ) / 2 ))${decimalX}"','\
"$(( ( ${ttfAscent} * 2 - ${3} - ${offCurvePreY} ) / 2 ))${decimalY}"

# Qコマンドは省略する
# 省略することでTTEditに「2を補完した」ことを伝える
printf '%s' " ${2},$(( ${ttfAscent} - ${3} ))"
# もし次がさらに「3」だったときのため覚えておく
offCurvePreX=${2}
offCurvePreY=${3}
offCurveFlag='true'
;;
esac
;;
# ---------------------------------------------------
( * )
# ---------------------------------------------------
return 2

;;
esac
# ↓csvデータ入力
done << __STDIN
${2}
__STDIN

# もしcsvの最終行が「3」だったならば、補完「2」を作成する

case "${offCurveFlag}" in
( 'false' ) # 「2」のとき
# 補完不要
:

;;
( 'true' ) # 「3」のとき
# Qコマンドのパラメータ
printf '%s' " ${onCurveStartX},$(( ${ttfAscent} - ${onCurveStartY} ))"

;;
esac


# 前後に空白を入れる
printf '%s' ' z '
return 0
}



tteSvgTemplateSh(){
# ■ 値を受け取って、svgの定型文を出力する
# ${1} 仮想ボディの値
# ${2} 一文字の横幅
# svg の <path> の前半、「d=""」まで
# 

printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Musashi System TTEdit -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">'

printf '%s\n' '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="'${2:-1024}'px" height="'${1:-1024}'px" viewBox="0 0 '${2:-1024}' '${1:-1024}'">'
printf '%s' '<path fill="#000000" d="'

}






# ----------------------------------------------------------------------
# メインループ
# ----------------------------------------------------------------------

while IFS='' read -r csvPath ; do

# stdin パス検査とファイル名取得
if test -f "${csvPath}" ; then
  csvFileName="${csvPath##*/}"
else
  exit 1
fi

{ # 1文字分の TTEdit csv を svg に変換

# csvデータを読み込んで、
# 1グリフパーツ1行の形式に変換、変数代入
glyphPartLineFunc csvData "`cat -- "${csvPath}"`"

while read LINE ; do
case "${LINE}" in
( '文字幅='* )

IFS="=${IFS}"
set -- ${LINE}
IFS="${IFS#?}"

tteSvgTemplateSh "${ttfBody}" "${2}"

;;
( '1,'* ) # グリフ部品ごとに座標をまとめた行

set -- ${LINE}
IFS="
${IFS}"
LINE="${*}"
IFS="${IFS#?}"

tteCsvPartData2svgPathSh ${ttfAscent} "${LINE}"

;;
( '"種別","X","Y"' )
true

;;
( * )
echo "${0##*/}:${LINE}" 1>&2
exit 2

;;
esac
done << __STDIN
${csvData}
__STDIN


# svgを閉じる
echo '"/>
</svg>'

} 1> "${outputDir}/${csvFileName%.*}.svg"

printf '%s' '.' 1>&2

done




echo "
出力先	${outputDir}" 1>&2

解説のコメント

# ▼ TTEdit csv の仕組み
# TTEdit csv は、TTEditグリフの「数値テーブル」を、csvにそのまま書き出したファイルである。
# 1文字1ファイル、1行1制御点で、xy座標値が並ぶ。
# 1列目「1 2 3」が制御点の種類を表す。
# 
# 1:輪郭開始オンカーブ点
# 2:オンカーブ点
# 3:オフカーブ点
# 
# 1で開始して、2または3が連続する。
# 輪郭の終了を明示するコマンドといったものは無い。csvファイルの終了、または
# 次の1の出現で区切られる。最終2または3から、最初の1へと戻る輪をつくる。
# csvファイルひとつは、この輪を単数か複数、含む。
# svgのpathなら端のある曲線を描くこともできるが、TTEditでは必ず輪でなければならない。
# 
# これらは、svg path の「M Q L z」コマンドに該当する。
# y座標を変換して、M,Q,Lコマンドに与えればpathになる。
# ただし「3 3」と連続したとき、「3」で終了したときには、不足分の座標値を補完する必要がある。
# 
# csvデータは個々のグリフの制御点の種類とxy座標、横幅のみ。その他の情報、グリフのUnicodeも含まれない。
# svgへの変換には、フォントファイルのアセント値が必要になる。
# 加えて、TTEditでの読み込み時には、仮想ボディ値や等幅/可変幅の情報、フォント名なども必要。
# 
# 
# 
# ▼ csv,svgのサンプル csv CP932_CRLF svg UTF8_CRLF 
# 仮想ボディ1024,アセント880,文字幅1024 
#  
# 文字幅=1024
# "種別","X","Y"
# 1,40,880
# 3,80,880
# 2,80,840
# 3,80,800
# 2,40,800
# 2,20,800
# 2,0,880
# 1,140,880
# 3,180,880
# 3,180,800
# 2,140,800
# 2,120,800
# 3,100,880
# 
# <?xml version="1.0" encoding="utf-8"?>
# <!-- Generator: Musashi System TTEdit -->
# <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
# <svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="1024px" height="1024px" viewBox="0 0 1024 1024">
# <path fill="#000000" d="M40,0 Q80,0 80,40 Q80,80 40,80 L20,80 L0,0 z M140,0 Q180,0 180,40 180,80 140,80 L120,80 Q100,0 140,0 z "/>
# </svg>
# 
# ▼ 変換比較図
# ・csvのy座標はsvgの値に変換済み
# ・[]は補完した座標
# 
# 1,40,0             M 40,0
# 3,80,0   2,80,40   Q 80,0 80,40
# 
# 3,80,80  2,40,80   Q 80,80 40,80
# 
# 2,20,80            L 20,80
# 2,0,0              L 0,0
#                    z
# 1,140,0            M 140,0
# 3,180,0            Q 180,0  [180,40]   <- 次のオフカーブ点との中点を補完
# 3,180,80 2,140,80    180,80 140,80     <- Qコマンドを省略
# 
# 2,120,80           L 120,80
# 3,100,0            Q 100,0  [140,0]    <- 開始オンカーブ点を補完
#                    z
# 
# 
# 
# ▼ 座標値の csv -> svg 変換
# x座標はcsvの数値をそのまま使う。
# y座標は変換が必要になる。
# 
# SVGy = CSVy * (-1) + ascent値
#      = ascent - CSVy
# 
# 
# 
# 
# 
# ▼ path以外のxml部分
# 
# ほぼ、定形と見なして差し支えない。
# 以下の数値がフォントから取得される。
# 
# ・width="1024px" 
# 文字情報の文字幅
# csvファイル内の「文字幅=1024」から取得する。
# プロポーショナルフォントでユーザが文字毎に設定する文字幅である。
# 
# ・height="1024px"
# Mac用アセント + Mac用ディセント で算出する。
# アセントとディセントの合計は分かるが、個々の値は分からない、ことに注意。
# csv pathに変換するためにはアセント値が必要だが、csvからは取得できない。
# 
# ・viewBox="0 0 1024 1024"
# 「0 0」は固定。続いて width,height と同じ値が入る。
# 
# ・失われる情報
# 
# macアセント         900
# macディセント       144
# winアセント        1000
# macディセント       244
# 文字幅              987
# 縦書き文字高さ上端  896
# 縦書き文字高さ下端  -54
# 
# <svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="987px" height="1044px" viewBox="0 0 987 1044">
# 
# csvに残る情報は「文字幅」のみ
# svgに残る情報は「文字幅」「macアセント+macディセント」のみ
# 
# TTEditの等幅フォントならば、フォントファイル単位のアセント,ディセント値をメモしておけば
# ほぼフォントの情報を失わずに保存することは可能だ。
# プロポーショナルフォントでは難しい。縦書き文字幅が失われるため。
# 
# 
# ▼ 処理の基本方針
# グリフの輪、部品ひとつずつ処理を行う。
# csvファイルデータを分割する。
# 
# ・1-2行目 「文字幅=*」「"種別","X","Y"」
# ・3行目以降行末まで xy座標値
# 
# 1-2行目からは文字幅を取得し、svgのpath以外のタグ類を作成する。
# 
# 3行目以降からは svg path を作成する。
# 座標行はグリフごとに分割する。「1,x,y」行で始まり、「2,x,y」「3,x,y」行で終わる。
# 座標行は予め適切に分割し、グリフ部品単位で処理する必要がある。
# 
# グリフの部品一つずつ、svg path へ変換するシェル関数へ入力する。
# シェル関数内では、while read の行単位処理を行っている。
# 
# 
# 
# ▼ 「1 2 3」について
# 
# すべての行の1列目は、「1 2 3」のいずれか。
# 「1」Mコマンドに確定している。保存が必須。
#   csvの最終行がオフカーブ点「3」のとき、続くオンカーブ点「2」にもなる。
# 「3」Qコマンドの1つ目の座標。保存が必須。
#      Qコマンドの2つ目の座標は、次に「2」と「3」のどちらが来るかで処理が異なる。
#      次に「3」が来たとき、Qコマンドの2つ目の座標を計算して補完する。
#      次に「2」が来たなら、それをそのままQコマンドの2つ目の座標に使う。
# 「2」Lコマンド、またはQコマンドの2つ目の座標
#     Lコマンドが連続しても、「L」は省略されない。
# 最終コマンドは「z」に確定している。すべての行の処理が終わったあと、無条件で出力する。
# 
# ・Qコマンドの座標補完
# 
# Q (x1 y1 x y)+
# 
# Q コマンドは、座標が2つずつセットでなければならない。
# (x1 y1) 「3」の座標             オフカーブ
# (x  y ) 「3」直後の「2」の座標  オンカーブ
# 
# Qコマンドが連続したときは、後続のQを省略できる。これはTTEditにおいては、
# オフカーブ点の連続(オンカーブ点の[有り/無し])を意味する。
# 下記は「3 3 2 3 2」の例。
# 
# Q オフカーブ [オンカーブ]  <- オンカーブ点は補完
#   オフカーブ  オンカーブ
# Q オフカーブ  オンカーブ
# .....
# 
# 
# 「3 3」と連続したときは、省略された「2」の座標を補完する必要がある。この座標は、
# 「3」と「3」の中点、つまり、足して2で割った座標になる。
# 最終行「3」のときは、輪郭開始オンカーブ点の座標になる。
# 
# スクリプトを行単位でリニアな処理をするならば、
# 開始オンカーブ点座標は、必ずグリフのリングの最終行まで保存すること。
# 最終行が3で終わるか終わらないか、不明だから。
# 
# また、「3」の行の座標は、必ず次の行の処理まで保存しなければならない。
# 座標補完に必要だから。「2」が来たか、または補完したならば、破棄してよい。
# 
# 
# ▼ 処理の注意点 小数点「0.5」は切り捨てない
# 
# Qコマンドの2つ目の座標は2で割り算をして中点を算出するが、この小数点を省略してはならない。
# XYともに割り切れるときは省略するが、どちらか片方でも「.5」のときは、省略できない。
# 片方が「.5」のとき、もう片方が割り切れても「.0」で出力する。