UMD格式与解析详解

UMD格式与解析详解

 

 

Powered by rhythmzhang, May 8th,2015

本文是对于UMD格式结构分析,并针对iOS平台利用Object-C解析UMD文件,给出完整流程与实现。

本篇文章包括以下部分:

1.       前言

2.       UMD结构说明

3.       解析UMD(以Objective-C语言为例)

1.       前言:

UMD是一种在几年前较为常见的电子书格式,尽管现在它已经逐渐被遗忘了。UMD主要分为3种格式类型:纯文本格式,漫画/写真集格式,连环画(文字+图片)格式。本文只讨论纯文本格式(即通用的小说格式)的umd文件的解析过程与格式结构分析。

UMD文件本质是经过zlib压缩后的压缩数据,并且按照特定的先后顺序来排列小说文章的结构与内容。小说的内容被顺序的分成有序且连续的数据块。UMD文件编码为UNICODE(UCS-2)。

2.       UMD结构说明:

UMD基本结构如下图(umd文件的数据块结构按照该图由上往下一一对应)

2.1   文件标识符

UMD文件的前4个字节(第1到第4字节,本文所有计数均从1开始,而非从0开始)一定为0xde9a9b89,若前4字节不为此标识,则一定不是UMD文件。

2.2   UMD类型标识

10个字节为类型标识。其值若为0x0001则表示是纯文本UMD文件,若为0x0002则表示为动漫UMD

2.3   小说基本属性

从第13字节开始(含当前字节),为小说属性块。依次描述小说的标题,作者,出版年,月,日,日,出版商,零售商信息。每个属性块由5个固定字节与若干接在属性后的内容段构成(内容段为unicode编码)。详细见下图。

其中属性标识符的值意义如下表,

属性标识符

描述

0x0002

标题

0x0003

作者

0x0004

出版年份

0x0005

出版月份

0x0006

出版日

0x0007

小说类型

0x0008

出版商

0x0009

零售商

0x000b

小说未压缩时的内容总长度(字节)

注意:属性内容长度实际应该为(N-5);

标识符0x000b的内容段的长度一定为4,此属性值不需要单独读取N值。

2.4   小说章节目录

小说基本属性结构后面紧跟的便是描述小说章节目录信息的字段,

章节基本信息:(18字节)

上图中小说章节目录的章节便宜标志的值一定为0x0083.

小说章节数目n=(N-9)/4;

章节偏移量:(n*4字节)

读取完章节数目后接着就是n个描述每个章节偏移量的32位数字offset,依次读取即可。

章节标题:(18字节)

紧跟在章节标题描述信息的这个18个字节之后,便是n个描述章节的标题长度与标题内容,结构如下:(n*L字节)

2.5   小说章节正文

紧跟在小说章节信息后的便是章节正文内容,均为zlib压缩后的数据,需要对数据进行zip解压。

UMD的正文内容是将小说的所有章节内容叠加为一个连续的字符串,然后分块的使用zlib压缩并存储,理论上每个数据块最大大小为32768字节。

此处小说章节正文结构如下图:

上图中,每个数据块的结构又如下图:

上图中数据块分隔/结束标识的具体结构为:

写入1字节’#’,2字节的0x00f1,以及18个无用字节;

或写入1字节’#’,2字节的0x000a,以及6个无用字节;

或写入1字节’#’,2字节的0x0081(表示正文数据块已经结束),以及其后若干个与解析无关的数据(对于解析UMD格式文件则可忽略,若为写

入则必须按照标准要求写入,此处则不予讨论,仅仅表示可以直接忽略这若干字节数据,如果你对于如何写入umd文件感兴趣,可以参考阅读 http://www.iteye.com/topic/465443 )。

2.6   小说封面图片

2.5步骤后循环读取若干字节(如循环读取1字节),直到读到某个字节为’#’,则表示此时可能是小说封面数据的开始。

