CoreText 實現圖文混排

NSTextView和Attribued String


第一次接觸蘋果系的富文本編程是在寫Mac平臺上的一個輸入框的時候,輸入框中的文字能夠設置各類樣式,並能夠在文字中間插入圖片,好在Mac的AppKit中提供了NSTextView這個支持富文本編輯器控件。此控件背後是經過什麼方式來描述富文本的呢?答案是NSAttributedString,不少編程語言都提供了AttributedString的概念。NSAttributedString比NSString多了一個Attribute的概念,一個NSAttributedString的對象包含不少的屬性,每個屬性都有其對應的字符區域,在這裏是使用NSRange來進行描述的。下面是一個NSTextView顯示富文本的例子 html

NSMutableAttributedString *attributedString = [[[NSMutableAttributedString alloc] initWithString:@"測試富文本顯示"] autorelease]; 
//爲全部文本設置字體
[attributedString addAttribute:NSFontAttributeName value:[NSFont systemFontOfSize:24] range:NSMakeRange(0, [attributedString length])]; 
//將「測試」兩字字體顏色設置爲藍色 
[attributedString addAttribute:NSForegroundColorAttributeName value:[NSColor blueColor] range:NSMakeRange(0, 2)]; 
//將「富文本」三個字字體顏色設置爲紅色 
[attributedString addAttribute:NSForegroundColorAttributeName value:[NSColor redColor] range:NSMakeRange(2, 3)]; 

//在「測」和「試」兩字之間插入一張圖片 
NSString *imageName = @"taobao.png"; 
NSFileWrapper *imageFileWrapper = [[[NSFileWrapper alloc] initRegularFileWithContents:[[NSImage imageNamed:imageName] TIFFRepresentation]] autorelease]; 
imageFileWrapper.filename = imageName;
imageFileWrapper.preferredFilename = imageName; 

NSTextAttachment *imageAttachment = [[[NSTextAttachment alloc] initWithFileWrapper:imageFileWrapper] autorelease];
NSAttributedString *imageAttributedString = [NSAttributedString attributedStringWithAttachment:imageAttachment]; 
[attributedString insertAttributedString:imageAttributedString atIndex:1]; 
/* 其實插入圖片附件以後 attributedString的長度增長了1 變成了8,因此能夠預見其實圖片附件屬性對應的內容應該是一個長度的字符 Printing description of attributedString: 測{  
    NSColor = "NSCalibratedRGBColorSpace 0 0 1 1";  
    NSFont = "\"LucidaGrande 24.00 pt. P [] (0x10051bfd0) fobj=0x101e687f0, spc=7.59\""; 
}{  
    NSAttachment = "<NSTextAttachment: 0x101e0c9c0> \"taobao.png\""; 
}試{  
    NSColor = "NSCalibratedRGBColorSpace 0 0 1 1";  
    NSFont = "\"LucidaGrande 24.00 pt. P [] (0x10051bfd0) fobj=0x101e687f0, spc=7.59\""; 
}富文本{  
    NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";  
    NSFont = "\"LucidaGrande 24.00 pt. P [] (0x10051bfd0) fobj=0x101e687f0, spc=7.59\""; 
}顯示{  
    NSFont = "\"LucidaGrande 24.00 pt. P [] (0x10051bfd0) fobj=0x101e687f0, spc=7.59\""; 
} */ 
[_textView insertText:attributedString];

還有就是NSAttributedString提供對全部屬性進行遍歷的方法,也提供了計算在特定size下渲染實際所佔的區域(boundingRectWithSize:options:) 這些這裏就不介紹了。 從上面的代碼能夠看出其實Mac下的富文本的渲染並非很複雜,只要將Attributed String理解和使用好,其他的事情都交給NSTextView來作了,你徹底不用考慮其底層是如何取渲染的。可是在iOS平臺上就沒有這麼幸運了,雖然iOS從3。2開始也提供了NSAttributedString,可是並無相似NSTextView這樣的控件直接來渲染Attributed String。 這個時候你就得使用Core Text了。 編程


Core Text


下面討論的Core Text相關編程都是特指在iOS平臺下。 Core Text是和Core Graphics配合使用的,通常是在UIView的drawRect方法中的Graphics Context上進行繪製的。 且Core Text真正負責繪製的是文本部分,圖片仍是須要本身去手動繪製,因此你必須關注不少繪製的細節部分。 app

