admin管理员组文章数量:1357652
The different coordinate systems of Cocoa and UIKit cause problem when drawing a string in a UIGraphicsContext.
I have written code for macOS
, where I draw a combination of primitives, attributedText, and single CGGlyphs
onto a CGContext
. Therefore I first create a container object that holds several arrays of objects. These objects confirm to a protocol DrawingContent
. The only protocol requirement is a method .draw(inContext: CGContext)
.
My custom NSView
is holding such a container, and the view's draw method then iterates over the container elements, calling their .draw
method with the current CGContext
. I have stripped the code down to the minimum and will show it at the end of this question. The result of the sample code before being flipped looks like this:
My actual code is much more complex, as the objects to be drawn are a result of multiple algorithms. The reason I put the objects into a container is that from the start on I intended to also create an iOS
version, so the results of the necessary calculations first go into a framework-agnostic abstract class. I was aware of the fact that UIKit
Views use a flipped coordinate system and that I eventually would have to deal with that. Now I am attempting to write the iOS
version – and I am at a loss.
The initial result when I just call the code without any alteration from an UIView looks like this:
This was – sort of – expected, although the text not being flipped is odd. So to flip the whole imagery I set a CGAffineTransform
to the context before passing it to the drawing container like this:
currentContext.translateBy(x: 0, y: rect.height)
currentContext.scaleBy(x: 1, y: -1)
The result looks like this:
As you can see the canvas is flipped as expected, but, unfortunately, the AttributedString
is drawn upside down. Interestingly, though, the single CGGlyph
, although using the same font, is drawn correctly.
WHAT I HAVE TRIED SO FAR:
Answers to similar questions suggest to apply n
CGAffineTransform
to the contexts's textMatrix (in addition to the context itself). This causes theCGGlyph
(which had been drawn correctly) now to be upside down and at a wrong location, while theAttributedText
remains untouched.In another place it was suggested to temporarily flip the context just for the drawing of the string like this:
context.saveGState()
context.scaleBy(x: 1, y: -1)
context.translateBy(x: 0, y: -bounds.height)
// draw string here...
context.restoreGState()
The problem with this is, that the container does not know about the frame's height. When I (for testing) hardcode the height the text now draws correctly, but at its „original“ wrong position at the top of the view. The two identical transformations obviously cancel out each other.
- I also tried to manually apply the scaling and tranlating to the y-position of the text, in addition to what I did in step 2. This revealed another problem: The text now shows at about the right position, but not quite: It now dangles from the imaginary baseline instead of sitting on it.
I also want to state, that even if I would find out how the font metrics come into play here, applying this to my actual code would be a tremendous amount of work, as – as stated – the drawing methods of my objects do not know the height of the view they draw in, so I'd have to pass that as an additional argument, and – most importantly – I would have to write all of that scaling and translating conditionally when compiling for
iOS
– which would counteract my approach to keep the container-code framework-agnostic.
I could go down two different basic routes:
I could rewrite my entire code to work in the flipped coordinate system of
iOS
, and formacOS
I set the .isFlipped parameter on my customNSView
to return true. This would involve a lot of work, and it still would not solve the problem of theAttributedStrings
being upside down.I continue trying to find a way to flip EVERYTHING on the canvas. I would prefer the second route, obviously.
QUESTION:
How do I draw Objects, that are positioned for a cartesian coordinate system (like Cocoa/Quartz 2D uses), into a CGContext
that uses a flipped coodinate system (like UIKIT does), so that all objects of different types (primitives, Glyphs and Strings) are drawn at the correct position and in the correct orientation?
Here is my code. For convenience, instead of showing the UIView
code I show a self containing version for macOS
with the NSView
set to .isFlipped
- the behaviour is - as far as I could examine - exactly the same.
import AppKit
public protocol DrawingContent {
func draw(inContext context: CGContext)
}
public struct TestPath: DrawingContent {
public func draw(inContext context: CGContext) {
context.beginPath()
context.setLineWidth(CGFloat(2))
context.move(to: CGPoint(x: 200, y: 30))
context.addLine(to: CGPoint(x: 250, y: 30))
context.addLine(to: CGPoint(x: 250, y: 60))
context.addLine(to: CGPoint(x: 230, y: 60))
context.strokePath()
}
}
public struct TestText: DrawingContent {
public func draw(inContext context: CGContext) {
let font = CTFontCreateWithName("Helvetica" as CFString, 24, nil)
var attributes: [NSAttributedString.Key: AnyObject] = [NSAttributedString.Key.font : font,
NSAttributedString.Key.foregroundColor : NSColor.black]
let attributedText = NSAttributedString(string: "Testtext", attributes: attributes)
let descender = CTFontGetDescent(font)
let textOrigin = CGPoint(x: 100, y: (30-descender))
attributedText.draw(at: textOrigin)
}
}
public struct TestGlyph: DrawingContent {
public func draw(inContext context: CGContext) {
var font = CTFontCreateWithName("Helvetica" as CFString, 24, nil)
var position = CGPoint(x: 30, y: 30)
var glyph = CGGlyph(36) // Capital Letter A
CTFontDrawGlyphs(font, &glyph, &position, 1, context)
}
}
public class FlippedTestView: NSView {
var drawingContent: [DrawingContent]
override public var isFlipped: Bool {return true}
override public init(frame: CGRect) {
let drawingContent: [DrawingContent] = [TestGlyph(), TestText(), TestPath()]
self.drawingContent = drawingContent
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override open func draw(_ rect: CGRect) {
let currentContext = NSGraphicsContext.current!.cgContext
currentContext.translateBy(x: 0, y: rect.height)
currentContext.scaleBy(x: 1, y: -1)
for element in drawingContent {
element.draw(inContext: currentContext)
}
}
}
@main
class AppDelegate: NSObject, NSApplicationDelegate {
@IBOutlet var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
let testView = FlippedTestView(frame: CGRect(x: 0, y: 0, width: 300, height: 100))
self.window.contentView = testView
self.window.setFrame(CGRect(x: 200, y: 200, width: 300, height: 100), display: true)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}
The different coordinate systems of Cocoa and UIKit cause problem when drawing a string in a UIGraphicsContext.
I have written code for macOS
, where I draw a combination of primitives, attributedText, and single CGGlyphs
onto a CGContext
. Therefore I first create a container object that holds several arrays of objects. These objects confirm to a protocol DrawingContent
. The only protocol requirement is a method .draw(inContext: CGContext)
.
My custom NSView
is holding such a container, and the view's draw method then iterates over the container elements, calling their .draw
method with the current CGContext
. I have stripped the code down to the minimum and will show it at the end of this question. The result of the sample code before being flipped looks like this:
My actual code is much more complex, as the objects to be drawn are a result of multiple algorithms. The reason I put the objects into a container is that from the start on I intended to also create an iOS
version, so the results of the necessary calculations first go into a framework-agnostic abstract class. I was aware of the fact that UIKit
Views use a flipped coordinate system and that I eventually would have to deal with that. Now I am attempting to write the iOS
version – and I am at a loss.
The initial result when I just call the code without any alteration from an UIView looks like this:
This was – sort of – expected, although the text not being flipped is odd. So to flip the whole imagery I set a CGAffineTransform
to the context before passing it to the drawing container like this:
currentContext.translateBy(x: 0, y: rect.height)
currentContext.scaleBy(x: 1, y: -1)
The result looks like this:
As you can see the canvas is flipped as expected, but, unfortunately, the AttributedString
is drawn upside down. Interestingly, though, the single CGGlyph
, although using the same font, is drawn correctly.
WHAT I HAVE TRIED SO FAR:
Answers to similar questions suggest to apply n
CGAffineTransform
to the contexts's textMatrix (in addition to the context itself). This causes theCGGlyph
(which had been drawn correctly) now to be upside down and at a wrong location, while theAttributedText
remains untouched.In another place it was suggested to temporarily flip the context just for the drawing of the string like this:
context.saveGState()
context.scaleBy(x: 1, y: -1)
context.translateBy(x: 0, y: -bounds.height)
// draw string here...
context.restoreGState()
The problem with this is, that the container does not know about the frame's height. When I (for testing) hardcode the height the text now draws correctly, but at its „original“ wrong position at the top of the view. The two identical transformations obviously cancel out each other.
- I also tried to manually apply the scaling and tranlating to the y-position of the text, in addition to what I did in step 2. This revealed another problem: The text now shows at about the right position, but not quite: It now dangles from the imaginary baseline instead of sitting on it.
I also want to state, that even if I would find out how the font metrics come into play here, applying this to my actual code would be a tremendous amount of work, as – as stated – the drawing methods of my objects do not know the height of the view they draw in, so I'd have to pass that as an additional argument, and – most importantly – I would have to write all of that scaling and translating conditionally when compiling for
iOS
– which would counteract my approach to keep the container-code framework-agnostic.
I could go down two different basic routes:
I could rewrite my entire code to work in the flipped coordinate system of
iOS
, and formacOS
I set the .isFlipped parameter on my customNSView
to return true. This would involve a lot of work, and it still would not solve the problem of theAttributedStrings
being upside down.I continue trying to find a way to flip EVERYTHING on the canvas. I would prefer the second route, obviously.
QUESTION:
How do I draw Objects, that are positioned for a cartesian coordinate system (like Cocoa/Quartz 2D uses), into a CGContext
that uses a flipped coodinate system (like UIKIT does), so that all objects of different types (primitives, Glyphs and Strings) are drawn at the correct position and in the correct orientation?
Here is my code. For convenience, instead of showing the UIView
code I show a self containing version for macOS
with the NSView
set to .isFlipped
- the behaviour is - as far as I could examine - exactly the same.
import AppKit
public protocol DrawingContent {
func draw(inContext context: CGContext)
}
public struct TestPath: DrawingContent {
public func draw(inContext context: CGContext) {
context.beginPath()
context.setLineWidth(CGFloat(2))
context.move(to: CGPoint(x: 200, y: 30))
context.addLine(to: CGPoint(x: 250, y: 30))
context.addLine(to: CGPoint(x: 250, y: 60))
context.addLine(to: CGPoint(x: 230, y: 60))
context.strokePath()
}
}
public struct TestText: DrawingContent {
public func draw(inContext context: CGContext) {
let font = CTFontCreateWithName("Helvetica" as CFString, 24, nil)
var attributes: [NSAttributedString.Key: AnyObject] = [NSAttributedString.Key.font : font,
NSAttributedString.Key.foregroundColor : NSColor.black]
let attributedText = NSAttributedString(string: "Testtext", attributes: attributes)
let descender = CTFontGetDescent(font)
let textOrigin = CGPoint(x: 100, y: (30-descender))
attributedText.draw(at: textOrigin)
}
}
public struct TestGlyph: DrawingContent {
public func draw(inContext context: CGContext) {
var font = CTFontCreateWithName("Helvetica" as CFString, 24, nil)
var position = CGPoint(x: 30, y: 30)
var glyph = CGGlyph(36) // Capital Letter A
CTFontDrawGlyphs(font, &glyph, &position, 1, context)
}
}
public class FlippedTestView: NSView {
var drawingContent: [DrawingContent]
override public var isFlipped: Bool {return true}
override public init(frame: CGRect) {
let drawingContent: [DrawingContent] = [TestGlyph(), TestText(), TestPath()]
self.drawingContent = drawingContent
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override open func draw(_ rect: CGRect) {
let currentContext = NSGraphicsContext.current!.cgContext
currentContext.translateBy(x: 0, y: rect.height)
currentContext.scaleBy(x: 1, y: -1)
for element in drawingContent {
element.draw(inContext: currentContext)
}
}
}
@main
class AppDelegate: NSObject, NSApplicationDelegate {
@IBOutlet var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
let testView = FlippedTestView(frame: CGRect(x: 0, y: 0, width: 300, height: 100))
self.window.contentView = testView
self.window.setFrame(CGRect(x: 200, y: 200, width: 300, height: 100), display: true)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}
Share
Improve this question
asked Mar 27 at 23:08
MassMoverMassMover
5772 silver badges21 bronze badges
4
|
2 Answers
Reset to default 2I was able to get this:
It isn't perfect but as a proof of concept it shows that you can get there from here. There are two techniques:
For each of your drawing packets, start by getting the height of the context, which you can obtain by saying:
let height = context.boundingBoxOfClipPath.height
Now you can position your drawing vertically in relation to the size of what you are drawing into.
In the case of your text, which is flipping, you need to add a flip and then remove it when you are finished drawing; this is done by saving the context state at the start and restoring it at the end:
public func draw(inContext context: CGContext) {
let height = context.boundingBoxOfClipPath.height
context.saveGState()
context.translateBy(x: 0, y: height)
context.scaleBy(x: 1, y: -1)
// ...
attributedText.draw(at: textOrigin)
context.restoreGState()
}
However, I do not know exactly where you want the drawing positioned vertically, so I leave it to you to do the necessary arithmetic.
After a lot of experimenting I finally found a working solution myself. I will post my updated code at the end of this post. I have altered it, so that four lines of text are written to different positions within the view, so that the correct positioning relative to each other can also be verified. In addition, I made the view and the window higher.
This is what happens:
Assuming a view of 400 points in height in Cocoa
coordinate space. A text object at a y-position of 30 will sit close to the bottom of the view, with its lower left corner at the specified Point. For convenience, let's ignore the descender, that I subtract in the code to actually put the baseline to 30. The area in the view below 30 is completly void.
Now we want to call the same drawing methods from an UIView
, or – as I do here – from an NSView
which is flipped. The text now undergoes a true odyssey of transformations:
The flipped view will position everything at the given coordinates, but the view's origin is now in the upper left corner. This means, the text will be drawn close to the top of the view, but still correctly orientated, with its lower left corner at the specified point. This means, in contrast to the original positioning, here the area below 30 (which actually means above) will be occupied by the text. This will be important later.
As a side note: The single Glyph, that is drawn with
CTFontDrawGlyphs
draws upside down at this point, which seems wrong to me, but this is solved in the next step.We now have to re-flip the context, so that everything will be drawn at the bottom again. That means, things that originally were drawn 30 points from the bottom must now be drawn at y=370, as 400 is the bottom of the view. We do that by a transformation to the y-axis:
(y * -1) – viewHeigth
. But this does not mean, that the text will be drawn at y=370 with its origin at that position, but instead every „pixel“ of the text will be transformed that way. This results not only in the text being drawn upside down, but it will now occupy the area below the bottom-most 30 points.As a side note: this happens also to the single Glyph, but as this had been drawn already upside down in step 1, here it's a good thing.
To prevent this from happening I step in when the text is drawn and apply another transformation. So the actual coded/calculated y-position that will eventually be passed to the
string.draw(at:)
method still is 30 in Cocoa space. I negate it to -30 and then apply an individual affine transformation to the context, again negating it, so it will become 30 again, but the context's transformation will then not only return the value back to positive 30 but also flip the text's orientation. In Addition, I get the font's height and calculate it in to compensate for the flip at the baseline that ocurred in step 1.And here is the interesting part: The value that finally gets passed to the draw(at:) method is -54 (-30 minus the font height). It now gets the individual transform which sets it to 54. But as we are in flipped coordinat space, 54 would place it close to the top of the screen. Instead the value should be 346 (400 – 54). And how do we get from 54 to 346? By applying the original
affineTransform
, that we already set before calling the draw method:(54 * -1) + 400 = 346
.
I would have thought that the transformations I apply between saveGState
and -restoreGState
replace the ones already in place, but obviously they are cumulative.
So the true path of my textposition of y=30 can best be traced backwards:
directly invert to -30 and translate by the font heigth to -54
invert by context transformation back to 54 – now it's upside down
invert by the outer transformation to -54 and translate by view's height(400) to 346 – now its standing on its feet again.
The reason why the route has to be that convoluted lies in the nature of my original code. If I had separate arrays for primitives, glyphs and strings I simply would recalculate the strings' y-positions and draw them directly, but separating them is not an option.
import Cocoa
@main
class AppDelegate: NSObject, NSApplicationDelegate {
@IBOutlet var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
let testView = FlippedTestView(frame: CGRect(x: 0, y: 0, width: 300, height: 400))
self.window.contentView = testView
self.window.setFrame(CGRect(x: 200, y: 200, width: 300, height: 400), display: true)
}
}
public class FlippedTestView: NSView {
var drawingContent: [DrawingContent]
override public var isFlipped: Bool {return true}
override public init(frame: CGRect) {
let drawingContent: [DrawingContent] = [TestGlyph(), TestText(), TestPath()]
self.drawingContent = drawingContent
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override open func draw(_ rect: CGRect) {
let currentContext = NSGraphicsContext.current!.cgContext
currentContext.translateBy(x: 0, y: rect.height)
currentContext.scaleBy(x: 1, y: -1)
for element in drawingContent {
element.draw(inContext: currentContext)
}
}
}
public protocol DrawingContent {
func draw(inContext context: CGContext)
}
public struct TestPath: DrawingContent {
public func draw(inContext context: CGContext) {
context.beginPath()
context.setLineWidth(CGFloat(2))
context.move(to: CGPoint(x: 200, y: 30))
context.addLine(to: CGPoint(x: 250, y: 30))
context.addLine(to: CGPoint(x: 250, y: 60))
context.addLine(to: CGPoint(x: 230, y: 60))
context.strokePath()
}
}
public struct TestText: DrawingContent {
var stringPositions: [CGPoint] = [
CGPoint(x:100, y: 30),
CGPoint(x:120, y: 50),
CGPoint(x:140, y: 70),
CGPoint(x:130, y: 90)
]
public func draw(inContext context: CGContext) {
let font = CTFontCreateWithName("Helvetica" as CFString, 24, nil)
var attributes: [NSAttributedString.Key: AnyObject] = [NSAttributedString.Key.font : font]
let attributedText = NSAttributedString(string: "Testtext", attributes: attributes)
let descender = CTFontGetDescent(font)
for point in stringPositions {
let textOrigin = CGPoint(x: point.x, y: point.y-descender)
context.saveGState()
context.scaleBy(x: 1, y: -1)
let fontHeight = attributedText.boundingRect(with: CGSize(width: 5000, height: 5000), context: nil).height
let newY = (textOrigin.y * -1) - fontHeight
let newPosition = CGPoint(x: textOrigin.x, y: newY)
attributedText.draw(at: newPosition)
context.restoreGState()
}
}
}
public struct TestGlyph: DrawingContent {
public func draw(inContext context: CGContext) {
var font = CTFontCreateWithName("Helvetica" as CFString, 24, nil)
var position = CGPoint(x: 30, y: 30)
var glyph = CGGlyph(36) // Capital Letter A
CTFontDrawGlyphs(font, &glyph, &position, 1, context)
}
}
版权声明:本文标题:ios - How do I re-use code that draws onto a macOS GraphicsContext to draw onto an UIKit context? - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1744063968a2584645.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
CGContext
from itsGraphicsContext
. – Sweeper Commented Mar 27 at 23:48CTFontDrawGlyphs
withNSAttributedString.draw(at:)
is going to be problematic -- is there a reason you aren't using only one or the other? – DonMag Commented Mar 28 at 20:03