其结构如下图:

最后紧跟在这之后的是文件结束标志内容,包括:1个字节的’#’,2个字节的0x000c(表示文件结束)2个字节的0x0901,4个字节的数据(内容为文件

长度+4),UMD文件结束。

3.       UMD解析代码

好了,废话不说,直入主题,上代码,完整工程见我的github工程:

https://github.com/rhythmkay/UMDParser

其它相关参考资料可见:http://www.iteye.com/topic/465443

UMD解析的详细代码可见如下:

UMDParser.m



//
//  UMDParser.m
//  reader.multidocs
//
//  Created by rhythmkay on 15-2-2.
//  Copyright (c) 2015年 rhythmzhang. All rights reserved.
//

#import "UMDParser.h"
#import "zlib.h"

@implementation UMDParser

- (instancetype)initWithFileURL:(NSURL *)t_url andDestinationFolder:(NSURL *)t_destURL{
    
    if(self=[super init])
    {
        _url=t_url;
        _destURL=t_destURL;
    }
    return  self;
}

-(void)dealloc{
    
    if(_utxHandle)
    {
        [_utxHandle closeFile];
    }
}


/*
 判断是否为UMD文件
 */
-(BOOL)isUMD{
    
    
    _fileLength=[Util fileLengthWithFile:self.url.path];
    _handle=[NSFileHandle fileHandleForReadingAtPath:self.url.path];
    
    //read first 4 bytes;
    NSData *data=[_handle readDataOfLength:DATA_BYTES_4];
    _offset=[_handle offsetInFile];
    UInteger4 mime;
    [data getBytes:&mime length:[data length]];
    if(UMD_TAG_MIME==mime)
        return YES;
    else
        return NO;
}

/*
 用来跳过当前bytes个字节.
 */
-(void)nextBytes:(unsigned int) bytes{
    unsigned long long t_offset=[_handle offsetInFile]+bytes;
    if(t_offset>_fileLength)
    {
        NSLog(@"error nextBytes,out of file length");
        return ;
    }
    else
        [_handle seekToFileOffset:t_offset];
}

/*
 用来回退bytes个字节.
 */
-(void)previousBytes:(unsigned int) bytes{
    if([_handle offsetInFile]<bytes)
        [_handle seekToFileOffset:4]; //跳回去....
    else
    [_handle seekToFileOffset:[_handle offsetInFile]-bytes]; //跳过这2个无意义字节.
}


/*
 将umd源文件的data转为合法的unicode字符.
 */
-(NSString*)dataToUnicodeString:(NSData*)data
{
    UInteger2 bom=0xfeff;
    NSMutableData *strData=[[NSMutableData alloc] initWithBytes:&bom length:2];
    if(data)
        [strData appendData:data];
    return [[NSString alloc] initWithData:strData encoding:NSUnicodeStringEncoding];
}