一.Core Text知識準備

在進入任何一個新的編程領域以前,咱們確定要先接觸相關的領域模型的知識。好比你軟件是進行科學計算的,那麼你就必須理解大量的數學原理;若是你的軟件是搞銀行系統,那麼你就得事先了解相關的銀行的業務知識。這些都是不可避免的事情。一般狀況下領域知識具備較高的通用性。但在特定的環境下,某些知識點也會被特殊處理。 Core Text是用來進行文字精細排版的,因此瞭解文字相關的知識也不可避免。 編程語言

1.字符(Character)和字形(Glyphs)

排版系統中文本顯示的一個重要的過程就是字符到字形的轉換,字符是信息自己的元素,而字形是字符的圖形表徵,字符還會有其它表徵好比發音。 字符在計算機中其實就是一個編碼,某個字符集中的編碼,好比Unicode字符集,就囊括了大都數存在的字符。 而字形則是圖形,通常都存儲在字體文件中,字形也有它的編碼,也就是它在字體中的索引。 一個字符能夠對應多個字形(不一樣的字體,或者同種字體的不一樣樣式:粗體斜體等);多個字符也可能對應一個字形,好比字符的連寫( Ligatures)。 
 
Roman Ligatures 編輯器

下面就來詳情看看字形的各個參數也就是所謂的字形度量Glyph Metrics 函數

 

  • bounding box(邊界框 bbox),這是一個假想的框子,它儘量緊密的裝入字形。
  • baseline(基線),一條假想的線,一行上的字形都以此線做爲上下位置的參考,在這條線的左側存在一個點叫作基線的原點,
  • ascent(上行高度)從原點到字體中最高(這裏的高深都是以基線爲參照線的)的字形的頂部的距離,ascent是一個正值
  • descent(下行高度)從原點到字體中最深的字形底部的距離,descent是一個負值(好比一個字體原點到最深的字形的底部的距離爲2,那麼descent就爲-2)
  • linegap(行距),linegap也能夠稱做leading(其實準確點講應該叫作External leading),行高lineHeight則能夠經過 ascent + |descent| + linegap 來計算。

一些Metrics專業知識還能夠參考Free Type的文檔 Glyph metrics,其實iOS就是使用Free Type庫來進行字體渲染的。 測試

以上圖片和部分概念來自蘋果文檔 Querying Font Metrics ,Text Layout 字體

2.座標系

首先不得不說 蘋果編程中的座標系花樣百出,常常讓開發者措手不及。 傳統的Mac中的座標系的原點在左下角,好比NSView默認的座標系,原點就在左下角。但Mac中有些View爲了其實現的便捷將原點變換到左上角,像NSTableView的座標系座標原點就在左上角。iOS UIKit的UIView的座標系原點在左上角。 
往底層看,Core Graphics的context使用的座標系的原點是在左下角。而在iOS中的底層界面繪製就是經過Core Graphics進行的,那麼座標系列是如何變換的呢? 在UIView的drawRect方法中咱們能夠經過UIGraphicsGetCurrentContext()來得到當前的Graphics Context。drawRect方法在被調用前,這個Graphics Context被建立和配置好,你只管使用即是。若是你細心,經過CGContextGetCTM(CGContextRef c)能夠看到其返回的值並非CGAffineTransformIdentity,經過打印出來看到值爲 ui

Printing description of contextCTM:
(CGAffineTransform) contextCTM = {
        a = 1
        b = 0
        c = 0
        d = -1
        tx = 0
        ty = 460
}

這是非retina分辨率下的結果,若是是若是是retina上面的a,d,ty的值將會乘2,若是是iPhone 5,ty的值會再大些。 可是做用都是同樣的就是將上下文空間座標系進行了flip,使得本來左下角原點變到左上角,y軸正方向也變換成向下。 編碼

上面說了一大堆,下面進入正題,Core Text一開始即是定位於桌面的排版系統,使用了傳統的原點在左下角的座標系,因此它在繪製文本的時候都是參照左下角的原點進行繪製的。 可是iOS的UIView的drawRect方法的context被作了次flip,若是你啥也不作處理,直接在這個context上進行Core Text繪製,你會發現文字是鏡像且上下顛倒。 
 
因此在UIView的drawRect方法中的context上進行Core Text繪製以前須要對context進行一次Flip。 

這裏再說起一個函數CGContextSetTextMatrix,它能夠用來爲每個顯示的字形單獨設置變形矩陣。

