[Pyobjc-dev] Call for help
Brought to you by:
ronaldoussoren
|
From: Nico W. <nic...@gm...> - 2009-02-01 04:00:46
|
Hi, I want to load a font from a ttf file and then draw some text using this font, and I want to do that in Python using the PyObjC bridge. My program already does some drawing; it uses NSBitmapImageReps and NSGraphicsContext for that. The font I'm trying to load is http://damieng.com/blog/2008/05/26/envy-code-r-preview-7-coding-font-released . If someone has an idea how I could get this to work, please speak up. Here's what I tried so far: 1.) Activate the font through ATS, then load it through NSFont through its postscript name. This is what most programs written in ObjC do – they call ATSFontActivateFromFileReference to load a ttf file and then can use NSFont to load the font given its postscript name. The problem is that there seems to be no built-in python binding for ATS. This fails: import ATS Now, ATS.framework is a subframework in the ApplicationServices framework, but this fails as well: import ApplicationServices Fair enough. The pyobjc documentation shows how to write your own wrapper: import objc b = objc.loadBundle("ApplicationServices", globals(), bundle_path=objc.pathForFramework(u'/System/Library/Frameworks/ ApplicationServices.framework/')) For reasons I don't quite understand, this doesn't load the ATS subframework methods, though (might be related that there is no ATS.bridgesupport in /System/Library/Frameworks/ ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/ Versions/A/Resources/BridgeSupport). I tried to create the bridgesupport file on my own, using gen_bridge_metadata -f /System/Library/Frameworks/ ApplicationServices.framework/Frameworks/ATS.framework -o ATS.bridgesupport This gave some linking errors, and didn't work. `man gen_bridge_metadata` even says that there might be problems if you want to create bridgesupport files for subframeworks of umbrella frameworks, but doesn't really say how to work around them. I tried explicitly setting special compiler flags with gen_bridge_metadata -f /System/Library/Frameworks/ ApplicationServices.framework/Frameworks/ATS.framework -o ATS.bridgesupport -c ' -F /System/Library/Frameworks/ ApplicationServices.framework/Frameworks/ATS.framework -framework ApplicationServices' , but in vain. Now, pyobjc has a `loadBundleFunctions` function that can be used to load functions from a bundle. One of the examples in /Developer/ Examples/Python/PyObjC/Cocoa/AppKit/PyObjCLauncher/LaunchServices.py does use this function to load a specific function from a bundle. So I tried loading ATSFontActivateFromFileReference thusly (in its own file `myfile.py`): import objc __bundle__ = objc.loadBundle("ApplicationServices", globals(), bundle_path=objc.pathForFramework(u'/System/Library/ Frameworks/ApplicationServices.framework/')) functions = [( u'ATSFontActivateFromFileReference', 's^{FSRef=[80C]}II^vIo^I' )] objc.loadBundleFunctions(__bundle__, globals(), functions) (See http://developer.apple.com/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/chapter_7_section_1.html for how the type encodings work; you can use `NSLog(@"FSRef*: %s", @encode(FSRef*));` to let gcc show you the type encoding for a given type). To some extent, this even worked. After `import myfile`, I can call ATSFontActivateFromFileReference on the `myfile` module. Cool. Now I need an FSRef of my font to pass it to the function. I tried: from Cocoa import * from CoreFoundation import * import myfile url = NSURL.fileURLWithPath_(u'Envy Code R Bold.ttf') status, fsref = CFURLGetFSRef(url, None) kATSFontContextLocal = 2 kATSFontFormatUnspecified = 0 kATSOptionFlagsDefault = 0 print myfile.ATSFontActivateFromFileReference(fsref, kATSFontContextLocal, kATSFontFormatUnspecified, None, kATSOptionFlagsDefault, None) However, this fails with the error message "# ValueError: depythonifying 'pointer', got 'Carbon.File.FSRef'". The problem is that I claimed that ATSFontActivate... takes a '^{FSRef=[80C]}' ("pointer to a struct FSRef which is 80 bytes"), but CFURLGetFSRef returns a Carbon.File.FSRef object instead (which, as far as I understand, is something from the days before pyobjc). Now, this object has a `.data()` function that returns the raw 80 bytes that make up the FSRef as a str, but there seems to be no way to pass raw data to a function that was loaded through loadBundleFunctions – so I have a reference to the function and all data I need to call it, but I can't get my data through the bridge. Looks like a dead end (and http://osdir.com/ml/python.pyobjc.devel/2006-12/msg00024.html seems to agree. Then again, that is a pre-Leopard post). 2.) Load the font through CoreGraphics, then convert it to a CoreText CTFont, which is toll-free bridged to NSFont, and use that for drawing. CoreGraphics provides a function to load a font from a file, which I can then convert to a CTFont, which is toll-free bridged to an NSFont. So I should be able to load a font this way and then use Cocoa's normal drawInRect_withAttributes_ function to draw it. I tried: from CoreGraphics import * from CoreText import * fontDataProvider = CGDataProviderCreateWithFilename('Envy Code R.ttf') customFont = CGFontCreateWithDataProvider(fontDataProvider) print customFont # looks good so far! ctFont = CTFontCreateWithGraphicsFont(customFont, 12.0, None, None) print CTFontCopyPostScriptName(ctFont) # looks still good print ctFont However, printing the ctFont or using it as an NSFont gives the error message "<Error>: can't find font object for font id 13828539." – and after that, even printing the postscript name returns "(null)". This does also happen when I call the same functions in an Objective-C program, so this is a limitation of the Cocoa runtime. Another dead end. 3.) Load the font through CoreGraphics, and also draw the text through CoreGraphics It's possible to get a CoreGraphics context from an NSGraphicsContext. Then I can set the current font of the cg context to the font I loaded from the ttf file. The first few steps even work: from CoreGraphics import * from CoreText import * # contains CGContextSetFont for some reason fontDataProvider = CGDataProviderCreateWithFilename('Envy Code R.ttf') customFont = CGFontCreateWithDataProvider(fontDataProvider) cgContext = NSGraphicsContext.currentContext().graphicsPort() CGContextSetFont(cgContext, customFont) Now, CG has a function CGContextShowTextAtPoint that draws a given string (char*) at a given position into a cg context. Sounds great, but as it turns out this function doesn't work for fonts loaded from a file – it only works for activated system fonts loaded through CGContextSelectFont (source: http://developer.apple.com/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_text/chapter_18_section_4.html ). There is a function CGContextShowGlyphsAtPoint that works, but it takes a list of glyphs (indices into the font's glyph table – which glyph corresponds to which character is font-dependent!). There is no official function that converts a string to a list of glyphs for a given font, however. But there is an undocumented function: CGFontGetGlyphsForUnichars. As this function is undocumented, we have to load it ourselves again: # CoreGraphics is part of the ApplicationServices umbrella framework functions = [ # bool CGFontGetGlyphsForUnichars(CGFontRef, unichar[], CGGlyph[], size_t); ( u'CGFontGetGlyphsForUnichars', 'B^{CGFont=}^So^SI'), ] # This gives a bus error btw: #import CoreGraphics #objc.loadBundleFunctions(CoreGraphics, globals(), functions) AppServices = objc.loadBundle("ApplicationServices", globals(), bundle_path=objc.pathForFramework(u'/System/Library/ Frameworks/ApplicationServices.framework/')) objc.loadBundleFunctions(AppServices, globals(), functions) Now, let's try to call it: print CGFontGetGlyphsForUnichars(customFont, 'py', None, 2) This gives "ValueError: depythonifying 'pointer', got 'str'" as error. I couldn't figure out how to pass python text to a function that takes `unichar*`. This is probably possible somehow, but since this needs undocumented functions anyways, I'm not too happy with this approach anyway. 4.) Give up, write a small C-based module that calls ATSFontActivateFromFileReference given an string, call that from Python, and call it a day That's what I ended up doing. It works, but it is strange that this seems necessary for such mundane a task. Thanks, Nico |