-(BOOL)parse{
    
    BOOL ok=NO;
    
    if(![self isUMD])
    {
        NSLog(@"illegal mime,it's not umd file.");
        
        if(_handle)
        {
            [_handle closeFile];
            _handle=nil;
        }
        
        return ok;
    }
    
    _offset=[_handle offsetInFile];
    _metaData=[UMDMetadata new];
    
    NSData *data;
    //开始第5个字节读取...
    [_handle seekToFileOffset:[_handle offsetInFile]+5]; //跳过这5个无意义字节.
    UInteger1 umd_type; //1字节
    data=[_handle readDataOfLength:DATA_BYTES_1];
    [data getBytes:&umd_type length:[data length]];
    switch(umd_type)
    {
        case UMD_TYPE_TEXT:
            NSLog(@"纯文本UMD");
            break;
        case UMD_TYPE_CARTOON:
            NSLog(@"动漫UMD,无法解析,程序退出.");
            return ok; //结束,无法解析动漫UMD
            break;
        default:
            NSLog(@"ERROR UMD,未知类型的UMD文件");
            return ok;
    }
    
    [self nextBytes:DATA_BYTES_2]; //跳过这2个无意义字节.

    //开始读取UMD文件的基本META属性,入作者,标题,等...
    BOOL isAttrEnd=NO;
    NSString *str;
    
    NSLog(@"读取属性");
    //这里应该一次性读5个字节才对
    while(!isAttrEnd)
    {
        //循环读取5字节.
        data=[_handle readDataOfLength:DATA_BYTES_5];
        
        UInteger1 tag_spilt;
        [data getBytes:&tag_spilt range:NSMakeRange(0, 1)]; //第一个字节,一定为'#'
        UInteger2 tag_attr;
        [data getBytes:&tag_attr range:NSMakeRange(1, 2)];//第2,3个字节,共2字节
        
        //空的1字节.不处理.
        
        if(UMD_TAG_SPILT==tag_spilt)
        {
            UInteger1 tag_length;
            [data getBytes:&tag_length range:NSMakeRange(4, 1)]; //第4个字节,最后一个字节.
            UInteger1 content_length=tag_length-5; //内容长度,必须减5.
            
            switch(tag_attr)
            {
                case UMD_TAG_TITLE:
                {
                    str=[self dataToUnicodeString:[_handle readDataOfLength:content_length]];
                    _metaData.title=str;
                    break;
                }
                case UMD_TAG_AUTHOR:
                {
                    str=[self dataToUnicodeString:[_handle readDataOfLength:content_length]];
                    _metaData.author=str;
                    break;
                }

                case UMD_TAG_PUB_YEAR:
                {
                    str=[self dataToUnicodeString:[_handle readDataOfLength:content_length]];
                    _metaData.pubDate=[NSString stringWithFormat:@"%@-",str];
                    break;
                }

                case UMD_TAG_PUB_MONTH:
                {
                    str=[self dataToUnicodeString:[_handle readDataOfLength:content_length]];
                    _metaData.pubDate=[_metaData.pubDate stringByAppendingFormat:@"%@-",str];
                    break;
                }
                    
                case UMD_TAG_PUB_DATE:
                {
                    str=[self dataToUnicodeString:[_handle readDataOfLength:content_length]];
                    _metaData.pubDate=[_metaData.pubDate stringByAppendingFormat:@"%@",str];
                    break;
                }

                case UMD_TAG_TYPE:
                {
                    str=[self dataToUnicodeString:[_handle readDataOfLength:content_length]];
                    _metaData.type=str;
                    break;
                }

                case UMD_TAG_PUBLISHER:
                {
                    str=[self dataToUnicodeString:[_handle readDataOfLength:content_length]];
                    _metaData.publisher=str;
                    break;
                }

                case UMD_TAG_RETAILER:
                {
                    //data=[_handle readDataOfLength:content_length];
                    str=[self dataToUnicodeString:[_handle readDataOfLength:content_length]];
                    //[self dataToUnicodeString:data];
                    _metaData.retailer=str;
                    break;
                }

                case UMD_TAG_CONTENT_LENGTH:
                {
                    data=[_handle readDataOfLength:DATA_BYTES_4];
                    UInteger4 tag_length;
                    [data getBytes:&tag_length length:DATA_BYTES_4];
                    _contentLength=tag_length;
                    //NSLog(@"_contentLength=%lli",_contentLength);
                    break;
                }
                default:
                {
                    [self previousBytes:DATA_BYTES_5]; //此处也会多度了,然后回退.
                    isAttrEnd=YES; //跳出当前while循环,属性读取完毕.
                    break;
                }

            }
        }
        else
        {
            [self previousBytes:DATA_BYTES_5]; //回退.
            NSLog(@"illegal tag spilt,break.");
            break;
        }
        
    }
    
    
    

    //开始读取章节目录
    data=nil; //时不时的清空自己.
    UInteger1 tag_spilt;
    //读取章节偏移量.
    //读18个字节.
    _chapters=[[NSMutableArray alloc] init];
    NSLog(@"读取章节偏移量信息");
    data=[_handle readDataOfLength:18];
    [data getBytes:&tag_spilt range:NSMakeRange(0, 1)];
    if(UMD_TAG_SPILT==tag_spilt)
    {
        UInteger2 tag_attr;
        [data getBytes:&tag_attr range:NSMakeRange(1, 2)];
        if(UMD_TAG_CHAPTER_OFFSET==tag_attr)
        {
            UInteger4 chapter_count;
            [data getBytes:&chapter_count range:NSMakeRange([data length]-DATA_BYTES_4, DATA_BYTES_4)]; //最后4字节.
            if(chapter_count>0)
            {
                chapter_count=(chapter_count-DATA_SPILT_LENGTH)>>2; //除以4,右移2位.
                UInteger4 chap_offset=0;
                
                int chap_counter=0;
                while(chap_counter<chapter_count)
                {
                    UMDChapter *chap=[UMDChapter new];
                    data=[_handle readDataOfLength:DATA_BYTES_4];
                    [data getBytes:&chap_offset length:[data length]];
                    chap.offset=chap_offset;
                    chap_counter++;
                    [_chapters addObject:chap];
                }
            }
        }
        
    }
    else
    {
        NSLog(@"error,章节偏移量解析出错.");
        return ok;
    }
    data=nil;
    
    NSLog(@"读取章节标题");
    data=[_handle readDataOfLength:18];
    [data getBytes:&tag_spilt range:NSMakeRange(0, 1)];
    if(UMD_TAG_SPILT==tag_spilt)
    {
        UInteger2 tag_attr;
        [data getBytes:&tag_attr range:NSMakeRange(1, 2)];
        if(UMD_TAG_CHAPTER_TITLE==tag_attr)
        {
            UInteger4 chapter_title_length;
            [data getBytes:&chapter_title_length range:NSMakeRange([data length]-DATA_BYTES_4, DATA_BYTES_4)]; //最后4字节.
            for(UMDChapter *chap in _chapters)
            {
                data=[_handle readDataOfLength:DATA_BYTES_1];
                UInteger1 title_length;
                [data getBytes:&title_length length:[data length]];
                data=[_handle readDataOfLength:title_length];
                chap.title=[self dataToUnicodeString:data];
            }
            
        }
        
    }
    else
    {
        NSLog(@"error,章节标题出错.");
        return ok;
    }
    data=nil;
    
    //接下来是正文数据了咯.......
    //为了避免过多内存占用,将把解析的正文数据块先统一的写入本地文件....
    //边解析边解压缩然后写入文件,这样就是一个完整的unicode编码的纯文本了呢....
    
    NSFileHandle *fileHandle=[self createDecompressFile];
    
    if(fileHandle)
    {
        BOOL isDataChunkEnd=NO;
        
        //采用BUFFER方式优化读写.
        NSMutableData *buffer=[[NSMutableData alloc] init];

        while(!isDataChunkEnd)
        {
            data=[_handle readDataOfLength:DATA_BYTES_1];
            UInteger1 tag_spilt;
            [data getBytes:&tag_spilt length:[data length]];

            if(UMD_TAG_CONTENT_START==tag_spilt)
            {
                [self nextBytes:DATA_BYTES_4];//跳过4个随机数字节
                UInteger4 chunk_compressed_length;
                data=[_handle readDataOfLength:DATA_BYTES_4];
                [data getBytes:&chunk_compressed_length length:[data length]];
                chunk_compressed_length-=DATA_SPILT_LENGTH; //减9才是实际压缩块数据的大小
                data=[_handle readDataOfLength:chunk_compressed_length];

                data=[self uncompress:data];
                
                [buffer appendData:data];
                data=nil;
                if([buffer length]>UMD_DECOMPRESSED_BUFFER_SIZE)
                {
                    [fileHandle writeData:buffer];
                    [buffer setLength:0];
                }
                
            }
            else if(UMD_TAG_SPILT==tag_spilt)
            {
                data=[_handle readDataOfLength:DATA_BYTES_2];
                UInteger2 tag_chunk_end;
                [data getBytes:&tag_chunk_end length:[data length]];
                switch(tag_chunk_end)
                {
                    case UMD_TAG_CHUNK_F:
                        [self nextBytes:18]; //跳过18字节
                        break;
                    case UMD_TAG_CHUNK_A:
                        [self nextBytes:6];//跳过6字节.
                        break;
                    case UMD_TAG_CONTENT_END:
                       // NSLog(@"正文结束");
                        isDataChunkEnd=YES;
                        break;
                }

            }
            else
            {
                NSLog(@"error解析正文");
                break;
            }
            
        }
        
        if([buffer length]>0)
        {
            [fileHandle writeData:buffer];
            [buffer setLength:0]; //clean it....
            buffer=nil; //release it.
        }

        [fileHandle closeFile];
        fileHandle=nil;
        //操作写入完毕,可以结束了......

    }
    else
    {
        NSLog(@"无法写入解析后的数据,fileHandle==nil,退出");
        return ok;
    }
    data=nil;
    
    //正文解析完毕,然后读取封面cover....
    NSLog(@"读取封面");
    //跳过这之间多余的字符
    BOOL isUMDEnd=NO;
    while([_handle offsetInFile]<_fileLength)
    {
        UInteger1 tag_spilt;
        data=[_handle readDataOfLength:DATA_BYTES_1];
        [data getBytes:&tag_spilt length:[data length]];
        if(UMD_TAG_SPILT==tag_spilt)
        {
            UInteger2 tag_attr;
            data=[_handle readDataOfLength:DATA_BYTES_2];
            [data getBytes:&tag_attr length:[data length]];
            switch(tag_attr)
            {
                case UMD_TAG_COVER:
                    //进行封面处理
                    //跳过12字节
                    [self nextBytes:UMD_NEXT_12];
                    UInteger4 tag_cover_length;
                    data=[_handle readDataOfLength:DATA_BYTES_4];
                    [data getBytes:&tag_cover_length length:[data length]];
                    if(tag_cover_length>DATA_SPILT_LENGTH)
                    {
                        tag_cover_length-=DATA_SPILT_LENGTH; //减9
                        //接下来得tag_cover_length就是实际的cover了
                        data=[_handle readDataOfLength:tag_cover_length];
                        _metaData.cover=[self createCoverFile:data];
                    }
                    break;
                case UMD_TAG_FINISHED:
                    NSLog(@"finished...");
                    isUMDEnd=YES;
                    break;
            }
        }
        if(isUMDEnd)
            break; //结束.
        
    }
    data=nil;
    
    
    [_handle closeFile]; //操作结束,关闭文件.
    _handle=nil;
    
    ok=YES; //这里才成功...
    
    return ok;

}