3.NSMutableAttributedString 和 CFMutableAttributedStringRef

Core Foundation和Foundation中的有些數據類型只須要簡單的強制類型轉換就能夠互換使用,這類類型咱們叫他們爲Toll-Free Bridged Types。 
CFMutableAttributedStringRef和NSMutableAttributedString就是其中的一對,Core Foundation的接口基本是C的接口,功能強大,可是使用起來沒有Foundation中提供的Objc的接口簡單好使,因此不少時候咱們可使用高層接口組織數據,而後將其傳給低層函數接口使用。

二.Core Text對象模型

這節主要來看看Core Text繪製的一些細節問題了,首先是Core Text繪製的流程: 

  • framesetter framesetter對應的類型是 CTFramesetter,經過CFAttributedString進行初始化,它做爲CTFrame對象的生產工廠,負責根據path生產對應的CTFrame
  • CTFrame CTFrame是能夠經過CTFrameDraw函數直接繪製到context上的,固然你能夠在繪製以前,操做CTFrame中的CTLine,進行一些參數的微調
  • CTLine 能夠看作Core Text繪製中的一行的對象 經過它能夠得到當前行的line ascent,line descent ,line leading,還能夠得到Line下的全部Glyph Runs
  • CTRun 或者叫作 Glyph Run,是一組共享想相同attributes(屬性)的字形的集合體

上面說了這麼多對也沒一個東西和圖片繪製有關係,其實吧,Core Text自己並不支持圖片繪製,圖片的繪製你還得經過Core Graphics來進行。只是Core Text能夠經過CTRun的設置爲你的圖片在文本繪製的過程當中留出適當的空間。這個設置就使用到CTRunDelegate了,看這個名字大概就能夠知道什麼意思了,CTRunDelegate做爲CTRun相關屬性或操做擴展的一個入口,使得咱們能夠對CTRun作一些自定義的行爲。爲圖片留位置的方法就是加入一個空白的CTRun,自定義其ascent,descent,width等參數,使得繪製文本的時候留下空白位置給相應的圖片。而後圖片在相應的空白位置上使用Core Graphics接口進行繪製。 
使用CTRunDelegateCreate能夠建立一個CTRunDelegate,它接收兩個參數,一個是callbacks結構體,一個是全部callback調用的時候須要傳入的對象。 callbacks的結構體爲CTRunDelegateCallbacks,主要是包含一些回調函數,好比有返回當前run的ascent,descent,width這些值的回調函數,至於函數中如何鑑別當前是哪一個run,能夠在CTRunDelegateCreate的第二個參數來達到目的,由於CTRunDelegateCreate的第二個參數會做爲每個回調調用時的入參。

三.Core Text實戰

這裏使用Core Text實現一個和以前NSTextView顯示相似的圖文混排的例子。

直接貼上代碼你們體會下:

