Core Text calculate letter frame in iOS

You did an impressive amount of work in your question and were so close on your own. The problem you were having comes from this line of code where you position the bounding boxes for each frame:

_characterFrames[ic].origin = CGPointMake(startOffset, lineOrigin.y);

The problem with it is that you are overriding whatever offset the frame already had.

If you were to comment out that line you would see that all the frames were positioned more or less in the same place but you would also see that they are not positioned at the exact same place. Some are positioned more to the left or right and some more up or down. This means that the frames for the glyphs have a position of their own.

enter image description here

The solution to your problem is to take the current position of the frames into account when you move them into their correct place on the lines. You can either do it by adding to x and y separately:

_characterFrames[ic].origin.x += startOffset;
_characterFrames[ic].origin.y += lineOrigin.y;

or by offsetting the rectangle:

_characterFrames[ic] = CGRectOffset(_characterFrames[ic],
                                    startOffset, lineOrigin.y);

Now the bounding boxes will have their correct positions:

enter image description here

and you should see that it works for some of the more extreme fonts out there

enter image description here


Swift version

Swift 5, Xcode 11:

 override func draw(_ rect: CGRect) {
        guard let context = UIGraphicsGetCurrentContext() else { return }
   
    
       context.textMatrix = .identity
       context.translateBy(x: 0, y: self.bounds.size.height)
       context.scaleBy(x: 1.0, y: -1.0)
   
    
       let string = "|優勝《ゆうしょう》の|懸《か》かった|試合《しあい》。|Test《テスト》.\nThe quick brown fox jumps over the lazy dog. 12354567890 @#-+"

    
       let attributedString = Utility.sharedInstance.furigana(String: string)
   
       let range = attributedString.mutableString.range(of: attributedString.string)

       attributedString.addAttribute(.font, value: font, range: range)

   
       let framesetter = attributedString.framesetter()
   
       let textBounds = self.bounds.insetBy(dx: 20, dy: 20)
       let frame = framesetter.createFrame(textBounds)
    
//Draw the frame text:
   
       frame.draw(in: context)
           
       let origins = frame.lineOrigins()
   
       let lines = frame.lines()

        context.setStrokeColor(UIColor.red.cgColor)
        context.setLineWidth(0.7)

        for i in 0 ..< origins.count {

            let line = lines[i]
         
            for run in line.glyphRuns() {
            
                let font = run.font
                let glyphPositions = run.glyphPositions()
                let glyphs = run.glyphs()
            
                let glyphsBoundingRects =  font.boundingRects(of: glyphs)
            
//DRAW the bounding box for each glyph:
            
                for k in 0 ..< glyphPositions.count {
                    let point = glyphPositions[k]
                    let gRect = glyphsBoundingRects [k]
            
                    var box = gRect
                    box.origin +=  point + origins[i] + textBounds.origin
                    context.stroke(box)
                        
                }// for k
        
            }//for run
        
       }//for i
    
    }//func draw

Made with a CoreText Swift Wrapper. Full Source: https://github.com/huse360/LetterFrame