23

Suppose I have a custom font "Foo" that I'm using in my iOS App. I've added it to my project, plist, etc. and I'm able to render UILabels and such with it just fine.

Now, if I want to find out a sequence of points that would "trace out" the letter 'P' in that font, how would I get that sequence of points? For example, suppose I wanted to use a CGPath to draw the letter 'P' slowly like a plotter would ...

I just need a sequence of CGPoints in an array that if drawn as a path would draw a 'P'.

I realize that for some letters like 'T', this may not make sense, since the pen would have to be lifted to cross the 'T'. So maybe I need a sequence of CGPaths ...

Any hints on how to accomplish this?

Thanks!

El Developer
  • 3,345
  • 1
  • 21
  • 40
Joel
  • 2,285
  • 2
  • 21
  • 22

3 Answers3

43

Going from the letter "P" to a sequence of points involves several steps. You'll need to use Core Text.

  1. Create a CTFont. Since iOS 7, you can use a UIFont where a CTFont is needed (they are “toll-free bridged”). You can also create a CTFont directly from a CGFont using the CTFontCreateWithGraphicsFont function, or by name using CTFontCreateWithName (or using a few other methods).

  2. Get the glyphs for the letter using the CTFontGetGlyphsForCharacters function. For the letter "P" there should be just one glyph. For some characters in non-English scripts you may get multiple (combining) glyphs.

  3. Use the CTFontCreatePathForGlyph function to get a CGPath for the glyph.

  4. Use CGPathApply to enumerate the elements of the path.

  5. Convert each line, quad curve, and cubic curve element of the path to a sequence of points. Apple doesn't provide any public API for doing this. You'll need to do it yourself. For straight line elements it's easy. For curve elements, you will need to do some research if you don't already know how to render a Bézier curve. For example, see convert bezier curve to polygonal chain?.

We can experiment with this easily in a Swift playground:

import UIKit
import CoreText
import XCPlayground

let font = UIFont(name: "HelveticaNeue", size: 64)!

var unichars = [UniChar]("P".utf16)
var glyphs = [CGGlyph].init(repeating: 0, count: unichars.count)
let gotGlyphs = CTFontGetGlyphsForCharacters(font, &unichars, &glyphs, unichars.count)
if gotGlyphs {
    let cgpath = CTFontCreatePathForGlyph(font, glyphs[0], nil)!
    let path = UIBezierPath(cgPath: cgpath)
    print(path)
    XCPlaygroundPage.currentPage.captureValue(value: path, withIdentifier: "glyph \(glyphs[0])")
}

Result:

<UIBezierPath: 0x7fbc89e0d370; <MoveTo {11.072000000000001, 23.808}>,
 <LineTo {11.072000000000001, 40.576000000000001}>,
 <LineTo {22.975999999999999, 40.576000000000001}>,
 <QuadCurveTo {30.560000000000002, 38.432000000000002} - {28.16, 40.576000000000001}>,
 <QuadCurveTo {32.960000000000001, 32.192} - {32.960000000000001, 36.288000000000004}>,
 <QuadCurveTo {30.560000000000002, 25.920000000000002} - {32.960000000000001, 28.096}>,
 <QuadCurveTo {22.975999999999999, 23.808} - {28.16, 23.744}>,
 <Close>,
 <MoveTo {4.992, 45.695999999999998}>,
 <LineTo {4.992, 0}>,
 <LineTo {11.072000000000001, 0}>,
 <LineTo {11.072000000000001, 18.687999999999999}>,
 <LineTo {25.024000000000001, 18.687999999999999}>,
 <QuadCurveTo {35.488, 22.208000000000002} - {31.936, 18.623999999999999}>,
 <QuadCurveTo {39.039999999999999, 32.192} - {39.039999999999999, 25.792000000000002}>,
 <QuadCurveTo {35.488, 42.143999999999998} - {39.039999999999999, 38.591999999999999}>,
 <QuadCurveTo {25.024000000000001, 45.695999999999998} - {31.936, 45.695999999999998}>,
 <Close>

P glyph path

Maciek Czarnik
  • 5,950
  • 2
  • 37
  • 50
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • @RobMayoff Excellent answer! Just what I was also looking for! Since you seem to know your CGPaths, do you know how I could add a line to the just created path? Say back to the starting point for example. Simply adding: `let pathP = CGPathCreateMutableCopy(CGPathCreateMutableCopy(cgpath)) CGPathAddLineToPoint(pathP, nil, 0, 0)`doesn't seem to work.` – user594883 May 07 '16 at 06:13
  • What does “doesn't seem to work” mean? You need to post a new question containing your code, the output (of dumping your modified path), and explaining what is wrong with the output. – rob mayoff May 07 '16 at 06:18
  • @RobMayoff http://stackoverflow.com/questions/37088660/swift-how-to-add-points-to-a-closed-cgpath – user594883 May 07 '16 at 12:41
  • I am using your code in my project,and i got letter "P", but it was flipped. I printed my layer contentsAreFlipped property, and it returns true. How can i fix this issue? let line : CAShapeLayer = CAShapeLayer() line.path = path.cgPath line.strokeColor = UIColor.blue.cgColor line.fillColor = UIColor.clear.cgColor line.position = self.view.center self.view.layer.addSublayer(line) – banu Sep 24 '18 at 04:28
  • It's fixed i added this code line.isGeometryFlipped = true. – banu Sep 24 '18 at 18:56
  • i want to do the same thing but reversed, i just want to extract the drawn path into text/glyph. how can i do that ? please help ? – MALIKK HABIB UR REHMAN Dec 17 '21 at 11:39
3

For those who are looking the same solution in Objective C

    UIFont *font = [UIFont fontWithName:@"HelveticaNeue" size:64];
    CGFontRef fontref = CGFontCreateWithFontName((__bridge CFStringRef)font.fontName);
    NSString *unichars = @"I";
    CFStringRef yourFriendlyCFString = (__bridge CFStringRef)unichars;
    CGGlyph glyphs = CGFontGetGlyphWithGlyphName(fontref, yourFriendlyCFString);
    CTFontRef fontCT = CTFontCreateWithName((__bridge CFStringRef)font.fontName, font.pointSize, NULL);
    CGPathRef cgpath = CTFontCreatePathForGlyph(fontCT, glyphs, nil);
    UIBezierPath *path = [UIBezierPath bezierPathWithCGPath:cgpath];
    NSLog(@"Complete path For p is %@", path);
    CGPathApply(cgpath, (__bridge void * _Nullable)(bezierPoints), MyCGPathApplierFunc);
    NSLog(@"Points on path %@", bezierPoints);
    for(NSValue *point in bezierPoints){

       //Do your work
    }
PrafulD
  • 548
  • 5
  • 13
3

rob's answer updated for swift 4 + vertical flipping for CoreGraphics uses an inverted coordinate system :

    var unichars = [UniChar]("P".utf16)
    var glyphs = [CGGlyph](repeating: 0, count: unichars.count)
    let gotGlyphs = CTFontGetGlyphsForCharacters(font, &unichars, &glyphs, unichars.count)
    if gotGlyphs {
        let cgpath = CTFontCreatePathForGlyph(font, glyphs[0], nil)!
        var inverse = CGAffineTransform(scaleX: 1, y: -1).translatedBy(x: 0, y: -cgpath.boundingBox.height - 1)
        let path = UIBezierPath(cgPath: cgpath.copy(using: &inverse)!)
        print(path)
floydaddict
  • 1,147
  • 1
  • 7
  • 5