метод swift text.draw портит CGContext

Я использую CoreGraphics для рисования отдельных глифов вместе с примитивами в CGContext. Следующий код работает на быстрой игровой площадке в XCode 9.2. При запуске на игровой площадке маленький прямоугольник с двойной буквой A должен появиться в заданных координатах в liveView игровой площадки.

import Cocoa
import PlaygroundSupport

class MyView: NSView {

    init(inFrame: CGRect) {
        super.init(frame: inFrame)
    }

    required init?(coder decoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func draw(_ rect: CGRect) {

        // setup context properties

        let context: CGContext = NSGraphicsContext.current!.cgContext

        context.setStrokeColor(CGColor.black)
        context.setTextDrawingMode(.fill)
        context.setFillColor(CGColor(red: 0.99, green: 0.99, blue: 0.85, alpha: 1))
        context.beginPath()
        context.addRect(rect)
        context.fillPath()
        context.setFillColor(.black)

        // prepare variables and constants

        var font = CTFontCreateWithName("Helvetica" as CFString, 48, nil)
        var glyph = CTFontGetGlyphWithName(font, "A" as CFString)
        var glyph1Position = CGPoint(x: self.frame.minX, y: self.frame.maxY/2)
        var glyph2Position = CGPoint(x: self.frame.minX+150, y: self.frame.maxY/2)

        let text = "Hello"
        var textOrigin = NSPoint(x: self.frame.minX+50, y: self.frame.maxY/2)

        // draw one character

        CTFontDrawGlyphs(font, &glyph, &glyph1Position, 1, context)

        // ***   ***   when the next line is uncommented the bug appears   ***   ***

        // text.draw(at: textOrigin)

        CTFontDrawGlyphs(font, &glyph, &glyph2Position, 1, context)
    }
}

var frameRect = CGRect(x: 0, y: 0, width: 200, height: 100)

PlaygroundPage.current.liveView = MyView(inFrame: frameRect)

Теперь я хочу нарисовать обычный текст в том же контексте. Однако, когда текстовая строка рисуется между рисованием двух глифов с помощью собственного метода рисования, текущий контекст кажется испорченным, второй глиф не отображается. Когда текст рисуется после того, как оба одиночных глифа нарисованы, все в порядке.

Таким образом, очевидно, что рисование текста влияет на текущий CGContext, но я не могу понять, что именно происходит. Я попробовал метод saveGstate() перед рисованием строки и последующим восстановлением, но безуспешно.

Так же пробовал методами CoreText создать атрибутированную String с CTFramesetterCreateWithAttributedString и показать ее с CTFramesetterCreateFrame, тоже не работает, тут после создания фреймсеттера перепутался контекст.

Моя реальная игровая площадка более сложная, там глифы не исчезают полностью, а отображаются в неправильном вертикальном положении, но основная проблема - и вопрос тот же:

Как я могу нарисовать текст в currentContext без каких-либо других изменений в контексте, выполняемых в фоновом режиме?


person MassMover    schedule 29.10.2018    source источник


Ответы (1)


Вам нужно установить текстовую матрицу (преобразование, применяемое к рисованию текста). Вы должны всегда устанавливать это перед рисованием текста, потому что это не является частью состояния графики и может быть уничтожено другим кодом рисования, таким как расширения рисования строк AppKit.

Добавьте следующее перед вызовом CTFontDrawGlyphs:

    context.textMatrix = .identity

Это следует вызывать при первоначальной настройке контекста, поскольку нет обещания, что текстовая матрица будет идентична до вызова drawRect. Затем каждый раз, когда вы совершали вызовы чего-то, что изменяет текстовую матрицу, вам нужно будет установить ее обратно на то, что вы хотите (в данном случае идентичность, но это может быть что-то еще, если вы хотите рисовать причудливым образом).

Не всегда очевидно, что изменит текстовую матрицу. Функции рисования AppKit почти всегда это делают (хотя я не знаю какой-либо документации, указывающей на это). Функции основного текста, которые изменяют контекст, такие как CTFrameDraw и CTLineDraw, обычно документируют этот факт с помощью утверждения, такого как:

Этот вызов может оставить контекст в любом состоянии и не сбрасывает его после операции рисования.

Точно так же CTFontDrawGlyphs предупреждает:

Эта функция изменяет состояние графики, включая шрифт, размер текста и текстовую матрицу, если эти атрибуты указаны в шрифте. Эти атрибуты не восстанавливаются.

Как правило, я не одобряю смешивание систем рисования текста. Используйте AppKit или Core Text, но не смешивайте их. Если вы выберете один и будете придерживаться его, то обычно это не проблема (до тех пор, пока вы инициализируете матрицу один раз в верхней части drawRect). Например, если бы вы сделали весь рисунок с помощью CTFontDrawGlyphs, вам не нужно было бы каждый раз сбрасывать матрицу; вы бы остались в матрице основного текста, и все было бы в порядке (вот почему это работает, когда вы закомментируете вызов draw(at:)).

person Rob Napier    schedule 29.10.2018
comment
Спасибо, Роб. К сожалению, это не помогает. Ни размещение этой строки непосредственно перед вызовом метода, ни в начале метода отрисовки ничего не меняет, второй глиф все равно не появится. - person MassMover; 29.10.2018
comment
Я взял вашу игровую площадку, раскомментировал строку 44 и добавил приведенный выше код в строку 46, и он рисует «Hello A» в режиме реального времени. Это Xcode 10.1b3. Какая у вас конфигурация? - person Rob Napier; 29.10.2018
comment
Привет, Роб, да, я узнал это сейчас. Но разве это не противоречит вашему объяснению? Сначала я вставил ваш код в строку 43, а также попытался вставить его в строку 19, как вы написали: всегда устанавливайте это перед рисованием текста. Теперь очевидно, что он работает только в том случае, если он установлен ПОСЛЕ рисования текста. Я хотел бы понять, что происходит внизу. Должен ли я устанавливать этот параметр каждый раз после рисования строки текста? - person MassMover; 29.10.2018
comment
Ваш код предполагает, что текстовая матрица является единичной матрицей (без поворотов, без переводов). drawRect не обещает, что текстовая матрица тождественна, но в целом так и есть. Функции рисования строк AppKit (например, text.draw(at: textOrigin)) изменяют текстовую матрицу, а не восстанавливают ее. Поэтому, если вы вызываете функцию рисования строки AppKit, вы должны установить текстовую матрицу на то, что вы хотите, что в вашем случае является идентификатором. - person Rob Napier; 29.10.2018
comment
Хорошо, но это фактически означает, что после каждого вызова такой функции матрица должна быть возвращена вручную, верно? Я отмечу ваш ответ как правильный ответ, но, возможно, вы могли бы переписать его части, чтобы отразить результаты этих комментариев. Спасибо за помощь! - person MassMover; 29.10.2018