-(NSFileHandle*)createDecompressFile{
    
    NSString *fileName=[[self.url lastPathComponent] stringByDeletingPathExtension];
    _baseURL=[_destURL URLByAppendingPathComponent:fileName];
    [Util createDirAtPath:_baseURL.path];
    _utxURL=[_baseURL URLByAppendingPathComponent:[fileName stringByAppendingPathExtension:UMD_DECOMPRESSED_EXTENSION] isDirectory:NO];
    [[NSFileManager defaultManager] createFileAtPath:_utxURL.path contents:nil attributes:nil];
    NSFileHandle *fileHandle=[NSFileHandle fileHandleForWritingAtPath:_utxURL.path];

    UInteger2 bom=0xfeff;
    [fileHandle writeData:[NSData dataWithBytes:&bom length:DATA_BYTES_2]];
    //先写入unicode编码的bom头部呢..

    return fileHandle;
}


-(NSString*)createCoverFile:(NSData*)data{
    
    if(!data)
        return nil;
    NSString *fileName=[[self.url lastPathComponent] stringByDeletingPathExtension];
    //_baseURl已经在前面的createDecompressFile方法中初始化了.
    NSURL *pathURL=[_baseURL URLByAppendingPathComponent:[fileName stringByAppendingPathExtension:UMD_COVER_EXTENSION] isDirectory:NO];
    [[NSFileManager defaultManager] createFileAtPath:pathURL.path contents:data attributes:nil];
    if([Util fileLengthWithFile:pathURL.path]>1024) //大于1个字节,才说明有图片信息么???
    {
        return pathURL.path;
    }
    return nil;
}


