• iOS-电子书开发【基于Coretext的排版引擎】 笔记


    前言

    刚接手电子书项目时,和安卓开发者pt Cai老师【aipiti Cai,一个我很敬佩很资深的开发工程师,设计领域:c++、Java、安卓、QT等】共同商议了一下,因为项目要做要同步,移动端【手机端】和PC【电脑端】的同步问题,让我们无法决定该用那种方式去呈现电子书,因为PC要展示的电子书有网络图片,有HTML标签,主要功能是能做标记(涂色、划线、书签等),而且后台数据源返回的只有这一种格式:HTML;所以我们第一时间想到了可以用加载网页的Webview来做;pt Cai老师做了一些基于JS的分页及手势操作,然后对图片进行了适配,但是当我在测试Webview时,效果并不尽人意:

    • Webview渲染比较慢,加载需要一定的等待时间,体验不是很好;
    • Webview内存泄漏比较严重;
    • Webview的与本地的交互,交互是有一定的延时,而且对于不断地传递参数不好控制操作;

     

    引入Coretext

    通过上面的测试,我决定放弃了Webview,用Coretext来尝试做这些排版和操作;我在网上查了很多资料,从对Coretext的基本开始了解,然后查看了猿题库开发者的博客,在其中学到了不少东西,然后就开始试着慢慢的用Coretext来尝试;

    demo

    1.主框架

    做电子书阅读,首先要有一个翻滚阅读页的一个框架,我并没有选择用苹果自带的 UIPageViewController 因为控制效果不是很好,我再Git上找了一个不错的 DZMCoverAnimation,因为是做demo测试,就先选择一个翻滚阅读页做效果,这个覆盖翻页的效果如下:

    2.解析数据源

    首先看一下数据源demo,我要求json数据最外层必须是P标签,P标签不能嵌套P标签,但可以包含Img和Br标签,Img标签内必须含有宽高属性,以便做排版时适配,最终的数据源为

    然后我在项目中用CocoaPods引入解析HTML文件的 hpple 三方库,在解析工具类CoreTextSource中添加解析数据模型和方法,假如上面的这个数据源是一章的内容,我把这一章内容最外层的每个P标签当做一个段落,遍历每个段落,然后在遍历每个段落里面的内容和其他标签;

    CoreTextSource.h

    #import <Foundation/Foundation.h>
    #import <hpple/TFHpple.h>
    
    #import <UIKit/UIKit.h>
    typedef NS_ENUM(NSInteger,CoreTextSourceType){
        ///文本
        CoreTextSourceTypeTxt = 1,
        ///图片
        CoreTextSourceTypeImage
    };
    
    /**
     文本
     */
    @interface CoreTextTxtSource : NSObject
    @property (nonatomic,strong) NSString *content;
    @end
    
    /**
     图片
     */
    @interface CoreTextImgSource : NSObject
    @property (nonatomic,strong) NSString *name;
    @property (nonatomic,assign) CGFloat width;
    @property (nonatomic,assign) CGFloat height;
    @property (nonatomic,strong) NSString *url;
    // 此坐标是 CoreText 的坐标系,而不是UIKit的坐标系
    @property (nonatomic,assign) NSInteger position;
    @property (nonatomic,assign) CGRect imagePosition;
    @end
    
    /**
     段落内容
     */
    @interface CoreTextParagraphSource : NSObject
    @property (nonatomic,assign) CoreTextSourceType type;
    @property (nonatomic,strong) CoreTextImgSource *imgData;
    @property (nonatomic,strong) CoreTextTxtSource *txtData;
    @end
    ///电子书数据源
    @interface CoreTextSource : NSObject
    ///解析HTML格式
    + (NSArray *)arrayReaolveChapterHtmlDataWithFilePath:(NSString *)filePath;
    @end
    View Code

    CoreTextSource.m

    #import "CoreTextSource.h"
    
    @implementation CoreTextImgSource
    
    @end
    @implementation CoreTextParagraphSource
    
    @end
    @implementation CoreTextTxtSource
    
    @end
    
    @implementation CoreTextSource
    
    + (NSArray *)arrayReaolveChapterHtmlDataWithFilePath:(NSString *)filePath{
        NSData  * data   = [NSData dataWithContentsOfFile:filePath];
        
        TFHpple * dataSource = [[TFHpple alloc] initWithHTMLData:data];
        NSArray * elements = [dataSource searchWithXPathQuery:@"//p"];
        
        NSMutableArray *arrayData = [NSMutableArray array];
        
        for (TFHppleElement *element in elements) {
            NSArray *arrrayChild = [element children];
            for (TFHppleElement *elementChild in arrrayChild) {
                CoreTextParagraphSource *paragraphSource = [[CoreTextParagraphSource alloc]init];
                NSString *type = [elementChild tagName];
                if ([type isEqualToString:@"text"]) {
                    CoreTextTxtSource *text = [[CoreTextTxtSource alloc]init];
                    text.content = elementChild.content;
                    paragraphSource.txtData = text;
                    paragraphSource.type = CoreTextSourceTypeTxt;
                }
                else if ([type isEqualToString:@"img"]){
                    CoreTextImgSource *image = [[CoreTextImgSource alloc]init];
                    NSDictionary *dicAttributes = [elementChild attributes];
                    image.name = [dicAttributes[@"src"] lastPathComponent];
                    image.url = dicAttributes[@"src"];
                    image.width = [dicAttributes[@"width"] floatValue];
                    image.height = [dicAttributes[@"height"] floatValue];
                    paragraphSource.imgData = image;
                    paragraphSource.type = CoreTextSourceTypeImage;
                    
                    if (image.width >= (Scr_Width - 30)) {
                        CGFloat ratioHW = image.height/image.width;
                        image.width = Scr_Width - 30;
                        image.height = image.width * ratioHW;
                    }
                }
                else if ([type isEqualToString:@"br"]){
                    CoreTextTxtSource *text = [[CoreTextTxtSource alloc]init];
                    text.content = @"
    ";
                    paragraphSource.txtData = text;
                    paragraphSource.type = CoreTextSourceTypeTxt;
                }
                
                [arrayData addObject:paragraphSource];
            }
            
            ///每个个<P>后加换行
            CoreTextParagraphSource *paragraphNewline = [[CoreTextParagraphSource alloc]init];
            CoreTextTxtSource *textNewline = [[CoreTextTxtSource alloc]init];
            textNewline.content = @"
    ";
            paragraphNewline.txtData = textNewline;
            paragraphNewline.type = CoreTextSourceTypeTxt;
            [arrayData addObject:paragraphNewline];
        }
        
        return arrayData;
    }
    @end
    View Code

    3.图片处理和分页

    添加好CoreTextSource类之后,就可以通过 arrayReaolveChapterHtmlDataWithFilePath 方法获取这一章的所有段落内容;但是还有一个问题,既然用Coretext来渲染,那图片要在渲染之前下载好,从本地获取下载好的图片进行渲染,具体什么时候下载,视项目而定;我在CoreTextDataTools类中添加了图片下载方法,该类主要用于分页;在分页之前,添加每个阅读页的model -> CoreTextDataModel,具体图片的渲染,先详看CoreTextDataTools分页类中 wkj_coreTextPaging 方法和其中引用到的方法;

    CoreTextDataModel.h

    #import <Foundation/Foundation.h>
    
    ///标记显示模型
    @interface CoreTextMarkModel : NSObject
    @property (nonatomic,assign) BookMarkType type;
    @property (nonatomic,assign) NSRange range;
    @property (nonatomic,strong) NSString *content;
    @property (nonatomic,strong) UIColor *color;
    @end
    
    @interface CoreTextDataModel : NSObject
    ///
    @property (nonatomic,assign) CTFrameRef ctFrame;
    @property (nonatomic,strong) NSAttributedString *content;
    @property (nonatomic,assign) NSRange range;
    ///图片数据模型数组 CoreTextImgSource
    @property (nonatomic,strong) NSArray *arrayImage;
    ///标记数组
    @property (nonatomic,copy) NSArray *arrayMark;
    @end
    View Code 

    CoreTextDataModel.m

    #import "CoreTextDataModel.h"
    @implementation CoreTextMarkModel
    
    @end
    
    @implementation CoreTextDataModel
    - (void)setCtFrame:(CTFrameRef)ctFrame{
        if (_ctFrame != ctFrame) {
            if (_ctFrame != nil) {
                CFRelease(_ctFrame);
            }
            CFRetain(ctFrame);
            _ctFrame = ctFrame;
        }
    }
    @end
    View Code

    CoreTextDataTools.h

    ///图片下载
    + (void)wkj_downloadBookImage:(NSArray *)arrayParagraph;
    ///分页
    + (NSArray *)wkj_coreTextPaging:(NSAttributedString *)str
                           textArea:(CGRect)textFrame
               arrayParagraphSource:(NSArray *)arrayParagraph;
    ///根据一个章节的所有段落内容,来生成 AttributedString 包括图片
    + (NSAttributedString *)wkj_loadChapterParagraphArray:(NSArray *)arrayArray;
    View Code

    CoreTextDataTools.m

    #import "CoreTextDataTools.h"
    #import <SDWebImage/UIImage+MultiFormat.h>
    
    @implementation CoreTextDataTools
    + (void)wkj_downloadBookImage:(NSArray *)arrayParagraph{
        dispatch_group_t group = dispatch_group_create();
        // 有多张图片URL的数组
        for (CoreTextParagraphSource *paragraph in arrayParagraph) {
            if (paragraph.type == CoreTextSourceTypeTxt) {
                continue;
            }
            
            dispatch_group_enter(group);
            // 需要加载图片的控件(UIImageView, UIButton等)
            NSData *data = [NSData dataWithContentsOfURL:[NSURL  URLWithString:paragraph.imgData.url]];
            UIImage *image = [UIImage sd_imageWithData:data];
            // 本地沙盒目录
            NSString *path = wkj_documentPath;
            ///创建文件夹
            NSString *folderName = [path stringByAppendingPathComponent:@"wkjimage"];
            
            if (![[NSFileManager defaultManager]fileExistsAtPath:folderName]) {
                
                [[NSFileManager defaultManager] createDirectoryAtPath:folderName  withIntermediateDirectories:YES  attributes:nil error:nil];
                
            }else{
                NSLog(@"有这个文件了");
            }
            
            // 得到本地沙盒中名为"MyImage"的路径,"MyImage"是保存的图片名
            //        NSString *imageFilePath = [path stringByAppendingPathComponent:@"MyImage"];
            
            // 将取得的图片写入本地的沙盒中,其中0.5表示压缩比例,1表示不压缩,数值越小压缩比例越大
            
            folderName = [folderName stringByAppendingPathComponent:[paragraph.imgData.url lastPathComponent]];
            
            BOOL success = [UIImageJPEGRepresentation(image, 0.1) writeToFile:folderName  atomically:YES];
            if (success){
                NSLog(@"写入本地成功");
            }
            
            dispatch_group_leave(group);
            
        }
        // 下载图片完成后, 回到主线
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            // 刷新UI
            
        });
    }
    /**
     CoreText 分页
     str: NSAttributedString属性字符串
     textFrame: 绘制区域
     */
    + (NSArray *)wkj_coreTextPaging:(NSAttributedString *)str
                           textArea:(CGRect)textFrame
               arrayParagraphSource:(NSArray *)arrayParagraph{
        NSMutableArray *arrayCoretext = [NSMutableArray array];
        
        CFAttributedStringRef cfStrRef = (__bridge CFAttributedStringRef)str;
        CTFramesetterRef framesetterRef = CTFramesetterCreateWithAttributedString(cfStrRef);
        CGPathRef path = CGPathCreateWithRect(textFrame, NULL);
        
        int textPos = 0;
        NSUInteger strLength = [str length];
        while (textPos < strLength)  {
            //设置路径
            CTFrameRef frame = CTFramesetterCreateFrame(framesetterRef, CFRangeMake(textPos, 0), path, NULL);
            CFRange frameRange = CTFrameGetVisibleStringRange(frame);
            NSRange range = NSMakeRange(frameRange.location, frameRange.length);
            
            //        [arrayPagingRange addObject:[NSValue valueWithRange:range]];
            //        [arrayPagingStr addObject:[str attributedSubstringFromRange:range]];
        
            
            CoreTextDataModel *model = [[CoreTextDataModel alloc]init];
            model.ctFrame = frame;
            model.range = range;
            model.content = [str attributedSubstringFromRange:range];
            model.arrayImage = [self wkj_arrayCoreTextImgRect:[self wkj_arrayCoreTextImg:arrayParagraph range:range] cfFrame:frame];
            
            [arrayCoretext addObject:model];
            //移动
            textPos += frameRange.length;
            CFRelease(frame);
        }
        CGPathRelease(path);
        CFRelease(framesetterRef);
        //    return arrayPagingStr;
        return arrayCoretext;
    }
    ///获取每页区域内存在的图片
    + (NSArray *)wkj_arrayCoreTextImg:(NSArray *)arrayParagraph
                                      range:(NSRange)range{
        NSMutableArray *array = [NSMutableArray array];
        
        for (CoreTextParagraphSource *paragraph in arrayParagraph) {
            if (paragraph.type == CoreTextSourceTypeTxt) {
                continue;
            }
            
            if (paragraph.imgData.position >= range.location &&
                paragraph.imgData.position < (range.location + range.length)) {
                [array addObject:paragraph.imgData];
            }
        }
        
        return array;
    }
    ///获取每个区域内存在的图片位置
    + (NSArray *)wkj_arrayCoreTextImgRect:(NSArray *)arrayCoreTextImg cfFrame:(CTFrameRef)frameRef{
        NSMutableArray *arrayImgData = [NSMutableArray array];
        
        if (arrayCoreTextImg.count == 0) {
            return arrayCoreTextImg;
        }
        NSArray *lines = (NSArray *)CTFrameGetLines(frameRef);
        NSUInteger lineCount = [lines count];
        CGPoint lineOrigins[lineCount];
        CTFrameGetLineOrigins(frameRef, CFRangeMake(0, 0), lineOrigins);
        int imgIndex = 0;
        CoreTextImgSource * imageData = arrayCoreTextImg[0];
        for (int i = 0; i < lineCount; ++i) {
    
            CTLineRef line = (__bridge CTLineRef)lines[i];
            NSArray * runObjArray = (NSArray *)CTLineGetGlyphRuns(line);
            for (id runObj in runObjArray) {
                CTRunRef run = (__bridge CTRunRef)runObj;
                NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
                CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes valueForKey:(id)kCTRunDelegateAttributeName];
                if (delegate == nil) {///如果代理为空,则未找到设置的空白字符代理
                    continue;
                }
                
    
                
                CoreTextImgSource * metaImgSource = CTRunDelegateGetRefCon(delegate);
                if (![metaImgSource isKindOfClass:[CoreTextImgSource class]]) {
                    continue;
                }
                
                CGRect runBounds;
                CGFloat ascent;
                CGFloat descent;
                runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
                runBounds.size.height = ascent + descent;
                
                CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
                runBounds.origin.x = lineOrigins[i].x + xOffset;
                runBounds.origin.y = lineOrigins[i].y;
                runBounds.origin.y -= descent;
                
                CGPathRef pathRef = CTFrameGetPath(frameRef);
                CGRect colRect = CGPathGetBoundingBox(pathRef);
                
                CGRect delegateBounds = CGRectOffset(runBounds, colRect.origin.x, colRect.origin.y);
                
                imageData.imagePosition = delegateBounds;
                CoreTextImgSource *img = imageData;
                [arrayImgData addObject:img];
                imgIndex++;
                if (imgIndex == arrayCoreTextImg.count) {
                    imageData = nil;
                    break;
                } else {
                    imageData = arrayCoreTextImg[imgIndex];
                }
            }
            
            if (imgIndex == arrayCoreTextImg.count) {
                break;
            }
            
        }
        
        return arrayImgData;
        
    }
    
    
    
    
    ///获取属性字符串字典
    + (NSMutableDictionary *)wkj_attributes{
        CGFloat fontSize = [BookThemeManager sharedManager].fontSize;
        CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
        ///行间距
        CGFloat lineSpacing = [BookThemeManager sharedManager].lineSpace;
        ///首行缩进
        CGFloat firstLineHeadIndent = [BookThemeManager sharedManager].firstLineHeadIndent;
        ///段落间距
        CGFloat paragraphSpacing = [BookThemeManager sharedManager].ParagraphSpacing;
        //换行模式
        CTLineBreakMode lineBreak = kCTLineBreakByCharWrapping;
        const CFIndex kNumberOfSettings = 6;
        CTParagraphStyleSetting theSettings[kNumberOfSettings] = {
            ///行间距
            { kCTParagraphStyleSpecifierLineSpacingAdjustment, sizeof(CGFloat), &lineSpacing },
            { kCTParagraphStyleSpecifierMaximumLineSpacing, sizeof(CGFloat), &lineSpacing },
            { kCTParagraphStyleSpecifierMinimumLineSpacing, sizeof(CGFloat), &lineSpacing },
            ///首行缩进
            { kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(CGFloat), &firstLineHeadIndent },
            ///换行模式
            { kCTParagraphStyleSpecifierLineBreakMode, sizeof(CTLineBreakMode), &lineBreak },
            ///段落间距
            { kCTParagraphStyleSpecifierParagraphSpacing, sizeof(CGFloat), &paragraphSpacing }
        };
        
        CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, kNumberOfSettings);
        
        UIColor * textColor = [BookThemeManager sharedManager].textColor;
        
        NSMutableDictionary * dict = [NSMutableDictionary dictionary];
        dict[(id)kCTForegroundColorAttributeName] = (id)textColor.CGColor;
        dict[(id)kCTFontAttributeName] = (__bridge id)fontRef;
        dict[(id)kCTParagraphStyleAttributeName] = (__bridge id)theParagraphRef;
        CFRelease(theParagraphRef);
        CFRelease(fontRef);
        return dict;
    }
    
    
    
    
    
    
    ///根据一个章节的所有段落内容,来生成 AttributedString 包括图片
    + (NSAttributedString *)wkj_loadChapterParagraphArray:(NSArray *)arrayArray{
        
        NSMutableAttributedString *resultAtt = [[NSMutableAttributedString alloc] init];
        
        for (CoreTextParagraphSource *paragraph in arrayArray) {
            if (paragraph.type == CoreTextSourceTypeTxt) {///文本
                NSAttributedString *txtAtt = [self wkj_parseContentFromCoreTextParagraph:paragraph];
                [resultAtt appendAttributedString:txtAtt];
            }
            else if (paragraph.type == CoreTextSourceTypeImage){///图片
                paragraph.imgData.position = resultAtt.length;
                NSAttributedString *imageAtt = [self wkj_parseImageFromCoreTextParagraph:paragraph];
                [resultAtt appendAttributedString:imageAtt];
            }
        }
        
        return resultAtt;
    }
    
    ///根据段落文本内容获取 AttributedString
    + (NSAttributedString  *)wkj_parseContentFromCoreTextParagraph:(CoreTextParagraphSource *)paragraph{
        NSMutableDictionary *attributes = [self wkj_attributes];
        return [[NSAttributedString alloc] initWithString:paragraph.txtData.content attributes:attributes];
    }
    
    
    /////根据段落图片内容获取 AttributedString 空白占位符
    + (NSAttributedString *)wkj_parseImageFromCoreTextParagraph:(CoreTextParagraphSource *)paragraph{
    
        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 *)(paragraph.imgData));
    
        // 使用0xFFFC作为空白的占位符
        unichar objectReplacementChar = 0xFFFC;
        NSString * content = [NSString stringWithCharacters:&objectReplacementChar length:1];
        NSMutableDictionary * attributes = [self wkj_attributes];
        //    attributes[(id)kCTBackgroundColorAttributeName] = (id)[UIColor yellowColor].CGColor;
        NSMutableAttributedString * space = [[NSMutableAttributedString alloc] initWithString:content attributes:attributes];
        CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space, CFRangeMake(0, 1),
                                       kCTRunDelegateAttributeName, delegate);
        CFRelease(delegate);
        return space;
    }
    
    //+ (NSAttributedString *)wkj_NewlineAttributes{
    //    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 *)(paragraph));
    //
    //    // 使用0xFFFC作为空白的占位符
    //    unichar objectReplacementChar = 0xFFFC;
    //    NSString * content = [NSString stringWithCharacters:&objectReplacementChar length:1];
    //    NSMutableDictionary * attributes = [self wkj_attributes];
    //    //    attributes[(id)kCTBackgroundColorAttributeName] = (id)[UIColor yellowColor].CGColor;
    //    NSMutableAttributedString * space = [[NSMutableAttributedString alloc] initWithString:content attributes:attributes];
    //    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space, CFRangeMake(0, 1),
    //                                   kCTRunDelegateAttributeName, delegate);
    //    CFRelease(delegate);
    //    return space;
    //}
    
    static CGFloat ascentCallback(void *ref){
    //    return [(NSNumber*)[(__bridge NSDictionary*)ref objectForKey:@"height"] floatValue];
        CoreTextImgSource *refP = (__bridge CoreTextImgSource *)ref;
        return refP.height;
    }
    
    static CGFloat descentCallback(void *ref){
        return 0;
    }
    
    static CGFloat widthCallback(void* ref){
    //    return [(NSNumber*)[(__bridge NSDictionary*)ref objectForKey:@"width"] floatValue];
        
        CoreTextImgSource *refP = (__bridge CoreTextImgSource *)ref;
        return refP.width;
    }
    
    @end
    View Code

    添加好CoreTextDataTools类之后,就可以通过 wkj_downloadBookImage 方法来下载图片;图片下载完之后,就可以对每页显示的内容区域进行分页;划线和涂色的一些方法在上一篇中已提到;

        ///获取测试数据源文件
        NSString *path = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
        ///获取该章所有段落内容
        NSArray *arrayParagraphSource = [CoreTextSource arrayReaolveChapterHtmlDataWithFilePath:path];
        ///下载该章中的所有图片
        [CoreTextDataTools wkj_downloadBookImage:arrayParagraphSource];
        ///根据一个章节的所有段落内容,来生成 AttributedString 包括图片
        NSAttributedString *att = [CoreTextDataTools wkj_loadChapterParagraphArray:arrayParagraphSource];
        ///给章所有内容分页 返回 CoreTextDataModel 数组
        NSArray *array = [CoreTextDataTools wkj_coreTextPaging:att textArea:CGRectMake(5, 5, self.view.bounds.size.width - 10, self.view.bounds.size.heigh                     t- 120) arrayParagraphSource:arrayParagraphSource];

    4.效果

  • 相关阅读:
    bzoj 1295 [SCOI2009]最长距离 最短路
    bzoj 3669 [Noi2014]魔法森林
    bzoj 1432 [ZJOI2009]Function 思想
    用JSP输出Hello World
    Web开发基础
    JSP相关背景
    JSP概述
    Java视频播放器的制作
    为JFileChooser设定扩展名过滤
    使用JFileChooser保存文件
  • 原文地址:https://www.cnblogs.com/wangkejia/p/8435435.html
Copyright © 2020-2023  润新知