How to draw proper vertical text in a CGContext?

89 views Asked by At

I need to draw an NSAttributedString to a CGContext using a vertical layout orientation (ie. a vertical “writing direction”). It is not sufficient to rotate horizontally-typeset lines of text, because that doesn’t activate vertical OpenType features or consider in which scripts the glyphs should be upright (ex: Chinese).

This is already possible with an NSTextView. The layout orientation of an NSTextView can be either horizontal or vertical either by setting a NSLayoutManager.TextLayoutOrientation value or, via the UI by right-clicking inside the view. But I need to draw the text into a Core Graphics PDF context—not a view.

I don’t see anything about vertical typesetting in CoreText, but I’m guessing that TextKit is capable of this. It appears that TextKit’s NSTextContainer can be subclassed to use a vertical orientation, and that seems like an important part of the puzzle. I’m currently doing that like so:

class VerticalTextContainer: NSTextContainer {
    override var layoutOrientation: NSLayoutManager.TextLayoutOrientation { .vertical }
}

I’m new to TextKit and can’t find many examples of people using it with a CGContext instead of a view, so I’m sure I’m setting this up incorrectly. Here’s my TextKit set up:

let attrString = NSAttributedString(
    string: "Hello 你好!"
)

/// The text box; the shape/frame to fill with the text.
let textContainer: NSTextContainer = VerticalTextContainer(size: NSSize(width: 300, height: 300) )

/// The part that handles laying out text elements, generating text layout fragments.
let textLayoutManager: NSTextLayoutManager = {
    var layoutManager = NSTextLayoutManager()
    layoutManager.textContainer = textContainer
    return layoutManager
}()

/// The part that stores the attributed string and connects to the layout manager.
let textContentStorage: NSTextContentStorage = {
    var storage = NSTextContentStorage()
    storage.attributedString = attrString
    storage.addTextLayoutManager( textLayoutManager )
    return storage
}()

Then, in my CGContext, I’ve tried this—but nothing appears:

// inside a new 300x300 CGContext:

// prep:
cgContext.scaleBy(x: 1, y: -1)
cgContext.translateBy(x: 0, y: size.height * -1)
cgContext.textMatrix = .identity
cgContext.setFillColor( .black )

// try drawing the TextKit line fragments
textLayoutManager.enumerateTextLayoutFragments(
    from: textLayoutManager.documentRange.location,
    options: .ensuresLayout
) { textLayoutFragment in
    textLayoutFragment.draw(at: CGPoint(x: 100, y: 100), in: cgContext)
    return true
}

I’ve tried many variations on this, including passing nil to the enumerate method’s from: parameter, iterating over each of the textLayoutFragment.textLineFragments and drawing each one, and doing textLayoutManager.ensureLayout(for: textLayoutFragment.rangeInElement). Nothing seems to work. I’m not even sure if the layout fragments are being created.

Any help would be so appreciated!

0

There are 0 answers