/*****************解压缩,使用zlib即可...本函数代码源于互联网*****************/
- (NSData *)uncompress:(NSData *)zlibData
{
    //auto release pool优化内存占用.
    @autoreleasepool {
    
        if ([zlibData length] == 0) return zlibData;
        
        unsigned long full_length = [zlibData length];
        unsigned long half_length = [zlibData length] / 2;
        
        NSMutableData *decompressed = [NSMutableData dataWithLength: full_length + half_length];
        BOOL done = NO;
        int status;
        
        z_stream strm;
        strm.next_in = (Bytef *)[zlibData bytes];
        strm.avail_in = (unsigned int)[zlibData length];
        strm.total_out = 0;
        strm.zalloc = Z_NULL;
        strm.zfree = Z_NULL;
        
        if (inflateInit (&strm) != Z_OK) return nil;
        
        while (!done)
        {
            // Make sure we have enough room and reset the lengths.
            if (strm.total_out >= [decompressed length])
                [decompressed increaseLengthBy: half_length];
            strm.next_out = [decompressed mutableBytes] + strm.total_out;
            strm.avail_out =(unsigned int) ([decompressed length] - strm.total_out);
            
            // Inflate another chunk.
            status = inflate (&strm, Z_SYNC_FLUSH);
            if (status == Z_STREAM_END) done = YES;
            else if (status != Z_OK) break;
        }
        if (inflateEnd (&strm) != Z_OK) return nil;
        
        // Set real length.
        if (done)
        {
            [decompressed setLength: strm.total_out];
            return [NSData dataWithData: decompressed];
        }
        else return nil;
    }
    
}


 //index从0开始,代表第index+1章节的内容....
