Core Text で縦書き

Core Text でテキストを描画するには CTFrameDraw(_:_:) を呼べばいいのだけど、そのパラメータをどうつくるかという話。

FrameDraw 第2引数の CGContextUIGraphicsGetCurrentContext() で得られる。

FrameDraw 第1引数の CTFrameCTFramesetterCreateFrame(_:_:_:_:) で作れる。

CreateFrame 第3引数 の CGPathUIBezierPath から作れるけど、 CGRect からなら CGPath(rect:transform:) のほうが早い。

CreateFrame 第1引数の CTFramesetterCTFramesetterCreateWithAttributedString(_:) で作れる。

引数の CFAttributedStringAPI ドキュメントによると

CFAttributedString is “toll-free bridged” with its Foundation counterpart, NSAttributedString.

とのことなので NSAttributedString を使えば良いようである。

let astr = NSAttributedString(string: string) // アトリビュートなし

アトリビュートに縦書きを指定するには、

let astr = NSAttributedString(string: string, attributes: [.verticalGlyphForm: true])

CTFrame を作るところだけまとめると、

func createFrame(string: String, rect: CGRect) -> CTFrame? {
    let astr = NSAttributedString(string: string, attributes: [.verticalGlyphForm: true])
    let setter = CTFramesetterCreateWithAttributedString(astr)
    let path = CGPath(rect: rect, transform: nil)
    return CTFramesetterCreateFrame(setter, CFRange(), path, nil)
}

描画はこうなる。

let frame = createFrame(string: string, rect: rect)!
let context = UIGraphicsGetCurrentContext()!
CTFrameDraw(frame, context)

赤い枠線は rect を描画したもの。

f:id:hrt1ro:20180927132425p:plain

文字が裏返っている理由は、 Core Textは LLO(Lower Left Origin)の座標系で描くのだけど、 UIKit 由来の CGContext は ULO (Upper Left Origin) の座標系だから。 LLOで描いたものがULOに正しく描画されるように、座標変換させる必要がある。

座標変換するときは、行儀よく保存・復帰させよう。

context.saveGState()
defer { context.restoreGState() }

LLO→ULO座標変換はy軸を反転させるのがふつうなので

context.scaleBy(x: 1, y: -1) // 上下反転(LLO→ULO)

変換後の座標系での rect を点線で描いてある。

f:id:hrt1ro:20180927132459p:plain

裏返るのは直ったけど、縦書きの向きにしたいので90度回転させる。

context.rotate(by: CGFloat.pi/2) // 90度回転
context.scaleBy(x: 1, y: -1) // 上下反転(LLO→ULO)

順序はこの通りでないといけない。

f:id:hrt1ro:20180927132521p:plain

90度回転させると枠の縦横が入れ替わってしまうので、rect を LLO の座標系に逆変換してやらねばなるまい。

まず、一度 Affine 変換をつくってCTMに結合するやりかたに変更する。

let transform = CGAffineTransform(rotationAngle: CGFloat.pi/2).scaledBy(x: 1, y: -1)
context.concatenate(transform)

逆変換したrectで描くと、正位置に出るはず。

let frame = createFrame(string: string, rect: rect.applying(transform.inverted()))!

f:id:hrt1ro:20180927132542p:plain

さて調子に乗って NSString.draw(at:withAttributes:) と混ぜると大変なことになる

f:id:hrt1ro:20180927132556p:plain

これは NSString.draw(at:withAttributes:)CGContext.textMatrix を変更するのだけど、元に戻してくれないため。 そして CTFrameDraw(_:_:)CGContext.textMatrix に従って描画するため、だから。

しかたがないので自分で元に戻してやる。というかそもそもそういうもののようである。

context.textMatrix = CGAffineTransform.identity

まとめるとこんなかんじ。

func drawVText(context: CGContext, string: String, rect: CGRect) {
    context.saveGState()
    defer { context.restoreGState() }
    let transform = CGAffineTransform(rotationAngle: CGFloat.pi/2).scaledBy(x: 1, y: -1)
    context.concatenate(transform)
    let frame = createFrame(string: string, rect: rect.applying(transform.inverted()))!
    context.textMatrix = CGAffineTransform.identity
    CTFrameDraw(frame, context)
}
func createFrame(string: String, rect: CGRect) -> CTFrame? {
    let astr = NSAttributedString(string: string, attributes: [.verticalGlyphForm: true])
    let setter = CTFramesetterCreateWithAttributedString(astr)
    let path = CGPath(rect: rect, transform: nil)
    return CTFramesetterCreateFrame(setter, CFRange(), path, nil)
}

さてASCIIな文字を混ぜたりするとちょっと期待と異なるので、調整してやらねばならないのだけれども、 たぶん次回に続く