解析DWG
解析DWG
本文来自 https://blog.csdn.net/jiangyb999
0x01_文件结构
总体结构
DWG文件可分为2部分:文件头和数据.
数据部分在物理上分页(Page)存储的,
在逻辑上由一个或多个页组成一个段(Section).
0x01a_文件头
文件头长度为0x100个字节,布局如下:
地址 | 长度(byte) | 描述 |
---|---|---|
0x00 | 7 | "AC1018\0" |
0x07 | 4 | 0x00 |
0x0B | 1 | 主版本号 |
0x0C | 1 | 0x00或0x01或0x03 |
0x0D | 4 | 预览图片的地址 |
0x11 | 1 | Dwg version |
0x12 | 1 | Dwg maintance version |
0x13 | 2 | Codepage index |
0x15 | 1 | 0x00 |
0x16 | 1 | App version |
0x17 | 1 | App maintance version |
0x18 | 4 | 安全标志 0x0001:加密数据(除了AcDb:Preview and AcDb:SummaryInfo) 0x0002:加密属性(用于AcDb:Preview and AcDb:SummaryInfo) 0x0010:sign data 0x0020:add timestamp |
0x1C | 4 | 未知 |
0x20 | 4 | SummaryInfo地址,points to summary info page + page header size(0x20) |
0x24 | 4 | VBA工程地址 |
0x28 | 4 | 0x00000080(似乎是2004header的地址) |
0x2C | 0x54 | 全是0x00 |
0x80 | 0x6C | 加密的数据,称为2004header |
0xEC | 0x14 | 填充数据(据说是魔术字节序列的前0x14字节,未做考证) |
开始的7个字节是DWG版本标志串,以AC后跟4个数字表述版本;
从AutoCAD2000开始,dwg版本号每3年做一次升级,
所以AC标志串中的数字并不是连续的;
对应关系为:
标注串 | AutoCAD的发行版本 |
---|---|
AC1012 | R13 |
AC1014 | R14 |
AC1015 | 2000 - 2003 |
AC1018 | 2004 - 2006 |
AC1021 | 2007 - 2009 |
AC1024 | 2010 - 2012 |
AC1027 | 2013 - 2017 |
AC1032 | 2018 - 2024 |
Autodesk官网展示DWG版本号
cad.net 用定点数表示DWG版本号
0x80处的0x6C个字节的加密数据,
与下列0x6C个魔术字节逐一做XOR运算,即可解密;
29 23 BE 84 E1 6C D6 AE 52 90 49 F1 F1 BB E9 EB
B3 A6 DB 3C 87 0C 3E 99 24 5E 0D 1C 06 B7 47 DE
B3 12 4D C8 43 BB 8B A6 1F 03 5A 7D 09 38 25 1F
5D D4 CB FC 96 F5 45 3B 13 0D 89 0A 1C DB AE 32
20 9A 50 EE 40 78 36 FD 12 49 32 F6 9E 7D 49 DC
AD 4F 14 F2 44 40 66 D0 6B C4 30 B7 32 3B A1 22
F6 22 91 9D E1 8B 1F DA B0 CA 99 02
// 它是用下面的代码得到的:
unsigned char pc[0x6c];
int randseed = 1;
for (int i=0; i<0x6c; i++) {
randseed *= 0x343fd;
randseed += 0x269ec3;
pc[i] = (unsigned char)(randseed>>0x10);
}
经过以上运算解密后的数据布局,如下表:
ACAD2004HeaderData
在这些数据当中,对解析最重要的当属标红的三个数据
了.
这段数据在文件尾部还有一份,即SecondHeaderData;
字地址 | 长度(byte) | 描述 |
---|---|---|
0x00 | 12 | "AcFssFcAJMB\0" 标志串 |
0x0C | 4 | 0x00(long) header address |
0x10 | 4 | 0x6c(long) header size |
0x14 | 4 | 0x04(long) |
0x18 | 4 | Root tree node gap |
0x1C | 4 | Lowermost left tree node gap |
0x20 | 4 | Lowermost right tree node gap |
0x24 | 4 | 未知(long) |
0x28 | 4 | Last section id |
0x2C | 4 | Last section address |
0x30 | 4 | 0x00 |
0x34 | 4 | Second header address |
0x38 | 4 | 0x00 |
0x3C | 4 | Gap总数 |
0x40 | 4 | Section总数 |
0x44 | 4 | 0x20(long) |
0x48 | 4 | 0x80(long) |
0x4C | 4 | 0x40(long) |
0x50 | 4 | Page Map Id |
0x54 | 4 | Page Map Address 该值是以数据部分为起点的偏移,在整个文件中的偏移需要加上文件头的大小(0x100) |
0x58 | 4 | 0x00 |
0x5C | 4 | Section map Id |
0x60 | 4 | Section page array size |
0x64 | 4 | Gap array size |
0x68 | 4 | CRC32(long) |
文件头代码
解析2004的文件头比较简单,只需要很少的代码;
先定义如下的文件头结构:
#pragma pack(push, 1)
typedef struct _tagDwg2004Header
{ // 务必保持成员的顺序
char version[7]; // 0x00: "AC1018\0"
char x07_unknown_0[4]; // 0x07: 4zeros
char is_maint; // 0x0B
char x0c_0_1_3; // 0x0C: 0 or 1 or 3
uint32_t thumbnail_addr; // 0x0D
char dwg_version; // 0x11
char dwg_maint_version; // 0x12
short codepage; // 0x13
char x15_unknown_0; // 0x15
char app_version; // 0x16
char app_maint_version; // 0x17
uint32_t security_type; // 0x18
uint32_t x1c_unknown_0; // 0x1C
uint32_t summary_info_address; // 0x20
uint32_t vba_proj_address; // 0x24
uint32_t r2004_header_address; // 0x28: 0x80
char x2c_0[0x54]; // 0x2C: 0
union
{
char encrypted_data[0x6c]; // 0x80
struct _r2004
{
char file_id_string[12]; // "AcFssFcAJMB\0"
uint32_t header_address; // 0x00
uint32_t header_size; // 0x6c
uint32_t x14_unknown_0; // 0
uint32_t root_tree_node_gap;
uint32_t lowermost_left_tree_node_gap;
uint32_t lowermost_right_tree_node_gap;
uint32_t x24_uknown;
uint32_t last_section_id;
uint64_t last_page_address;
uint64_t second_header_address;
uint32_t gap_amount;
uint32_t page_amount;
uint32_t x44_x20;
uint32_t x48_x80;
uint32_t x4c_x40;
uint32_t page_map_id;
uint64_t page_map_address; // offset from DATA, not include this HEADER size
uint32_t section_map_id;
uint32_t section_array_size;
uint32_t gap_array_size;
uint32_t crc32;
} r2004;
};
char padding[0x14];
} Dwg2004Header; // total 0x100 bytes
#pragma pack(pop)
解析的代码
// 说明:m_header的类型是上面定义的Dwg2004Header,
// DWG_DATA是dwg文件的原始数据
memcpy(&m_header, DWG_DATA, sizeof(Dwg2004Header));
// decrypt r2004 header
int rseed = 1;
for (int i = 0; i < 0x6c; i++) {
rseed *= 0x343fd;
rseed += 0x269ec3;
m_header.encrypted_data[i] ^= (rseed >> 0x10);
}
0x01b_数据段
DWG数据是分段(Section)组织的,文件中涉及到的数据段有:
/// \enum Section type of R2004+
typedef enum _tagDwg2004SectionType {
SECTION_UNKNOWN = 0, ///< The very first 160 byte
SECTION_HEADER = 1, ///< AcDb:Header
SECTION_AUXHEADER = 2, ///< AcDb:AuxHeader
SECTION_CLASSES = 3, ///< AcDb:Classes
SECTION_HANDLES = 4, ///< AcDb:Handles
SECTION_TEMPLATE = 5, ///< AcDb:Template
SECTION_OBJFREESPACE = 6, ///< AcDb:ObjFreeSpace
SECTION_OBJECTS = 7, ///< AcDb:AcDbObjects
SECTION_REVHISTORY = 8, ///< AcDb:RevHistory
SECTION_SUMMARYINFO = 9, ///< AcDb:SummaryInfo
SECTION_PREVIEW = 10, ///< AcDb:Preview
SECTION_APPINFO = 11, ///< AcDb:AppInfo
SECTION_APPINFOHISTORY = 12, ///< AcDb:AppInfoHistory
SECTION_FILEDEPLIST = 13, ///< AcDb:FileDepList
SECTION_SECURITY, ///< AcDb:Security, if stored with a password
SECTION_VBAPROJECT, ///< AcDb:VBAProject
SECTION_SIGNATURE, ///< AcDb:Signature
SECTION_ACDS, ///< AcDb:AcDsPrototype_1b = 12 (ACIS datastorage)
SECTION_SECTIONMAP, ///< system section: section map
SECTION_PAGEMAP, ///< system section: page map
} Dwg2004SectionType;
所有的Section(段)中,
Page Map段和Section Map段,这2个Section至关重要,
必须先根据ACAD2004HeaderData(文件头)中标红的三个数据
从文件中读取,
再根据读到的结果,一一读取不同的数据段予以解析.
首先从Page Map Address处读取Pagen Map,
然后从此map中,
取得Section Map Id对应的段(即Section Map Section的地址),
并从文件中读出它的数据.
这两种Section统称为System Section(系统段)
,
其他的Section则称为Data Section(数据段)
.
之后,由这两兄弟配合,
就可以通过Section Name确定各种Section的数据页在文件中的位置,
根据描述信息予以读取.
System Section都只有一页,而Data Section至少有一页.
0x01c_系统段
System Section的数据是按页(Page)组织的.
一个Page包含2部分,页头和页数据:
项目 | 长度(bytes) |
---|---|
header | 0x14 |
data | 压缩数据,长度由header.CompDataSize指定 |
其中header对于两种System Section来说,结构是一样的,差别在于data的结构的不同.
0x01c1_系统段页头
SystemSectionPageHeader
地址(偏移) | 长度(byte) | 含义 |
---|---|---|
0x00 | 4 | Page类型有2种system page. Page Map Page:0x41630e3b; Section Map Page:0x4163003b |
0x04 | 4 | 解压后的数据长度(DecompDataSize) |
0x08 | 4 | 压缩后的数据长度(CompDataSize) |
0x0C | 4 | 压缩类型(0x02) |
0x10 | 4 | CRC |
当 page.CompDataSize==0时,这个System Section就结束了,
后面也不会再跟随Page data.
如下图所示就是System Section的组织形式:
0x01c2_页映射页数据对
PageMapPageData
Page Map段的页数据经解压缩后,为多个(Page number, Page size)数据对,
其中的Page number从1开始计数,
该数据对记录的是文件中的各数据页的编号和大小.
地址 | 长度 | 描述 |
---|---|---|
0x00 | 4 | page number, 1-based |
0x04 | 4 | page size |
这种数据对一直重复,直到本页的数据耗尽.
在读取时,可以顺便计算出各Section在文件中的绝对偏移地址:
第一个读到的Section的地址,
总是从文件头后(即0x100处)开始,
后续读到的Section的偏移,
可从其前一个Section的偏移和大小计算得到.
0x01c3_段映射页数据对
SectionMapPageData
从上面的Page Map只能得到已知某个编号的Page的位置和大小,
无法得到该Page的具体信息,包括压缩否,加密否等等,
这时就该Section Map上场了;
Section Map的Page data解压缩后的布局如下,
这是一个复合结构:
偏移量 (Hex) | 长度 (Bytes) | 含义 |
---|---|---|
0x00 | 4 | 描述num_descs 组重复数量 |
0x04 | 4 | Compressed? (1=no,2=yes,normally 2) |
0x08 | 4 | Max_size (normally 0x7400) |
0x0C | 4 | Encrypted? (0=no,1=yes,2=unknown) |
0x10 | 4 | Num descriptions 2 |
接着上面,之后重复num_descs
组
偏移量 (Hex) | 长度 (Bytes) | 含义 |
---|---|---|
0x00 | 8 | Size of section |
0x08 | 4 | 描述num_pages 组重复数量 |
0x0C | 4 | Max decompress size |
0x10 | 4 | 未知(会不会是long型的Max decompress size的高位?) |
0x14 | 4 | Compressed? (1=no,2=yes,normally 2) |
0x18 | 4 | Section type |
0x1C | 4 | Encrypted? (0=no, 1=yes, 2=unknown) |
0x20 | 64 | Section name ("AcDbObjects"等等) |
接着上面,嵌套在num_descs
组内,重复num_pages
组
偏移量 (Hex) | 长度 (Bytes) | 含义 |
---|---|---|
0x00 | 4 | page number |
0x04 | 4 | page data size |
0x08 | 8 | Start offset |
对System Section的归纳图示:
下面是一个解析实例中,关于AcDbObjects的Section Map的描述;
Description No.7
------------------
size: 0x2b0e9
num_pages: 0x6
max_decomp_size: 0x7400
unknown2: 0x1
compressed: 0x2
type: 0x7
encrypted: 0x0
name: AcDb:AcDbObjects
No.1
.............
page number: 6
data size: 0x30ea
offset: 0x0
unknown: 0x0
No.2
.............
page number: 7
data size: 0x7f0
offset: 0x7400
No.3
.............
number: 8
data size: 0x628
offset: 0xe800
No.4
.............
page number: 9
data size: 0xf1f
offset: 0x15c00
No.5
.............
page number: 10
data size: 0xcc0
offset: 0x1d000
No.6
.............
page number: 11
data size: 0xe0c
offset: 0x24400
0x01d_数据段页
DataSectionPage
Data Section Page的布局为:
项目 | 长度 | 含义 |
---|---|---|
Header | 0x20 | 加密头 |
Data | DataSize | 数据 |
Padding | page size - DataSzie - 0x20 | 填充 |
对加密头解密后的数据结构为:
偏移 | 长度 | 含义 |
---|---|---|
0x00 | 4 | Tag of data section (0x4163043B) |
0x04 | 4 | Section type |
0x08 | 4 | DataSize, Data的长度 |
0x0C | 4 | page size 本page的总长度(包括header,data和padding) |
0x10 | 8 | offset,Data解压后在解压缓冲中的存放偏移 |
0x18 | 4 | Check sum 1 |
0x1C | 4 | Check sum 2 |
加密头的解密方法:
假设0x20长度的头数据存放在header[8]中,
Offset是本Page在DWG文件中的绝对偏移,
可从Page map中得到:
int32_t header[8];
int32_t mask = 0x4164536b ^ offset;
for (int i=0; i<8; ++i) {
header[i] ^= mask;
}
从中可以看出Autodesk也挺鸡贼的,还整了一个动态加密;
Data Section Page的图示:
当要读取某种类型的数据时,
先从Setion Map中查找此类型的Description,
依据Description中的Page列表,
然后透过Page map定位到各Page,依次读取Page数据,
将之解压且存放到目的缓冲的StartOffset处,
最后拼接出此类型数据的数据流供后续解析;
大致过程如下:
附一个实例示意图:
关于dwg文件的结构性的解析,至此就算完成了;
0x02_零七格式
前文讲述了DWG2004的文件结构,
在引言中曾提及2004后的DWG文件结构,除了2007外基本相同,
本文就详细讲述一下DWG2007的文件结构.
其实从2000版后,除了文件结构的不同外(可分为2000,2004,2007三种不同结构),
从文件中解析出某个Section的数据后,其内部编码都是相同的(即位码).
总体结构
总体结构依然是文件头+数据页的组织形式,主要差别在于2007的文件头的不同.
0x02a_文件头
对文件头的解码过程如下图,
其中的r2007header类似于2004中的r2004header,
但是r2004header只需要在位解密就行.
首先从dwg文件头部读取0x80字节的数据,
这部分数据称为MetaData,描述的内容与2004相似.
其布局如下:
偏移 | 大小 | 说明 |
---|---|---|
0x00 | 7 | 标志串"AC1021\0" |
0x07 | 4 | 4个0 |
0x0B | 1 | Maintenance release version |
0x0C | 1 | 0 or 1 or 3 |
0x0D | 4 | preview address |
0x11 | 1 | dwg version |
0x12 | 1 | dwg maintance version |
0x13 | 2 | codepage index |
0x15 | 1 | unknown |
0x16 | 1 | app version |
0x17 | 1 | app maintance version |
0x18 | 4 | security type |
0x1C | 4 | unknown |
0x20 | 4 | summary info address |
0x24 | 4 | vba project address |
0x28 | 4 | r2007 header address (0x80) |
0x2C | 4 | application info address |
0x30 | 0x50 | unknown |
偏移量为 r2007_header_address (0x80) 处有 0x400 字节的数据,
其中前 0x3D8 个字节是经 Reed-Solomon (255, 239)
因子为3编码的压缩的 r2007header,
尾部的 0x28 个字节是5个64位整数表示的校验数据等;
其布局如下:
偏移 | 大小 | 说明 |
---|---|---|
0x00 | 8 | CRC |
0x08 | 8 | key |
0x10 | 8 | DATA_CRC |
0x18 | 8 | DATA_Length |
0x20 | 绝对值(Data_Length) | DATA |
DATA未压缩:DATA_Length为负数;
DATA已压缩:如果DATA_Length为正数;
如果需要解压,则解压后的数据长度固定为0x110字节.
r2007header的布局如下:
偏移 | 大小 | 说明 |
---|---|---|
0x00 | 8 | header size (通常位0x70) |
0x08 | 8 | file size |
0x10 | 8 | PageMapCrcCompressed |
0x18 | 8 | PageMapCorrectionFactor |
0x20 | 8 | PageMapCrcSeed |
0x28 | 8 | PageMap2offset(相对于数据页的开始处,在整个文件中偏移应加上文件头的长度0x480) |
0x30 | 8 | PageMap2 Id |
0x38 | 8 | PageMapOffset (计算方法同PageMap2Offset) |
0x40 | 8 | PageMapId |
0x48 | 8 | Header2Offset(计算方法同PageMapOffset) |
0x50 | 8 | PageMapSizeCompressed |
0x58 | 8 | PageMapSizeUncompressed |
0x60 | 8 | PageAmount |
0x68 | 8 | PageMaxId |
0x70 | 8 | Unknown(0x20) |
0x78 | 8 | Unknown(0x40) |
0x80 | 8 | PageMapCrcUncompressed |
0x88 | 8 | Unknown(0xF800) |
0x90 | 8 | Unknown(0x4) |
0x98 | 8 | Unknown(0x1) |
0xA0 | 8 | SectionAmount(=section总数+1) |
0xA8 | 8 | SectionMapCrcUncompressed |
0xB0 | 8 | SectionMapSizeCompressed |
0xB8 | 8 | SectionMap2Id |
0xC0 | 8 | SectionMapId |
0xC8 | 8 | SectionMapSizeUncompressed |
0xD0 | 8 | SectionMapCrcCompressed |
0xD8 | 8 | SectionMapCorrectionFactor |
0xE0 | 8 | SectionMapCrcSeed |
0xE8 | 8 | StreamVersion(通常为0x60100) |
0xF0 | 8 | CrcSeed |
0xF8 | 8 | CrcSeedEncoded |
0x100 | 8 | RandomSeed |
0x108 | 8 | Header CRC64 |
从以上表格可以看出其与 2004header 的不同,
增加了很多数据项,而且全部向64位转变.
0x02b_系统段
SystemSection
接下来解析 System Section,
首先是Pagemap,
我们根据 r2007header 中参数的指引,从数据流中读取.
以下是解析实例中的参数:
pages_map_offset: 0
pages_map_correction: 7
pages_map_size_comp: 84
pages_map_size_uncomp: 170
sections_map_id: 16
sections_map_correction: 4
sections_map_size_comp: 3c9
sections_map_size_uncomp: 8d2
System Section只有一个数据页,
对Page map和Section map的读取方式(解码解压)是一样的,
直接上代码来解释吧;
方法read_system_page的参数:
参数 | 读page map的page时 | 读section map的page时 |
---|---|---|
data | 由pages_map_offset计算 | 从pagemap中查询sections_map_id对应的page的偏移计算得到 |
size_comp | pages_map_size_comp | sections_map_size_comp |
size_uncomp | pages_map_size_uncomp | sections_map_size_uncomp |
repeat_count | pages_map_correction | sections_map_correction |
uint8_t* CSystemSection2007::read_system_page(
const uint8_t* data,
int64_t size_comp,
int64_t size_uncomp,
int64_t repeat_count) {
// pesize: Pre rs Encoded size
// Round to a multiple of 8
uint64_t pesize = ((size_comp + 7) & ~7) * repeat_count;
// Divide pre encoded size by RS k-value (239)
uint64_t block_count = (pesize + 238) / 239;
if (block_count == 0)
{
PRINT("Empty r2007 system page block_count. size_comp: %lld, repeat_count: %lld",
size_comp, repeat_count);
return NULL;
}
// Multiply with codeword size (255) and round to a multiple of 8
uint64_t page_size = (block_count * 255 + 7) & ~7;
uint64_t pedata_size = block_count * 239;
uint8_t* pedata = new uint8_t[pedata_size];
reed_solomon_decode(pedata, data, block_count, 239, page_size);
// allocate memory for decompressing
check_buffer(size_uncomp);
if (size_comp < size_uncomp)
{
CCompressedData2007 comp(pedata, size_comp, size_uncomp);
comp.decompress(m_decomp_data);
}
else
memcpy(m_decomp_data, pedata, size_uncomp);
delete[] pedata;
return m_decomp_data;
}
从代码可知,
DWG2007的数据页是需要先RS解码,再解压缩的;
与DWG2004相同的是,
这里的Page map仍然是(Page number, Page size)数据对,
不同的是,
不再分页了,读取顺序是(Page size, Page number);
page map的数据布局如下:
偏移 | 大小 | 说明 |
---|---|---|
0x00 | 8 | page size |
0x08 | 8 | page number |
0x10 | 8 | page size |
0x18 | 8 | page number |
...... | ...... | ...... |
对Section map的解析要略微复杂一些,其数据布局为:
偏移 | 大小 | 说明 |
---|---|---|
0x00 | 8 | data size |
0x08 | 8 | max size |
0x10 | 8 | encrption |
0x18 | 8 | hashcode |
0x20 | 8 | section name length,包含尾0的2个字节 |
0x28 | 8 | unknown |
0x30 | 8 | encoding |
0x38 | 8 | number of pages |
0x40 | section name length | UNICODE section name end with "\0\0" |
number of pages 组 |
8 | page data offset |
number of pages 组 |
8 | page size |
number of pages 组 |
8 | page id |
number of pages 组 |
8 | page uncompressed size |
number of pages 组 |
8 | page compressed size |
number of pages 组 |
8 | page checksum |
number of pages 组 |
8 | page CRC |
解析实例:
0x02c_数据段
DataSection
从 Section map 可知,一个 Data section 可能会有多个页,
因此同2004一样,按照 Section map 的指引把这些数据页读入,
类似于System Section Page的读取,
依然要解码再解压(也可能无压缩),上代码看吧;
uint8_t* CDataSection2007::read_data_page(
const uint8_t* data,
uint8_t* dest,
int64_t page_size,
int64_t size_comp,
int64_t size_uncomp) {
int64_t pesize = ((size_comp + 7) & ~7);
int64_t block_count = (pesize + 250) / 251;
int64_t pedata_size = block_count * 251; //0xFB
uint8_t* pedata = new uint8_t[pedata_size];
reed_solomon_decode(pedata, data, block_count, 251, page_size);
if (size_comp < size_uncomp)
{
CCompressedData2007 comp(pedata, size_comp, size_uncomp);
comp.decompress(dest);
}
else
memcpy(dest, pedata, size_uncomp);
delete[] pedata;
return dest;
}
2007的结构性解析到这里就算结束了.
除了RS编码和解压算法不同外,与2004的结构基本类似;
不知道什么原因,此格式只存活了这一届,
之后Autodesk就回去找旧爱2004继续缠绵;
0x03_位码
BITCODE
在学习了如何解析DWG的结构后,接下来就是解读各Section(段)的内容;
不过,在这之前仍然有一件事是必需要了解的,
那就是DWG中的数据流的编码格式;
Autodesk大概是为了减小保存文件的尺寸,将数据流按位进行了编码
(个别Section仍然是以字节为单位编码,如Preview),
所以,本节只讲位码的编排格式;
编码名称 | 含义 |
---|---|
B | 1位 |
BB | 2位 |
3B | 1-3位 |
BS | Bit Short |
BL | Bit Long |
BLL | Bit Long Long |
BD | Bit Double |
DD | Bit Double with Default |
RC | Raw Char |
RS | Raw Short |
RD | Raw Double |
RL | Raw Long |
MC | Modular Char |
MS | Modular Short |
H | Handle reference |
TV | Ansi Text |
TU | Unicode text |
T | TV for 2004-, TU for 2007+ |
以下编码 | 只是为了便于简短表述或者方便读入代码的书写 |
2BD | 2D point (2 bitdoubles) |
3BD | 3D point (3 bitdoubles) |
2RD | 2 raw doubles |
3RD | 3 raw doubles |
BE | Bit Extrusion |
BT | Bit Thickness |
CMC | Object color |
ENC | Entity color |
OT | Object Type |
1, B
只有1位,表示0或1,通常用于开关属性;
2, 2B
连续2位,表示0-3
3,3B
连续1到3位,最少1位,最多3位;
先从流中读出一位,如果是0,停止,否则继续读下一位,直到遇到0或读满3位;
它得表示范围是0到7;
4,BS
前2位 | 结果 |
---|---|
00 | 后跟一个short值(2字节) |
01 | 后跟一个unsigned char(1字节) |
10 | 表示0,后面无数据 |
11 | 表示256,后面无数据 |
举例:
假设有这样的位流:
00 00000001 00000001 10 11 01 00001111 10
要读取5个short值,则按如下顺序解读
00,00000001,00000001,10,11,01,00001111,10
即
00 00000001 00000001 => 257(十进制)
10 => 0
11 => 256
01 00001111 => 15
10 => 0
5,BL
前2位 | 结果 |
---|---|
00 | 后跟一个long值(4字节) |
01 | 后跟一个unsigned char(1字节) |
10 | 表示0,后面无数据 |
11 | 表示256,后面无数据 |
举例:
假设有这样的位流:
00 00000001 00000001 00000000 00000000 10 01 00001111 10
全部是BitLong编码,则按如下顺序解读:
00,00000001,00000001,00000000,00000000,10,01,00001111,10
即
00 00000001,00000001,00000000,00000000 => 257(十进制)
10 => 0
01 00001111 => 15
10 => 0
6,BLL
前3位表示的数字,后面就跟着几个字节表示longlong值;
7,BD
前2位 | 结果 |
---|---|
00 | 后跟一个double值(8字节IEEE) |
01 | 表示1.0,后面无数据 |
10 | 表示0.0,后面无数据 |
11 | 未用 |
8,BDD
读入这种编码的数值,必须要提供一个默认值;
前2位 | 结果 |
---|---|
00 | 后面无数据,使用默认值 |
01 | 后面跟4个字节,并这4个字节替换默认值的前4个字节 |
10 | 后面跟6个字节,用其前2个字节替换默认值的5,6字节,后4个字节替换默认值的前4个字节 |
11 | 后面跟一个8字节的double值 |
9,RC,RS,RL,RD
这几个编码,其实没有编码,只需从位流中按照其原生格式读取即可;
10,MC
这是一种由不定长字节存储整型值的方法,连续读入字节,
直到读到的字节其最高位为1停止,
然后按规则组合成最终结果;
举例:
假设位流 10000010 00100100 表示一个MC值,
显然第二个字节的高位为1;
1,颠倒顺序: 00100100 10000010
2,丢弃每个字节的高位:0010010010000010
3,将剩下的位都压到右侧: __010010 00000010
4,最高2位赋0: 00010010 00000010
最后得到的值为: 0x1202
又假设位流 11101001 10010111 11100110 00110101 也表示一个MC值,
注意第四个字节的高位为1;
1,颠倒顺序: 00110101 11100110 10010111 11101001
2,丢弃每个字节的最高位:00110101111001101001011111101001
3,将剩下的位都压到右侧: ____0110 10111001 10001011 11101001
4,最高4位赋0: 00000110 10111001 10001011 11101001
最终结果为: 0x06B98BE9
这里有个需要注意的地方就是负数怎么表示?
读入的最后一个字节的第7位如果是1,则表示这个MC的值是负数.
如 10000101 01001011
0,将最后一个字节的负数标志位赋0: 10000101 00001011
1,颠倒顺序: 00001011 10000101
2,丢弃每个字节的最高位:0000101110000101
3,将剩下的位都压到右侧: __000101 10000101
4,将最高2位赋0: 00000101 10000101
5,计算: 0x0585 = 1413
6,最后取相反数,得: -1413
可见负数比正数的解码多了2步;
11,MS
与MC类似,这里的基本单位是short,而非char;
解码方式也和MC类似,但是要注意的是,颠倒顺序时,
不仅所有的short要颠倒顺序,
而且每个short内部的两个字节也要颠倒顺序,
后续操作按MC的方式进行即可;
12,H
从R13开始,dwg中所有对象都有一个句柄(Handle)与之关联以方便检索;
Handle在位流中存储格式为:code|counter|value
项目 | 长度 | 含义 |
---|---|---|
code | 4位 | handle的类型 |
counter | 4位 | value的字节数 |
value | counter个字节 | handle value 或 offset |
视code的不同,value有不同的含义.
表中的参考值可从位流中对象的上下文中取得;
code | counter | value含义 | 真正句柄值 |
---|---|---|---|
0x2, 0x3, 0x4, 0x5 | >0 | handle value | value |
0x6 | 0 | - | 参考值+1 |
0x8 | 0 | - | 参考值-1 |
0xA | >0 | offset | 参考值+offset |
0xC | >0 | offset | 参考值-offset |
13,TV
2004及之前的字符串,
一个BS表示的字节数(length),
后跟length个’\0’结尾的char代表的字符串;
14,TU
2007+版本的字符串.
一个BS表示的双字节字符数(chars),
后跟chars个’\0\0’结尾的short代表的字符串;
15,2BD,2RD,3BD,3RD
连续的BD或RD
16,BE
根据版本不同读入extrusion,3个double值;
17,BT
根据版本不同,读入Thickness,1个double值;
18,CMC/ENC
根据版本不同,读入Object/Entity的颜色;
19,OT
根据版本不同,读入Object的类型;
严格地说,BE/BT/CMC/ENC和OT不应该归到位编码这里,
因为他们不涉及新的位编码格式,
只是针对具体版本做不同的读取的简化写法;
以上就是位编码总结,是解析dwg中对象的基础;
0x04_位流解码
经过前面的结构性解码,DWG中的Section已经呈现为位编码模式了.
从位流的视角看,
我们需要按照DWG中各种对象的组织形式,连续从位流中解读位码.
但是,我们知道电脑处理的最小的单位是字节,我们也没有对连续位流的读取能力,
所以终究还是要从字节上处理.
0x04a_位流解析类
CBitChain
不妨把我们解析位流的类叫CBitChain,它的功能就是解码位流;
class CBitChain {
...
private:
uint8_t* m_chain; // 位流数据
uint32_t m_size; // 数据大小(bytes)
uint32_t m_byte; // 读取指针所在字节
uint8_t m_bit; // 读取指针所在字节中的位
}
这里要注意的是,m_bit的值与字节本身的位的值不同,顺序刚好相反,
这样定义符合我们从左到右的阅读习惯;
bit of byte in computer:76543210 76543210 76543210
bit of byte in CBitChain:01234567 01234567 01234567
此类能够从当前位置处,读取不同编码格式的数据,
并更新读取指针的值(即m_byte和m_bit的值);
这里贴一个读取RawChar的代码,其他代码就不贴了,
按照编码规则使用位运算,不难实现;
// Read 1 byte (raw char)
BITCODE_RC CBitChain::bit_read_RC()
{
uint8_t result;
uint8_t byte = m_chain[m_byte];
if (m_bit == 0)
result = byte;
else
{
result = byte << m_bit;
if (m_byte < m_size - 1)
{
byte = m_chain[m_byte + 1];
result |= byte >> (8 - m_bit);
}
}
bit_advance_position(8);
return ((uint8_t)result);
}
0x04b_字符串
2007之前的版本,字符串都是Ansi编码且夹杂在数据中间,
读取时按照对象规格,遇到字符串就从当前位流处读取.
从2007开始,字符串为UCS-2编码,
且一个对象的所有字符串都统一集中到对象的尾部存放.
这样如果仍然使用一个CBitChian来读,势必会引起不便.
因此在解码对象时,无论什么版本,
都提供2个位流解码器:
假设一个叫chain,一个叫strchain,它们的初始值相同,
对于2007+的dwg,
对象编码数据中会提供参数计算strchain的值.
以AcDb:Classes Section 为例,其数据布局为:
SN : 8D A1 C4 B8 C4 A9 F8 C5 C0 DC F4 5F E7 CF B6 8A
RL : bytesize, total bytes of data (data的范围为[bitsize,CRC)), RLL for 2010+
RL : bitsize, total bit size of data(bytesize中有效的bit数)
BL : Maxiumum class number
B : bool value
X : Class Data (format described below)
X : String stream data
B : bool value (true if string stream data is present)
RS : CRC
SN : 72 5E 3B 47 3B 56 07 3A 3F 23 0B A0 18 30 49 75
看实例:
AcDb:Classes的section map
name: AcDb:Classes
name length: 1a
data size: 529
max size: f800
encrption: 0
hashcode: 3f54045f
encoding: 4
page count: 1
page id: 13
offset: 0
size: ed00
uncompressed size: 529
compressed size: 36c
check sum: be37c4fb
crc: 27eb3fe9256530aa
AcDb:Classes section的数据
00000000 8D A1 C4 B8 C4 A9 F8 C5 C0 DC F4 5F E7 CF B6 8A
00000010 FB 04 00 00 D8 27 00 00 3F C0 40 00 27 A0 0C 3C
00000020 C0 50 14 59 2A A3 D4 06 1E 60 28 0A 2C 95 51 EC
00000030 03 0F 30 14 0D 16 4A A8 F7 01 3F C3 C7 98 0A 02
...
00000090 01 07 98 0A 04 8D A0 34 88 A7 80 31 00 35 00 32
000000A0 80 31 80 3A 00 22 00 21 00 2C 00 10 00 21 80 36
000000B0 00 30 80 39 80 39 80 32 80 39 80 23 28 20 0C 60
000000C0 08 80 0C 40 08 80 0D 20 0C 60 0E 80 0D 20 0D E0
000000D0 0D C0 0C 20 0E 40 0F 20 0A E0 0D 20 0E 80 0D 00
...
000004E0 60 03 60 02 98 03 A0 03 C8 03 60 03 28 02 68 03
000004F0 08 03 80 02 18 86 00 8A 00 98 00 98 00 A6 00 A8
00000500 00 B2 00 98 00 8A 00 9A 00 82 00 A0 01 50 47 1B
00000510 02 72 5E 3B 47 3B 56 07 3A 3F 23 0B A0 18 30 49
00000520 75 00 00 00 00 00 00 00 00
0x10:size=0x4FB
0x14:bitsize=0x27D8
0x14+0x4FB = 0x50F,以byte为边界,class的data到0x50E结束
0x14+0x27D8/8 = 0x50F,以bit为边界,class的data也到0x50E结束
本例中两个边界一致,只是巧合;
一般来说,bit的边界小于等于byte的边界;
strchain起始位置的计算,要从bit边界的尾部向前倒推;
将strchain的指针移到尾部(0x50E, 7),并记str_chain的当前位置为p;
unsigned long p = strchain.position();
char has_string = strchain.read_B();
if (has_string == 0) return;
p -= 16;
strchain.bit_set_position(p);
long str_bit_size = strchain.bit_read_RS();
if (str_bit_size && 0x8000)
{
str_bit_size &= 0x7FFF;
p -= 16;
strchain.bit_set_position(p);
long high = strchain.bit_read_RS();
str_bit_size |= (high << 15);
}
strchain.bit_set_position(p - str_bit_size);
从代码可知,字符串区的大小是分两步得到的,
先看short能否记录其大小,
若不能,则再补充一个short组合成long型表示大小,
尽可能的压榨存储空间;
代码中高位的short值向左移15位,
是因为低位的short其最高位是标志位,要用高位的short的0位替换;
截取实例中0x50A到0x50E的内容来分析:
最后,计算strchain的起始位置:
字符串区大小 = 0x23A8 bits
bit position = 0x50E*8 + 7 - 0x10 - 0x23A8 = 0x4BF
byte = 0x4BF/8 = 0x97
bit = 0x4BF - 0x97 * 8 = 7
让我们定位到0x97字节,取一些数据加以验证;
按照unicode字符串的编码格式解读如下:
对应的是第一个class的app name:ObjectDBX Classes
number: 500(1F4)
proxyflag: 0
app name: ObjectDBX Classes
cpp name: AcDbDictionaryWithDefault
dxf name: ACDBDICTIONARYWDFLT
class id: 0x1F3
num instance: 1
dwg ver: 22
maint ver: 42
0x04c_句柄
对于Object/Entity除了跟字符串类似,
也要计算,其字符串区的起始位置(2007+除外),
对象引用的句柄也是集中在句柄区,同样要计算其起始位置.
不过,句柄区一般都在对象其他属性读完之后再读取的,
因此不必另外使用一个句柄位流对象,借用chian再定位到句柄区即可.
句柄区位置的计算比较简单,不详述了.
void CDwgObjectDecoderBase::calc_handle_stream_pos()
{
SINCE(R2010)
m_handlestream_pos = m_chain.size() * 8 - m_handlestream_size;
OTHER_VERSIONS
m_handlestream_pos = m_data_start * 8 + m_bitsize;
}
0x05_数据段
DataSection
我们知道,DWG的存储逻辑上是按照Section分组的,
而且我们已经能够从DWG的数据流中解析出各种Section;
这里的Section与DXF中Section是对应的,DXF实质上就是DWG的文本表述;
自然而然,我们下一步就是解析各种Section;
涉及的Data Section为:
/// \enum Section type of R2004+
typedef enum _tagDwg2004SectionType
{
SECTION_UNKNOWN = 0, ///< The very first 160 byte
SECTION_HEADER = 1, ///< AcDb:Header
SECTION_AUXHEADER = 2, ///< AcDb:AuxHeader
SECTION_CLASSES = 3, ///< AcDb:Classes
SECTION_HANDLES = 4, ///< AcDb:Handles
SECTION_TEMPLATE = 5, ///< AcDb:Template
SECTION_OBJFREESPACE = 6, ///< AcDb:ObjFreeSpace
SECTION_OBJECTS = 7, ///< AcDb:Objects
SECTION_REVHISTORY = 8, ///< AcDb:RevHistory
SECTION_SUMMARYINFO = 9, ///< AcDb:SummaryInfo
SECTION_PREVIEW = 10, ///< AcDb:Preview
SECTION_APPINFO = 11, ///< AcDb:AppInfo
SECTION_APPINFOHISTORY = 12, ///< AcDb:AppInfoHistory
SECTION_FILEDEPLIST = 13, ///< AcDb:FileDepList
SECTION_SECURITY, ///< AcDb:Security, if stored with a password
SECTION_VBAPROJECT, ///< AcDb:VBAProject
SECTION_SIGNATURE, ///< AcDb:Signature
SECTION_ACDS, ///< AcDb:AcDsPrototype_1b = 12 (ACIS datastorage)
} Dwg2004SectionType;
从本节开始,将选取部分Section加以解读,
这些Section也是我们平常比较关心的内容,
对其他的Section感兴趣的可以自己对照ODA的说明加以分析;
计划选择以下Section并按此顺序分析:
• AcDbPreview
• AcDbClasses
• AcdbHandles
• AcDbObjects
0x05a_预览图
AcDb:Preview
这个Section是存放DWG的预览图的;
它既可以按照常规Section的方式定位读取,
也可以直接从DWG头中根据参数(0x0D处的4字节的偏移量)直接定位解读;
通过直接读取的方式,
不必解析System Section就可以快速得到预览图像;
这块数据是无压缩的,也没有使用位编码,是可以这直接读的,
尽管仍然可以用位流方式读取数据(只是效率要低一些);
而且,每个Image的起始地址start是DWG二进制数据中的绝对地址,
这样安排,都是为了方便在打开文件时,能够快速显示预览图;
Section的布局如下:
项目 | 长度(byte) | 说明 |
---|---|---|
SN | 16 | 开始标记: 1F 25 6D 07 D4 36 28 28 9D 57 CA 3F 9D 44 10 2B |
size | RL | image数据总长度,计数范围从count开始,到image data结束 |
count | RC | image的数量 |
image param | - | count组 |
image data | - | count组 |
SN | 16 | 结束标记: E0 DA 92 F8 2B C9 D7 D7 62 A8 35 C0 62 BB EF D4 |
每组Image param的布局:
项目 | 长度(byte) | 说明 |
---|---|---|
code | RC | Image类型 |
start | RL | Image起始地址 |
ImgSize | RL | Image数据长度 |
Image类型:
code | 类型 | 说明 |
---|---|---|
1 | HDR | 全是0 |
2 | BMP | 只有BMP的位图数据,如果要保存到文件,需要自己加上文件头 |
3 | WMF | 完整的WMF,可以直接保存到文件 |
4 | - | 未知 |
5 | - | 未知 |
6 | PNG | 完整的PNG图像,可以直接保存到文件 |
一个实例:img
0x05b_位流
AcDb:Classes
这个Section在 0x04b_字符串 中已经做了较为详细的说明.
0x05c_句柄区
AcDb:Handles
简单说,AcDb:Handles Section描述的是对象句柄,与其存储位置的对应关系,
称为Object Map,其结构简单易于理解;
其中的存储位置,
指的是该对象在AcDb:Objects Section,在解码解压后的数据中的绝对地址;
首先要明确的是,其数据是分页存储的;
每页以一个BigEndian short值,表示页尺寸(page data + crc的尺寸),
当页尺寸值为2(即只包含一个CRC)时,则结束;
贴代码比文字描述要轻松,其实也没什么需要再描述的了;
void CObjectMap::decode(CDataSection::Result data)
{
if (data.buffer == 0 || data.size == 0) return;
CBitChain chain(data.buffer, data.size, UNKNOWN);
uint16_t page_size = 0;
m_obj_map.clear();
do
{
uint32_t page_start = chain.byte();
// page_size: raw short saved as BigEndian
unsigned char sgdc[2];
sgdc[0] = chain.bit_read_RC();
sgdc[1] = chain.bit_read_RC();
page_size = (sgdc[0] << 8) | sgdc[1];
// ODA的描述中是把omd的初始化放在do循环的前面,这是个bug,应该对每页数据初始化
// 也就是说,每页开始的那个handle和offset是初值,后续的应累加计算
ObjMapData omd = { 0, 0 };
while (chain.byte() - page_start < page_size)
{
// the offset from the previous handle. default: 1, unsigned
uint32_t hdl_offset = chain.bit_read_UMC();
// the offset from the previous address. default: object size, signed
int32_t loc_offset = chain.bit_read_MC();
omd.handle += hdl_offset;
omd.offset += loc_offset;
m_obj_map.push_back(omd);
}
// move cursor to CRC
chain.set_byte(page_start + page_size);
chain.set_bit(0);
// ODA:这个CRC是BE short;反正现在没用到它,就这么读过吧
uint16_t crc = chain.bit_read_RS();
} while (page_size > 2);
}
0x05d_对象区
AcDb:AcDbObjects
AcDb:AcDbObject Section存储着整个DWG文件中的所有对象,
包括Object和Entity;
通常,这个Section一般会由多个Page组成;
通过0x01c_系统段中的方法
可以容易地得到Section Data,
并且藉由ObjectMap根据Object的handle定位到编码的数据,
最后,根据Object的类型解码得到具体的Object;
在我们得到的data中,前4个字节(CA 0D 00 00)含义未明;
00000000 CA 0D 00 00 17 00 8D 01 0C 00 40 6A 41 48 04 3A
00000010 C4 40 28 24 40 BE A4 40 C2 24 06 23 E6 3A 5F 83
00000020 73 71 00 DD 04 0C 40 47 EA 90 8B 54 01 50 04 30
00000030 A8 01 34 01 BC 01 90 01 94 01 B0 01 7C 01 4C 01
对该例,其Object map如下,可以验证,对象的起始地址从4开始:
Object Map
(212 objs)
**************
No. Handle Offset
1 1 4
2 2 77A
3 3 791
4 5 7A2
5 6 7B8
6 7 7C5
7 8 7D0
...
一般地,handle为1的对象是Block Control,
handle为2的对象是Layer Control;
要解码对象,我们必须要知道对象是如何编码的,即他们的基本排布结构;
这里,贴出一个我整理的dwg对象的布局图(截止到R2018版);
其中的part6是与Object/Entity相关的各种参数,
part8和part10分别是Object和Entity相关的各种handle,
其余部分都是通用的;
字符串流
从R2007开始,
Object的字符串从ansi编码转为ucs-2编码,
并且把散布于各处的字符串统一归集到:对象尾部的handle stream
前,
称为string stream
,
所有解码过程中,要读取的字符串都要从这个流中读取;
因此,在解码时,
我们要根据object头部的size信息
计算出string stream
和handle stream
的位置;
由于string stream
的最后的那个标志位在handle stream
的正上方,
所以string stream
位置的计算要依赖handle stream
的计算;
handle stream
位置的计算,
分为2000-2007版和2007之后的版本两种方式(这里不考虑R13/14),
因为他们存储的size信息不同;
两种stream和各种size表示的范围,我都在图上标明了,
可以很清晰地看出如何计算其位置;
有了handle stream
的位置,
再根据其正上方的string stream
标志位,
采用与计算section的string stream
相同的方法,
即可得到string stream
的位置;
取handle为2的Layer Control的数据(地址范围[77A,790]),
做一个解码示例;
数据片段:
00000760 7B 63 96 C4 81 94 44 3F 54 88 0B 00 15 01 40 75
00000770 63 5A 00 00 87 4A 22 1F B6 B0 12 00 65 0C 80 40
00000780 A8 40 C8 06 40 25 C4 22 04 40 2C 84 40 62 3F E0
00000790 A2 0C 00 35 0D 00 40 EA 40 88 04 22 24 40 7B 3F
000007A0 44 7C 11 00 5D 0E 00 41 6A 40 C8 04 22 C4 40 7B
解码结果:
Object(LAYER_CONTROL)
handle: 0.1.2
owner: 4.0.0
xdic missing flag: 0
xdicobjhandle: 3.2.12E
num reactors: 0
entries: 3
1), 2.1.10
2), 2.2.164
3), 2.2.311
解码图示:
本节所述的Object的解码,
是解读DWG文件中戏份最多的,也是我们最关心的部分;
关于所有Object的解码规范,可以参考ODA的文档;
对DWG文件的解析,基本上差不多了,这里就暂时告一段落;
当然,还有其他Section以及很多细节没有提及
(比如Color,我以前专门写过几篇文章,在本系列里就没有解释),
以后再考虑是否补充吧;