登录 立即注册
金钱:

Code4App-iOS开发-iOS 开源代码库-iOS代码实例搜索-iOS特效示例-iOS代码例子下载-Code4App.com

查看: 256|回复: 6

iOS-电子书开发 笔记,ios电子书格式

[复制链接]

221

主题

455

帖子

1024

金钱

手工艺人

发表于 2018-2-9 17:30:32 | 显示全部楼层 |阅读模式
前言

        刚接手电子书项目时,和安卓开发者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




[Java] 查看源文件 复制代码
#import 
#import 

#import 
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




[Java] 查看源文件 复制代码
#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 = #"\n";
                paragraphSource.txtData = text;
                paragraphSource.type = CoreTextSourceTypeTxt;
            }
            
            [arrayData addObject:paragraphSource];
        }
        
        ///每个个< P>后加换行
        CoreTextParagraphSource *paragraphNewline = [[CoreTextParagraphSource alloc]init];
        CoreTextTxtSource *textNewline = [[CoreTextTxtSource alloc]init];
        textNewline.content = #"\n";
        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




[Java] 查看源文件 复制代码
#import 

///标记显示模型
#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




[Java] 查看源文件 复制代码
#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




[Java] 查看源文件 复制代码
///图片下载
+ (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




[Java] 查看源文件 复制代码
#import "CoreTextDataTools.h"
#import 

#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 = range.location &amp;&amp;
            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), &amp;ascent, &amp;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), &amp;lineSpacing },
        { kCTParagraphStyleSpecifierMaximumLineSpacing, sizeof(CGFloat), &amp;lineSpacing },
        { kCTParagraphStyleSpecifierMinimumLineSpacing, sizeof(CGFloat), &amp;lineSpacing },
        ///首行缩进
        { kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(CGFloat), &amp;firstLineHeadIndent },
        ///换行模式
        { kCTParagraphStyleSpecifierLineBreakMode, sizeof(CTLineBreakMode), &amp;lineBreak },
        ///段落间距
        { kCTParagraphStyleSpecifierParagraphSpacing, sizeof(CGFloat), &amp;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(&amp;callbacks, 0, sizeof(CTRunDelegateCallbacks));
    callbacks.version = kCTRunDelegateVersion1;
    callbacks.getAscent = ascentCallback;
    callbacks.getDescent = descentCallback;
    callbacks.getWidth = widthCallback;
    CTRunDelegateRef delegate = CTRunDelegateCreate(&amp;callbacks, (__bridge void *)(paragraph.imgData));

    // 使用0xFFFC作为空白的占位符
    unichar objectReplacementChar = 0xFFFC;
    NSString * content = [NSString stringWithCharacters:&amp;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(&amp;callbacks, 0, sizeof(CTRunDelegateCallbacks));
//    callbacks.version = kCTRunDelegateVersion1;
//    callbacks.getAscent = ascentCallback;
//    callbacks.getDescent = descentCallback;
//    callbacks.getWidth = widthCallback;
//    CTRunDelegateRef delegate = CTRunDelegateCreate(&amp;callbacks, (__bridge void *)(paragraph));
//
//    // 使用0xFFFC作为空白的占位符
//    unichar objectReplacementChar = 0xFFFC;
//    NSString * content = [NSString stringWithCharacters:&amp;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 方法来下载图片;图片下载完之后,就可以对每页显示的内容区域进行分页;划线和涂色的一些方法在上一篇中已提到;



[Java] 查看源文件 复制代码
    ///获取测试数据源文件
    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.效果

        


        

322

主题

583

帖子

2403

金钱

手工艺人

发表于 2018-2-9 17:31:05 | 显示全部楼层
内容很好,棒棒哒

298

主题

516

帖子

2423

金钱

手工艺人

发表于 2018-2-9 17:32:21 | 显示全部楼层
帮帮顶顶!!

215

主题

455

帖子

1142

金钱

手工艺人

发表于 2018-2-9 17:33:33 | 显示全部楼层
我是来学习的

309

主题

554

帖子

2479

金钱

手工艺人

发表于 2018-2-9 17:34:22 | 显示全部楼层
写的真的很不错

307

主题

556

帖子

2416

金钱

手工艺人

发表于 2018-2-9 17:35:36 | 显示全部楼层
code4app好的代码demo真的很多,谢谢啦~

292

主题

538

帖子

2423

金钱

手工艺人

发表于 2018-2-9 17:36:34 | 显示全部楼层
好好 学习了 确实不错
*滑动验证:
您需要登录后才可以回帖 登录 | 立即注册 新浪微博登陆

本版积分规则

关闭

每日头条

通过邮件订阅最新 Code4App 信息
上一条 /4 下一条

广告投放| Github|申请友链|手机版|Code4App ( 粤ICP备15117877号-1 )

快速回复 返回顶部 返回列表