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.2UMD类型标识

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;elsereturn 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; //结束,无法解析动漫UMDbreak;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

UMD格式与解析详解相关推荐

  1. java bip-39_Java中对XML的解析详解

    先简单说下前三种方式: DOM方式:个人理解类似.net的XmlDocument,解析的时候效率不高,占用内存,不适合大XML的解析: SAX方式:基于事件的解析,当解析到xml的某个部分的时候,会触 ...

  2. android Json解析详解(详细代码)

    android Json解析详解(详细代码)   JSON的定义: 一种轻量级的数据交换格式,具有良好的可读和便于快速编写的特性.业内主流技术为其提供了完整的解决方案(有点类似于正则表达式 ,获得了当 ...

  3. JSON数据构造及解析详解

    JSON数据构造及解析详解 1.JSON格式数据长啥样? 2.JSON简介 JSON(Javascript Object Notation)是一种轻量级的数据交换格式,易于阅读和编写,也易于机器解析和 ...

  4. 【H264/AVC 句法和语义详解】(二):h264码流格式与NALU详解一

    上一篇中,我们站在句法元素(或称语法元素)的角度,介绍了H.264的句法和语义,和句法元素的分层结构.在这篇中,我们更进一步,从比特的角度出发,来探索h264码流的组成.通过这篇的学习,我们会初步具备 ...

  5. php 显示要上传的图片格式,php判断文件上传图片格式的实例详解

    php判断文件上传图片格式的实例详解 判断文件图片类型, $type = $_FILES['image']['tmp_name'];//文件名 //$type = $this->getImage ...

  6. Nginx内置变量以及日志格式变量参数详解

    Nginx内置变量以及日志格式变量参数详解 $args #请求中的参数值 $query_string #同 $args $arg_NAME #GET请求中NAME的值 $is_args #如果请求中有 ...

  7. 用windows系统下的DOS命令将腾讯视频客户端下载的qlv文件转换成MP4格式(图文详解)

    用windows系统下的DOS命令将腾讯视频客户端下载的qlv文件转换成MP4格式(图文详解) 前言 原理 工具 步骤 延伸 博主联系方式 前言 本人喜欢收集各种优秀的视频,但是很多情况下我们看到的视 ...

  8. 【C】C语言格式输入函数scanf()详解

    参考了:C语言格式输入函数scanf()详解 总述 scanf函数称为格式输入函数,即按用户指定的格式从键盘上把数据输入到指定的变量之中. scanf函数的一般形式 scanf函数是一个标准库函数,它 ...

  9. MP3格式技术发展详解

    MP3格式技术发展详解 (2008-06-17 17:30) 分类: 多媒体 MPEG-1 Audio Layer 3,经常称为MP3,是当今较流行的一种数字音频编码和有损压缩格式,它设计用来大幅度地 ...

  10. 不同格式的图像详解_不同类型的图像格式

    不同格式的图像详解 Image Format basically describes how data related to the image will be stored or we can sa ...

最新文章

  1. java 位运算 多个状态_位运算表示对象所处状态
  2. 如何搜索国外上市企业的财务数据以及年报
  3. (0036) iOS 开发之HTTPS、SSL验证
  4. benet 3.0的构建企业网络视频第二章地址
  5. javaScript——廖雪峰老师学习笔记(一)
  6. 为什么python用不了中文_【TK例子】为什么不显示中文
  7. *args and **kwargs in Python 变长参数
  8. 『骑士精神 IDA*』
  9. 这里有一份面筋请查收(五)
  10. [设计模式] Javascript 之 观察者模式
  11. Stringbuffer的线程安全是怎么实现的
  12. 如何解决忙死领导,闲死下属的问题?
  13. 移植最新版libmemcached到VC++的艰苦历程和经验总结(上)
  14. 【转】用instruments来检验你的app
  15. 虚拟仪器是在计算机基础上通过增加相关硬件和软件构建而成的仪器,无损检测考试...
  16. HCIP-Cloud Service Solutions Architect
  17. CHROME扩展开发文档之·chrome.runtime
  18. 无人汽车无法避开陌生物体?这里有最新解决方案
  19. 计算机接口接触不良,如何处理电脑耳机插口接触不良
  20. 从奶茶品牌【茶颜悦色】看互联网的品牌保护

热门文章

  1. 使用docker搭建steam 饥荒服务器
  2. 洛谷1498-谢尔宾斯基三角形-python-(递归)
  3. 美团面试官:Java 性能调优你会多少?一个问题就把我问的哑口无言,哭了
  4. 利用matlab函数创建数组
  5. 双本振双输出后接八切一影响其它端口信号
  6. RTL8153 VC CG
  7. 甄零一诺合同——专注合同信息化管理
  8. [整理]VS2010中文版配置opencv2.4.8
  9. SQL Server数据库学习(1)
  10. 明日之后登不上去一直连接服务器,《明日之后》登不上去怎么办 明日之后进不去怎么回事...