//_chapter存储的offset时解压后的文件的偏移
-(NSString*)contentForChapter:(int)index{
    
    if(index<0||index>=[self.chapters count])
    {
        NSLog(@"非法章节序号.从0开始,小于chapters数目才可");
        return nil;
    }
    NSString *content;
    if(!_utxHandle&&_utxURL.path)
    {
        _utxHandle=[NSFileHandle fileHandleForReadingAtPath:_utxURL.path];
        _utxContentLength=[Util fileLengthWithFile:_utxURL.path];
    }
    
    UMDChapter *currChap=[self.chapters objectAtIndex:index];
    unsigned long long offset=currChap.offset;
    NSLog(@"chapter=%@",currChap.title);
    unsigned long long length=0;
    if(index+1>=[self.chapters count])
    {
        length=_utxContentLength-offset;
    }
    else
    {
        UMDChapter *nextChap=[self.chapters objectAtIndex:index+1];
        length=nextChap.offset-currChap.offset;
    }
    
    if(offset>_utxContentLength||length>_utxContentLength)
    {
        NSLog(@"非法offset与length,超过文件预期大小");
        return nil;
    }
    
    if(_utxHandle)
    {
        [_utxHandle seekToFileOffset:offset];
        NSData *data=[_utxHandle readDataOfLength:length];
        content=[self dataToUnicodeString:data];
    }
    
    return content;
}


@end


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值