美文网首页
图文混排的关键:CTRunRef 与 CTRunDelegate

图文混排的关键:CTRunRef 与 CTRunDelegate

作者: 苏沫离 | 来源:发表于2020-08-23 10:06 被阅读0次

    1、文本属性 (Attributes) 的最小单元:CTRunRef

    /// 或者叫做 Glyph Run,是一组相同属性(Attributes)的字形的集合体
    typedef const struct CF_BRIDGED_TYPE(id) __CTRun * CTRunRef;
    

    CTLineRef 是渲染到屏幕上的一行字形的集合,如果再细分,那么CTRunRef就是某一行 CTLineRef 上属性 Attributes 相同的字形的集合体;CTLineRef 是由一个或者多个CTRunRef组成!

    1.1、获取包含的字形

    CTRunRef 是排版时属性 Attributes 相同的基础单位,包括一个或多个字形;

    /// 字形实际上是一个 unsigned short 的类型
    typedef unsigned short CGGlyph;
    
    /// 获取 CTRunRef 包含的字形个数
    CFIndex CTRunGetGlyphCount(CTRunRef run);
    
    /// 获取 CTRunRef 包含的字形 : 该数组长度等于 CTRunGetGlyphCount() 返回值
    const CGGlyph * _Nullable CTRunGetGlyphsPtr(CTRunRef run);
    
    /** 将指定范围的字形复制到用户提供的缓冲区中
     * @param range 指定范围;如果 range.location = 0 ,range.length = CTRunGetGlyphCount ,则全部复制
     *                      如果 range.length = 0 ,则从 range.location 开始复制到结尾;
     * @param buffer 缓冲区,长度要足够使用
    */
    void CTRunGetGlyphs(CTRunRef run,CFRange range,CGGlyph buffer[_Nonnull]);
    
    1.1.1、 获取字符对应的坐标位置

    CTRunRef 可以获取到每个字符对应的位置:相对于 CTLine 的原点的位置:

    /// 获取存储在 CTRunRef 中的每个字形的位置
    const CGPoint * _Nullable CTRunGetPositionsPtr(CTRunRef run);
    
    /// 拷贝存储在 CTRunRef 中的指定范围的字形的位置
    void CTRunGetPositions(CTRunRef run,CFRange range,CGPoint buffer[_Nonnull]);
    
    1.1.2、获取字符对应的索引

    获取 CTRunRef 每个字形的索引:映射到存储区中的字形

    /// 获取在 CTRunRef 中存储的字形索引
    const CFIndex * _Nullable CTRunGetStringIndicesPtr(CTRunRef run);
    
    /// 获取(拷贝)存储在 CTRunRef 中的指定范围的字形的索引
    void CTRunGetStringIndices(CTRunRef run,CFRange range,CFIndex buffer[_Nonnull]);
    
    1.1.3、获取字符对应的 advance

    文字默认排版时,宽度由 advance width指定,但是仅靠 advances 并不足以在 CTLine 中正确地定位字形,因为 CTRunRef 可能具有非单位矩阵,或者 CTLineorigin 可能是非零原点!

    /// 获取存储在 CTRunRef 中的每个字形的 advance
    const CGSize * _Nullable CTRunGetAdvancesPtr(CTRunRef run);
    
    /// 获取(拷贝)存储在 CTRunRef 中的指定范围的字形的 advance
    void CTRunGetAdvances(CTRunRef run,CFRange range,CGSize buffer[_Nonnull]);
    
    /** 获取(拷贝)存储在 CTRunRef 中的指定范围的字形的 advances 和 origins
     * @discussion CTRunRef 的 base advances 和 origins 决定字形的位置,在用于绘图之前需要进行额外的处理。
     *   当前字形的实际位置由其原点从起始位置的偏移量决定,而下一个字形的位置由当前字形base advance 从起始位置的偏移量决定。
     */
    void CTRunGetBaseAdvancesAndOrigins(CTRunRef runRef,CFRange range,
                CGSize advancesBuffer[_Nullable],CGPoint originsBuffer[_Nullable]);
    
    1.2、获取CTRunRef 的文字属性

    获取的文字属性,可能来自 NSAttributeString,也可能来自于内部排版引擎的生成:

    /// 获取 CTRunRef 的属性
    CFDictionaryRef CTRunGetAttributes(CTRunRef run);
    
    1.3、获取CTRunRef 的文字范围

    CTRunRef 可以获取生成时的Range,以便定位到这段文字在整体的位置;

    ///获取用于创建 CTRunRef 的字符范围
    CFRange CTRunGetStringRange(CTRunRef run);
    
    1.4、获取CTRunRef 的排版 size
    /** 获取 CTRunRef 的指定范围的字符的排版边界
     * @param range 指定范围;如果 range.location = 0 ,range.length = CTRunGetGlyphCount ,则是整个 CTRunRef;
     *                      如果 range.length = 0 ,则从 range.location 开始复制到结尾;
     * @param ascent  上行高度;回调函数,如果不需要,可以将其设置为NULL。
     * @param descent 下行高度;基线距字体中最低的字形底部的距离,是一个负值
     * @param leading 行距
     * @result 排版宽度;如果 CTRunRef 或 CFRange 无效,则返回 0
     * @discussion 行高 lineHeight = ascent + |descent| + linegap      
     */
    double CTRunGetTypographicBounds(CTRunRef run, CFRange range, CGFloat * _Nullable ascent,
                                     CGFloat * _Nullable descent,CGFloat * _Nullable leading);
    
    /** 计算 CTRunRef 中指定范围的字形绘制成图像所需要的 bounds :一个紧密包含字形的边界
     * @param context 计算图像 bounds 的上下文,可以传 NULL;
     * @discussion 计算这行文字绘制成图片所需要的最小 size,没有各种边距,是一种是尽可能小的理想状态的size
     * @result 如果行无效,将返回 CGRectNull;
     */
    CGRect CTRunGetImageBounds(CTRunRef run,CGContextRef _Nullable context,CFRange range);
    
    1.5、其它函数
    /// 由 CTRunGetStatus() 传回的位字段,用于指示 CTRunRef 的处理
    typedef CF_OPTIONS(uint32_t, CTRunStatus){
        kCTRunStatusNoStatus = 0, /// 没有特殊的属性 attributes
        kCTRunStatusRightToLeft = (1 << 0), /// 设置文本从右向左书写
        kCTRunStatusNonMonotonic = (1 << 1), ///以某种方式重新排序,字符串索引不再严格地从左到右的递增或从右到左的递减
        kCTRunStatusHasNonIdentityMatrix = (1 << 2) /// CTRunRef 需要在当前 CGContext 中设置一个特定的文本矩阵来进行适当的绘图
    };
    
    /** 获取 CTRunRef 的状态
     * @discussion 除了属性 attributes 之外,CTRunRef 还具有可用于加快某些操作的状态:
     *             知道 CTRunRef 的方向和顺序可以为字符串索引分析提供帮助;
     *             知道位置是否引用标识文本矩阵可以避免额外比较;
     * @note 该状态不是严格必要的,仅仅是为了方便
     */
    CTRunStatus CTRunGetStatus(CTRunRef run);
    
    /** 获取绘制此 CTRunRef 所需的文本矩阵
     * @note 一个CTLine里面会包括多个CTRun,每个CTRun都包括各自的位置信息,
     *       在排版的时候可以通过CTRunGetTextMatrix获取相应的位置,
     *       再通过 CGContextSetTextMatrix() 设置到CGContext
     */
    CGAffineTransform CTRunGetTextMatrix(CTRunRef run);
    
    /** 绘制 CTRunRef 
     * @discussion 还可以通过访问其 glyphs、positions 和text matrix 来复杂的绘制 CTRunRef。
     *      与调用 CTLineDraw() 绘制包含 CTRun 的整个 CTLine 不同;
     *      CTRun 如果有下划线,将不会被绘制,因为下划线可能依赖于该 CTLine 中的其他CTRun;
    */
    void CTRunDraw(CTRunRef run,CGContextRef context,CFRange range);
    

    2、回调代理 CTRunDelegate

    CTRunDelegateCTRunRef 的代理回调,通过 Delegate 可以手动设置 CTRunRefAscentDescentWidth等属性,这是图文混排的基础;插入一个空白的字符,将其字符的大小设置为(width, height),留出对应的大小空白区域,然后在排版结束完通过 CGContextDrawImage() 在对应的位置插入Image 就实现了图文混排的效果;

    typedef const struct CF_BRIDGED_TYPE(id) __CTRunDelegate * CTRunDelegateRef;
    
    2.1、CTRunDelegate 回调函数
    /** 当 CTRunDelegate 的保留计数达到 0 且CTRunDelegate 被释放时的回调函数 
     * @param refCon 创建 CTRunDelegate 的传入的参数,一般是关于 Ascent、Descent、Width 等度量信息;
     */
    typedef void (*CTRunDelegateDeallocateCallback)(void * refCon);
    
    /// 上行高度的回调
    typedef CGFloat (*CTRunDelegateGetAscentCallback)(void * refCon);
    
    /// 下行高度的回调
    typedef CGFloat (*CTRunDelegateGetDescentCallback)(void * refCon);
    
    /// 宽度的回调
    typedef CGFloat (*CTRunDelegateGetWidthCallback)(void * refCon);
    
    ///回调的版本号,作为参数传递给CTRunDelegateCreate() 函数
    enum {
        kCTRunDelegateVersion1 = 1,
        kCTRunDelegateCurrentVersion = kCTRunDelegateVersion1
    };
    
    /** 包含 CTRunDelegate 的回调函数的结构
     * @discussion 这些回调函数由开发者提供,用于在布局期间修改字形度量。
    */
    typedef struct{
        CFIndex version; ///建议设置为 kCTRunDelegateCurrentVersion
        CTRunDelegateDeallocateCallback dealloc; // 设置为 NULL
        CTRunDelegateGetAscentCallback  getAscent; // 设置为 NULL 时,默认为 0
        CTRunDelegateGetDescentCallback getDescent;
        CTRunDelegateGetWidthCallback   getWidth; 
    } CTRunDelegateCallbacks;
    
    2.2、创建代理 CTRunDelegate
    /** 创建一个代理 CTRunDelegate 
     * @param callbacks 该代理的回调
     * @refCon 一般是关于 Ascent、Descent、Width 等度量信息
     * @discussion 该代理常用来占位:保留一片空白区域绘制图片
    */
    CTRunDelegateRef _Nullable CTRunDelegateCreate(const CTRunDelegateCallbacks* callbacks,void * _Nullable refCon);
    
    /// 获取创建 CTRunDelegate 的传入的 refCon:一般是关于 Ascent、Descent、Width 等度量信息
    void * CTRunDelegateGetRefCon(CTRunDelegateRef runDelegate);
    

    3、CTRunRef 函数使用

    3.1、 图文混排中图片的处理

    CoreText 实际上并没有相应API直接将一个图片转换为 CTRun 并进行绘制,它所能做的只是为图片预留响应的空白区域,而真正的绘制则是交由CoreGraphics完成。

    NSAttributedStringKey const kYLAttachmentAttributeName = @"com.yl.attachment";
    
    //富文本中的链接(图片、网页)
    @interface YLAttachment : NSObject
    
    //链接
    @property (nonatomic ,copy) NSString *url;
    
    //网页的标题
    @property (nonatomic ,copy) NSString *title;
    
    //图片的相关信息
    @property (nonatomic ,strong) UIImage *image;
    @property (nonatomic ,assign) CGRect imageFrame;
    
    @end
    
    3.1.1、文字排版时为图片的展示占位
    ///上行高度
    static CGFloat ascentCallback(void *ref){
        YLAttachment *model = (__bridge YLAttachment *)ref;
        return model.imageFrame.size.height;
    }
    
    ///下行高度
    static CGFloat descentCallback(void *ref){
        return 0;
    }
    
    ///图片宽度
    static CGFloat widthCallback(void *ref){
        YLAttachment *model = (__bridge YLAttachment *)ref;
        return model.imageFrame.size.width;
    }
    
    /** 将图片处理为 CoreText
     * @param image 图片
     * @param drawSize 画布的尺寸,图片的宽高不能超出 drawSize
     */
    + (NSAttributedString *)parseImage:(UIImage *)image drawSize:(CGSize)drawSize{
        /**************** 计算图片宽高 **************/
        CGSize imageShowSize = image.size;//屏幕上展示的图片尺寸
        if (image.size.width > drawSize.width) {
            imageShowSize = CGSizeMake(drawSize.width, image.size.height / image.size.width * drawSize.width);
        }
        
        YLAttachment *model = [[YLAttachment alloc]init];
        model.image = image;
        model.imageFrame = CGRectMake(0, 0, imageShowSize.width, imageShowSize.height);
        
        //注意:此处返回的富文本,最主要的作用是占位!
        //为图片的绘制留下空白区域
        CTRunDelegateCallbacks callbacks;
        memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
        callbacks.version = kCTRunDelegateVersion1;//设置回调版本,默认这个
        callbacks.getAscent = ascentCallback;//上行高度
        callbacks.getDescent = descentCallback;//下行高度
        callbacks.getWidth = widthCallback;//图片宽度
        CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)model);
        
        //使用0xFFFC作为空白占位符
        unichar objectReplacementChar = 0xFFFC;
        NSString *content = [NSString stringWithCharacters:&objectReplacementChar length:1];
        NSMutableAttributedString *placeholder = [[NSMutableAttributedString alloc] initWithString:content attributes:@{kYLAttachmentAttributeName:model}];
        CFAttributedStringSetAttribute((CFMutableAttributedStringRef)placeholder, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
        CFRelease(delegate);
        return placeholder;
    }
    
    3.1.2、 矫正图片的坐标错位问题

    绘制图片的时候实际上在一个 CTRunRef 中,以它坐标系为基准,以 origin 点作为原点进行绘制:使用 frameSetter 求出的 image 的坐标是不正确的,需要我们另行计算:

    /** 矫正 CTFrame 中的图片坐标
     * 思路: 遍历 CTFrameRef 中的所有 CTRun,检查 CTRun 否绑定图片,
     *       如果是,根据 CTRun 所在 CTLine 的 origin 以及在 CTLine 中的横向偏移量计算出 CTRun 的原点,
     *       加上其尺寸即为该CTRun的尺寸
     */
    + (void)setImageFrametWithCTFrame:(CTFrameRef)frame{
        CFArrayRef lines = CTFrameGetLines(frame);
        int lineCount = (int)CFArrayGetCount(lines);
        CGPoint points[lineCount];
        CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), points);
        for (int i = 0; i < lineCount; i ++) {//外层for循环,为了取到所有的 CTLine
            CTLineRef line = CFArrayGetValueAtIndex(lines, i);
            
            CFArrayRef glyphRuns = CTLineGetGlyphRuns(line);
            int runCount = (int)CFArrayGetCount(glyphRuns);
            for (int j = 0; j < runCount ; j ++) {//内层for循环,检查每个 CTRun
                CTRunRef run = CFArrayGetValueAtIndex(glyphRuns, j);
                CFDictionaryRef attributes = CTRunGetAttributes(run);
                CTRunDelegateRef delegate = CFDictionaryGetValue(attributes, kCTRunDelegateAttributeName);;//获取代理属性
                if (delegate == nil) {
                    continue;
                }
                YLAttachment *model = CTRunDelegateGetRefCon(delegate);
                if (![model isKindOfClass:[YLAttachment class]]) {
                    continue;
                }
                
                CGPoint linePoint = points[i];//获取当前 CTLine 的原点
                CGFloat ascent;  //上行高度
                CGFloat descent; //下行高度
                CGFloat leading = 0; //行距
                CGRect boundsRun;
                //获取宽、高
                boundsRun.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, &leading);
                boundsRun.size.height = ascent + fabs(descent) + leading;
                //获取对应 CTRun 的 X 偏移量
                CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
                boundsRun.origin.x = linePoint.x + xOffset;
                boundsRun.origin.y = linePoint.y - descent - leading;//图片原点
                CGPathRef path = CTFrameGetPath(frame);//获取绘制区域
                CGRect colRect = CGPathGetBoundingBox(path);//获取绘制区域边框
                model.imageFrame = CGRectOffset(boundsRun, colRect.origin.x, colRect.origin.y);//设置图片坐标
            }
        }
    }
    
    3.1.3、 获取 CTFrameRef 中的所有图片插件
    ///获取 CTFrameRef 中的所有图片插件
    + (NSMutableArray<YLAttachment *> *)getImagesWithCTFrame:(CTFrameRef)frame{
        NSMutableArray *resultArray = [NSMutableArray array];
        
        CFArrayRef lines = CTFrameGetLines(frame);
        int lineCount = (int)CFArrayGetCount(lines);
        CGPoint points[lineCount];
        CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), points);
        for (int i = 0; i < lineCount; i ++) {//外层for循环,为了取到所有的 CTLine
            CTLineRef line = CFArrayGetValueAtIndex(lines, i);
            
            CFArrayRef glyphRuns = CTLineGetGlyphRuns(line);
            int runCount = (int)CFArrayGetCount(glyphRuns);
            for (int j = 0; j < runCount ; j ++) {//内层for循环,检查每个 CTRun
                CTRunRef run = CFArrayGetValueAtIndex(glyphRuns, j);
                CFDictionaryRef attributes = CTRunGetAttributes(run);
                if (attributes) {
                    if (CFDictionaryContainsKey(attributes, kYLAttachmentAttributeName)) {
                        YLAttachment *attachment = CFDictionaryGetValue(attributes, kYLAttachmentAttributeName);;//获取属性
                        if (attachment && attachment.image) {
                            [resultArray addObject:attachment];
                        }
                    }
                }
            }
        }
        return resultArray;
    }
    
    3.1.4、 绘制图片
    - (void)drawRect:(CGRect)rect{
         //1.获取当前绘图上下文
        CGContextRef context = UIGraphicsGetCurrentContext();
        
        //2.旋转坐坐标系(默认和UIKit坐标是相反的)
        CGContextSetTextMatrix(context, CGAffineTransformIdentity);//设置当前文本矩阵
        CGContextTranslateCTM(context, 0, CGRectGetHeight(rect));//文本沿y轴移动
        CGContextScaleCTM(context, 1.0, -1.0);//文本翻转成为CoreText坐标系
            
        //3.绘制文字
        CTFrameDraw(_frameRef, context);
        
        //4.绘制图片
        [[YLCoreText getImagesWithCTFrame:_frameRef] enumerateObjectsUsingBlock:^(YLAttachment * _Nonnull attachment, NSUInteger idx, BOOL * _Nonnull stop) {
            CGContextDrawImage(context, attachment.imageFrame, attachment.image.CGImage);
        }];
    }
    
    阅读器点击链接 阅读器仿真翻页 阅读器覆盖翻页 阅读器其它翻页
    点击链接.gif 仿真翻页.gif 覆盖翻页.gif 平移、滚动、无效果等翻页.gif

    第一篇 CoreText的简单了解
    第二篇 CoreText 排版与布局
    第三篇 CTLineRef 的函数库及使用
    第四篇 图文混排的关键 CTRunRef 与 CTRunDelegate
    Demo:小说阅读器的文字分页、图文混排

    相关文章

      网友评论

          本文标题:图文混排的关键:CTRunRef 与 CTRunDelegate

          本文链接:https://www.haomeiwen.com/subject/tchpjktx.html