iOS中的核心文本计算字母框架

iOS中的核心文本计算字母框架,ios,objective-c,ipad,core-text,glyph,Ios,Objective C,Ipad,Core Text,Glyph,我需要为NSAttributedString(核心文本)中的每个字符(glyph)计算精确的边界框。 将用于解决类似问题(核心文本选择等)的一些代码组合在一起后,结果相当不错,但只有少数帧(红色)得到了正确计算: 大多数帧在水平方向或垂直方向(通过一点点)错位。原因是什么?如何完善此代码?: -(void)recalculate{ // get characters from NSString NSUInteger len = [_attributedString.strin

我需要为NSAttributedString(核心文本)中的每个字符(glyph)计算精确的边界框。 将用于解决类似问题(核心文本选择等)的一些代码组合在一起后,结果相当不错,但只有少数帧(红色)得到了正确计算:

大多数帧在水平方向或垂直方向(通过一点点)错位。原因是什么?如何完善此代码?:

-(void)recalculate{

    // get characters from NSString
    NSUInteger len = [_attributedString.string length];
    UniChar *characters = (UniChar *)malloc(sizeof(UniChar)*len);
    CFStringGetCharacters((__bridge CFStringRef)_attributedString.string, CFRangeMake(0, [_attributedString.string length]), characters);

    // allocate glyphs and bounding box arrays for holding the result
    // assuming that each character is only one glyph, which is wrong
    CGGlyph *glyphs = (CGGlyph *)malloc(sizeof(CGGlyph)*len);
    CTFontGetGlyphsForCharacters(_font, characters, glyphs, len);

    // get bounding boxes for glyphs
    CTFontGetBoundingRectsForGlyphs(_font, kCTFontDefaultOrientation, glyphs, _characterFrames, len);
    free(characters); free(glyphs);

    // Measure how mush specec will be needed for this attributed string
    // So we can find minimun frame needed
    CFRange fitRange;
    CGSize s = CTFramesetterSuggestFrameSizeWithConstraints(_framesetter, rangeAll, NULL, CGSizeMake(W, MAXFLOAT), &fitRange);

    _frameRect = CGRectMake(0, 0, s.width, s.height);
    CGPathRef framePath = CGPathCreateWithRect(_frameRect, NULL);
    _ctFrame = CTFramesetterCreateFrame(_framesetter, rangeAll, framePath, NULL);
    CGPathRelease(framePath);


    // Get the lines in our frame
    NSArray* lines = (NSArray*)CTFrameGetLines(_ctFrame);
    _lineCount = [lines count];

    // Allocate memory to hold line frames information:
    if (_lineOrigins != NULL)free(_lineOrigins);
    _lineOrigins = malloc(sizeof(CGPoint) * _lineCount);

    if (_lineFrames != NULL)free(_lineFrames);
    _lineFrames = malloc(sizeof(CGRect) * _lineCount);

    // Get the origin point of each of the lines
    CTFrameGetLineOrigins(_ctFrame, CFRangeMake(0, 0), _lineOrigins);

    // Solution borrowew from (but simplified):
    // https://github.com/twitter/twui/blob/master/lib/Support/CoreText%2BAdditions.m


    // Loop throught the lines
    for(CFIndex i = 0; i < _lineCount; ++i) {

        CTLineRef line = (__bridge CTLineRef)[lines objectAtIndex:i];

        CFRange lineRange = CTLineGetStringRange(line);
        CFIndex lineStartIndex = lineRange.location;
        CFIndex lineEndIndex = lineStartIndex + lineRange.length;

        CGPoint lineOrigin = _lineOrigins[i];
        CGFloat ascent, descent, leading;
        CGFloat lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);


        // If we have more than 1 line, we want to find the real height of the line by measuring the distance between the current line and previous line. If it's only 1 line, then we'll guess the line's height.
        BOOL useRealHeight = i < _lineCount - 1;
        CGFloat neighborLineY = i > 0 ? _lineOrigins[i - 1].y : (_lineCount - 1 > i ? _lineOrigins[i + 1].y : 0.0f);
        CGFloat lineHeight = ceil(useRealHeight ? abs(neighborLineY - lineOrigin.y) : ascent + descent + leading);

        _lineFrames[i].origin = lineOrigin;
        _lineFrames[i].size = CGSizeMake(lineWidth, lineHeight);

        for (int ic = lineStartIndex; ic < lineEndIndex; ic++) {

            CGFloat startOffset = CTLineGetOffsetForStringIndex(line, ic, NULL);
            _characterFrames[ic].origin = CGPointMake(startOffset, lineOrigin.y);
        }
    }
}


#pragma mark - Rendering Text:

