解析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,丢弃每个字节的高位: 00100100 10000010
3,将剩下的位都压到右侧: __010010 00000010
4,最高2位赋0: 00010010 00000010
最后得到的值为: 0x1202

又假设位流 11101001 10010111 11100110 00110101 也表示一个MC值,
注意第四个字节的高位为1;

1,颠倒顺序: 00110101 11100110 10010111 11101001
2,丢弃每个字节的最高位: 00110101 11100110 10010111 11101001
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,丢弃每个字节的最高位: 00001011 10000101
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 streamhandle 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,我以前专门写过几篇文章,在本系列里就没有解释),
以后再考虑是否补充吧;

posted @ 2024-11-07 06:53  惊惊  阅读(139)  评论(0编辑  收藏  举报