Using objective-c CGContextRef drawing in ui.ImageContext?
I have a need to do some drawing using some of the abilities that the objective-c CGContext api has available to it, but I am having trouble figuring out how to be able to make calls to the various CGContext-related functions.
I found this old thread, which does some things with a graphics context:
But it doesn't illustrate very clearly how it manages to successfully call functions like:
It seems like I can call:
context = objc_util.c.UIGraphicsGetCurrentContext()
but if I try to use that context ref value in any CGContext draw function, like
it crashes hard. I was pretty sure that was invalid...but I'm not clear on how to call plain objective-c functions like this. There's examples of more complex creation/calling of functions in that thread I linked to above:
def CGBitmapContextCreate(baseAddress, width, height, param_0, bytesPerRow, colorSpace, flags): func = c.CGBitmapContextCreate func.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_void_p, ctypes.c_int32] func.restype = ctypes.c_void_p print ObjCInstance(colorSpace).ptr result = func(baseAddress, width, height, param_0, bytesPerRow, ObjCInstance(colorSpace).ptr, flags) print result if result is not None: return result else: raise RuntimeError('Failed to create context')
But I'm not clear on when/how/why you use that sort of setup.
For the non objc functions (things inside objc_util.c, which are c functions and symbols), you almost always need to define argtypes and restype, unless everything is c_void_p.
Check out https://github.com/zacbir/geometriq/tree/master/geometriq/backends _quartz.py and coregraphics.py, which have already have the functions defined, and many useful constants. Also, for those standard libraries, you can often search GitHub, and things like pyobjc, or pybee/Rubicon sometimes have wrappers written out. You can also use cffi, and pass in the actual headers.
Thanks, that's what I was looking for.
What is "cffi", exactly? A command-line to generate the info from the header somehow?
Cffi is a python module that parses c headers and generates the appropiate python functions/argtypes/restype . @dgelessus probably is the best reference for getting this to work in pythonista.
I tend to manually transcode stuff into ctypes - but it can be sometimes annoying to dig up all the various constants, etc
@JonB nevermind, could've just googled it first. C Foreign Function Interface...cool stuff.
Just for completeness, in case anyone reads through this thread:
objc_util.c is the right place to access raw functions like this, of course, but as @JonB said, you have to be sure to set the right argtypes/restype for the function in order to call it successfully.
So, for example, for my attempt to call UIGraphicsGetCurrentContext(), I needed:
UIGraphicsGetCurrentContext = objc_util.c.UIGraphicsGetCurrentContext
UIGraphicsGetCurrentContext.restype = ctypes.c_void_p
UIGraphicsGetCurrentContext.argtypes = 
which ensures that the function will return a reference to a C struct (in this case a CGContextRef), which will be a c_void_p type. The function takes no arguments, so an empty list is fine for argtypes.
Now, to use that context reference to draw something in a pythonista ui.ImageContext, I can do something like:
import ui import objc_util import ctypes UIGraphicsGetCurrentContext = objc_util.c.UIGraphicsGetCurrentContext UIGraphicsGetCurrentContext.restype = ctypes.c_void_p UIGraphicsGetCurrentContext.argtypes =  CGContextFillRect = objc_util.c.CGContextFillRect CGContextFillRect.restype = None CGContextFillRect.argtypes = [ctypes.c_void_p, objc_util.CGRect] def drawWithGraphicsContext(): image = None with ui.ImageContext(300,300) as context: path = ui.Path.rect(10,10,280,280) path.line_width = 1 path.stroke() c = UIGraphicsGetCurrentContext() r = objc_util.CGRect(objc_util.CGPoint(0,0),objc_util.CGSize(200,200)) CGContextFillRect(c, r) image = context.get_image() return image m = drawWithGraphicsContext() m.show()
and the lovely intermixing of pythonista and objc calls works fine.
@shinyformica, thanks for sharing! I am curious: what magical drawing tools are you going to be able to use now?
@shinyformica the same for me 👍
@mikael, for the moment, I wanted to be able to use the nice gradient drawing functions that CGContext's provide. So, for example, I can draw a nice clean, fast, linear gradient between two colors with:
UIGraphicsGetCurrentContext = objc_util.c.UIGraphicsGetCurrentContext UIGraphicsGetCurrentContext.restype = ctypes.c_void_p UIGraphicsGetCurrentContext.argtypes =  CGColorSpaceCreateDeviceRGB = objc_util.c.CGColorSpaceCreateDeviceRGB CGColorSpaceCreateDeviceRGB.restype = c_void_p CGColorSpaceCreateDeviceRGB.argtypes =  CGGradientCreateWithColors = objc_util.c.CGGradientCreateWithColors CGGradientCreateWithColors.restype = c_void_p CGGradientCreateWithColors.argtypes = [c_void_p, c_void_p, ctypes.POINTER(ctypes.c_float)] CGContextDrawLinearGradient = objc_util.c.CGContextDrawLinearGradient CGContextDrawLinearGradient.restype = None CGContextDrawLinearGradient.argtypes = [c_void_p, c_void_p, objc_util.CGPoint, objc_util.CGPoint, objc_util.NSUInteger] with ui.ImageContext(300,300) as context: c = UIGraphicsGetCurrentContext() colorSpace = CGColorSpaceCreateDeviceRGB() start = objc_util.UIColor.colorWithRed_green_blue_alpha_(1.0,0.0,0.0,1.0) end = objc_util.UIColor.colorWithRed_green_blue_alpha_(0.0,0.0,1.0,1.0) colors = [start.CGColor(), end.CGColor()] gradient = CGGradientCreateWithColors(colorSpace, objc_util.ns(colors), None) CGContextDrawLinearGradient(c, gradient, objc_util.CGPoint(0,0), objc_util.CGPoint(300,0), 0)
There's radial gradients as well, of course.
The above code works fine...but I did encounter one thing which I don't understand, and it's probably just because I don't fully get how python/ctypes/objc type conversion works...
In the above code, on this line:
gradient = CGGradientCreateWithColors(colorSpace, objc_util.ns(colors), None)
it should be possible to set that "None" parameter to an array of floats representing the normalized locations of the colors in the gradient. However, the docs say that final parameter is passed in as "const float *" which meant I thought I had to do something like this:
c_float_p = ctypes.POINTER(ctypes.c_float) locations = (ctypes.c_float * 2)(0.0,1.0) gradient = CGGradientCreateWithColors( colorSpace, objc_util.ns(colors), ctypes.cast(locations, c_float_p))
which should produce the same result as the previous version...but it doesn't, it ends up making a gradient with the second color coming first, and as a tiny sliver...so I must be mistaken in how I am creating and passing that locations parameter.
Here's the apple docs on that CGGradientCreateWithColors() function:
In answer to my own question at the end there: that "locations" parameter is not a "const float *", it is actually "const CGFloat *" which can be 32 or 64 bits depending on platform architecture, and therefore a C float is not necessarily compatible...so I needed to explicitly use the CGFloat datatype:
CGFloat_p = ctypes.POINTER(objc_util.CGFloat) CGGradientCreateWithColors = objc_util.c.CGGradientCreateWithColors CGGradientCreateWithColors.restype = c_void_p CGGradientCreateWithColors.argtypes = [c_void_p, c_void_p, CGFloat_p] locations = (objc_util.CGFloat * 2)(0.0,1.0) gradient = CGGradientCreateWithColors(colorSpace, objc_util.ns(colors), ctypes.cast(locations, CGFloat_p))
and it works fine. As always, thanks all!