-(void)renderInContext:(CGContextRef)context contextSize:(CGSize)size{

    CGContextSaveGState(context);

    // Draw Core Text attributes string:
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextTranslateCTM(context, 0, CGRectGetHeight(_frameRect));
    CGContextScaleCTM(context, 1.0, -1.0);

    CTFrameDraw(_ctFrame, context);

    // Draw line and letter frames:
    CGContextSetStrokeColorWithColor(context, [UIColor colorWithRed:0.0 green:0.0 blue:1.0 alpha:0.5].CGColor);
    CGContextSetLineWidth(context, 1.0);

    CGContextBeginPath(context);
    CGContextAddRects(context, _lineFrames, _lineCount);
    CGContextClosePath(context);
    CGContextStrokePath(context);

    CGContextSetStrokeColorWithColor(context, [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.5].CGColor);
    CGContextBeginPath(context);
    CGContextAddRects(context, _characterFrames, _attributedString.string.length);
    CGContextClosePath(context);
    CGContextStrokePath(context);

    CGContextRestoreGState(context);

}
-(作废)重新计算{
//从NSString获取字符
NSUTEGER len=[\u attributedString.string长度];
UniChar*字符=(UniChar*)malloc(sizeof(UniChar)*len);
CFStringGetCharacters((uu桥CFStringRef)attributedString.string,CFRangeMake(0,[[uAttributedString.string长度]),字符);
//分配用于保存结果的图示符和边界框数组
//假设每个字符只有一个标志符号,这是错误的
cglyph*glyphs=(cglyph*)malloc(sizeof(cglyph)*len);
CTFontGetGlyphsForCharacters(_字体、字符、字形、len);
//获取glyph的边界框
CTFontGetBoundingRects字体(_字体,kCTFontDefaultOrientation,glyphs,_字符框,len);
自由(字符);自由(字形);
//测量此属性化字符串需要多少mush specec
//所以我们可以找到所需的最小帧
CFRange fitRange;
CGSize s=CTFramesetterSuggestFrameSizeWithConstraints(_framesetter,rangeAll,NULL,CGSizeMake(W,MAXFLOAT),&fitRange);
_frameRect=CGRectMake(0,0,s.宽度,s.高度);
CGPathRef framePath=CGPathCreateWithRect(_frameRect,NULL);
_ctFrame=CTFramesetterCreateFrame(_framesetter,rangeAll,framePath,NULL);
cgpathlease(framePath);
//把线放在我们的框架里
NSArray*行=(NSArray*)CTFrameGetLines(_ctFrame);
_lineCount=[行数];
//分配内存以保存行帧信息:
如果(_lineOrigins!=NULL)自由(_lineOrigins);
_lineOrigins=malloc(sizeof(CGPoint)*\u lineCount);
如果(_线框!=NULL)空闲(_线框);
_线框=malloc(sizeof(CGRect)*\u线数);
//获取每条线的原点
ctFrameGetLineOriginates(_ctFrame,CFRangeMake(0,0),_LineOriginates);
//解决方案借鉴(但简化):
// https://github.com/twitter/twui/blob/master/lib/Support/CoreText%2BAdditions.m
//环线
对于(CFIndex i=0;i<_lineCount;++i){
CTLineRef行=(_桥CTLineRef)[行对象索引:i];
CFRange lineRange=CTLineGetStringRange(行);
CFIndex lineStartIndex=lineRange.location;
CFIndex lineEndIndex=lineStartIndex+lineRange.length;
CGPoint lineOrigin=_lineOrigins[i];
上升、下降、领先;
CGFloat线宽=CtlineGettyGraphicBounds(直线、上升、下降和领先);
//如果我们有多条线,我们想通过测量当前线和上一条线之间的距离来找到线的实际高度。如果只有一条线,那么我们将猜测线的高度。
BOOL useRealHeight=i<\u行数-1;
cgloat neighborLineY=i>0?_lineOrigins[i-1].y:(_lineCount-1>i?_lineOrigins[i+1].y:0.0f);
CGFloat lineHeight=ceil(useRealHeight?abs(neightrliney-lineOrigin.y):上升+下降+领先);
_线框[i]。原点=线原点;
_线框[i].size=CGSizeMake(线宽、线宽);
对于(int ic=lineStartIndex;ic
你在问题上做了大量的工作,而且你自己也做得很好。您遇到的问题来自这一行代码,其中您为每个帧定位边界框:

_characterFrames[ic].origin = CGPointMake(startOffset, lineOrigin.y);
它的问题是,您正在覆盖帧已有的任何偏移

如果您要注释掉该行,您将看到所有帧或多或少都位于同一位置,但您也会看到它们并非位于完全相同的位置。有些被定位在更左边或右边,有些则向上或向下。这意味着图示符的帧有自己的位置

问题的解决方案是在将帧移动到线上的正确位置时考虑帧的当前位置。您可以通过分别添加到x和y来执行此操作:

_characterFrames[ic].origin.x += startOffset;
_characterFrames[ic].origin.y += lineOrigin.y;
或通过偏移矩形:

_characterFrames[ic] = CGRectOffset(_characterFrames[ic],
                                    startOffset, lineOrigin.y);
现在,边界框将具有正确的位置:

你应该看到它适用于一些更极端的字体

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
override func draw(rect:CGRect){
guard let context=UIGraphicsGetCurrentCo