void RunDelegateDeallocCallback( void* refCon ){ 
} 
CGFloat RunDelegateGetAscentCallback( void *refCon ){ 
    NSString *imageName = (NSString *)refCon; 
    return [UIImage imageNamed:imageName].size.height; 
} 
CGFloat RunDelegateGetDescentCallback(void *refCon){ 
    return 0; 
} 
CGFloat RunDelegateGetWidthCallback(void *refCon){ 
    NSString *imageName = (NSString *)refCon; 
    return [UIImage imageNamed:imageName].size.width; 
} 
- (void)drawRect:(CGRect)rect { 
    CGContextRef context = UIGraphicsGetCurrentContext(); 

    //這四行代碼只是簡單測試drawRect中context的座標系 CGContextSetRGBFillColor (context, 1, 0, 0, 1); 
    CGContextFillRect (context, CGRectMake (0, 200, 200, 100 ));
    GContextSetRGBFillColor (context, 0, 0, 1, .5); 
    CGContextFillRect (context, CGRectMake (0, 200, 100, 200));

    CGContextSetTextMatrix(context, CGAffineTransformIdentity);//設置字形變換矩陣爲CGAffineTransformIdentity,也就是說每個字形都不作圖形變換 
    CGAffineTransform flipVertical = CGAffineTransformMake(1,0,0,-1,0,self.bounds.size.height);
    CGContextConcatCTM(context, flipVertical);//將當前context的座標系進行flip 

    NSMutableAttributedString *attributedString = [[[NSMutableAttributedString alloc] initWithString:@"測試富文本顯示"] autorelease]; 

    //爲全部文本設置字體 
    //[attributedString addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:24] range:NSMakeRange(0, [attributedString length])]; // 6.0+ 
    UIFont *font = [UIFont systemFontOfSize:24]; 
    CTFontRef fontRef = CTFontCreateWithName((CFStringRef)font.fontName, font.pointSize, NULL); 
    [attributedString addAttribute:(NSString *)kCTFontAttributeName value:(id)fontRef range:NSMakeRange(0, [attributedString length])]; 

    //將「測試」兩字字體顏色設置爲藍色 //[attributedString addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:NSMakeRange(0, 2)]; //6.0+ [attributedString addAttribute:(NSString *)kCTForegroundColorAttributeName value:(id)[UIColor blueColor].CGColor range:NSMakeRange(0, 2)]; 

    //將「富文本」三個字字體顏色設置爲紅色 
    //[attributedString addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(2, 3)]; //6.0+ 
    [attributedString addAttribute:(NSString *)kCTForegroundColorAttributeName value:(id)[UIColor redColor].CGColor range:NSMakeRange(2, 3)]; 

    //爲圖片設置CTRunDelegate,delegate決定留給圖片的空間大小 
    NSString *taobaoImageName = @"taobao.png"; 
    CTRunDelegateCallbacks imageCallbacks; 
    NSMutableAttributedString *imageAttributedString = [[NSMutableAttributedString alloc] initWithString:@" "];//空格用於給圖片留位置 
    [imageAttributedString addAttribute:(NSString *)kCTRunDelegateAttributeName value:(id)runDelegate range:NSMakeRange(0, 1)]; CFRelease(runDelegate); 

    [imageAttributedString addAttribute:@"imageName" value:taobaoImageName range:NSMakeRange(0, 1)]; 

    [attributedString insertAttributedString:imageAttributedString atIndex:1]; 

    CTFramesetterRef ctFramesetter = CTFramesetterCreateWithAttributedString((CFMutableAttributedStringRef)attributedString); 

    CGMutablePathRef path = CGPathCreateMutable(); 
    CGRect bounds = CGRectMake(0.0, 0.0, self.bounds.size.width, self.bounds.size.height); 
    CGPathAddRect(path, NULL, bounds); 

    CTFrameRef ctFrame = CTFramesetterCreateFrame(ctFramesetter,CFRangeMake(0, 0), path, NULL); 
    CTFrameDraw(ctFrame, context); 

    CFArrayRef lines = CTFrameGetLines(ctFrame); 
    CGPoint lineOrigins[CFArrayGetCount(lines)];
    CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, 0), lineOrigins); 

    for (int i = 0; i < CFArrayGetCount(lines); i++) { 
        CTLineRef line = CFArrayGetValueAtIndex(lines, i); 
        CGFloat lineAscent; 
        CGFloat lineDescent; 
        CGFloat lineLeading; 
        CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, &lineLeading); 
        CFArrayRef runs = CTLineGetGlyphRuns(line); 
        for (int j = 0; j < CFArrayGetCount(runs); j++) { 
            CGFloat runAscent; 
            CGFloat runDescent; 
            CGPoint lineOrigin = lineOrigins[i]; 
            CTRunRef run = CFArrayGetValueAtIndex(runs, j); 
            NSDictionary* attributes = (NSDictionary*)CTRunGetAttributes(run);
            CGRect runRect; 
            runRect.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0,0), &runAscent, &runDescent, NULL); 
            runRect=CGRectMake(lineOrigin.x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL), lineOrigin.y - runDescent, runRect.size.width, runAscent + runDescent); 

            NSString *imageName = [attributes objectForKey:@"imageName"]; 

            //圖片渲染邏輯 
            if (imageName) { 
                UIImage *image = [UIImage imageNamed:imageName]; 
                if (image) { 
                    CGRect imageDrawRect; 
                    imageDrawRect.size = image.size; 
                    imageDrawRect.origin.x = runRect.origin.x + lineOrigin.x;
                    imageDrawRect.origin.y = lineOrigin.y; 
                    CGContextDrawImage(context, imageDrawRect, image.CGImage); 
                } 
            } 
        } 
    } 
    CFRelease(ctFrame); 
    CFRelease(path); 
    CFRelease(ctFramesetter); 
}
相關文章
相關標籤/搜索