Implemented/fixed/ported loading of blend files and getting blocks & props

This commit is contained in:
Sybren A. Stüvel 2018-02-22 15:52:10 +01:00
parent e66b1edaf4
commit 9c7791f762
6 changed files with 93 additions and 38 deletions

View File

@ -102,18 +102,19 @@ class BlendFile:
self.sdna_index_from_id = {} self.sdna_index_from_id = {}
self.block_from_offset = {} self.block_from_offset = {}
self.load_dna1_block()
def load_dna1_block(self): def load_dna1_block(self):
"""Read the blend file to load its DNA structure to memory.""" """Read the blend file to load its DNA structure to memory."""
fileobj = self.fileobj
while True: while True:
block = BlendFileBlock(fileobj, self) block = BlendFileBlock(self)
if block.code == b'ENDB': if block.code == b'ENDB':
break break
if block.code == b'DNA1': if block.code == b'DNA1':
self.structs, self.sdna_index_from_id = self.decode_structs(block) self.structs, self.sdna_index_from_id = self.decode_structs(block)
else: else:
fileobj.seek(block.size, os.SEEK_CUR) self.fileobj.seek(block.size, os.SEEK_CUR)
self.blocks.append(block) self.blocks.append(block)
self.code_index[block.code].append(block) self.code_index[block.code].append(block)
@ -255,8 +256,8 @@ class BlendFileBlock:
old_structure = struct.Struct(b'4sI') old_structure = struct.Struct(b'4sI')
"""old blend files ENDB block structure""" """old blend files ENDB block structure"""
def __init__(self, fileobj: typing.BinaryIO, bfile: BlendFile): def __init__(self, bfile: BlendFile):
self.file = bfile self.bfile = bfile
# Defaults; actual values are set by interpreting the block header. # Defaults; actual values are set by interpreting the block header.
self.code = b'' self.code = b''
@ -267,11 +268,12 @@ class BlendFileBlock:
self.file_offset = 0 self.file_offset = 0
"""Offset in bytes from start of file to beginning of the data block. """Offset in bytes from start of file to beginning of the data block.
Points to the data after the block header. Points to the data after the block header.
""" """
self.endian = bfile.header.endian
header_struct = bfile.block_header_struct header_struct = bfile.block_header_struct
data = fileobj.read(header_struct.size) data = bfile.fileobj.read(header_struct.size)
if len(data) != header_struct.size: if len(data) != header_struct.size:
self.log.warning("Blend file %s seems to be truncated, " self.log.warning("Blend file %s seems to be truncated, "
"expected %d bytes but could read only %d", "expected %d bytes but could read only %d",
@ -286,17 +288,17 @@ class BlendFileBlock:
if len(data) <= 15: if len(data) <= 15:
self.log.debug('interpreting block as old-style ENB block') self.log.debug('interpreting block as old-style ENB block')
blockheader = self.old_structure.unpack(data) blockheader = self.old_structure.unpack(data)
self.code = dna_io.read_data0(blockheader[0]) self.code = self.endian.read_data0(blockheader[0])
return return
blockheader = header_struct.unpack(data) blockheader = header_struct.unpack(data)
self.code = dna_io.read_data0(blockheader[0]) self.code = self.endian.read_data0(blockheader[0])
if self.code != b'ENDB': if self.code != b'ENDB':
self.size = blockheader[1] self.size = blockheader[1]
self.addr_old = blockheader[2] self.addr_old = blockheader[2]
self.sdna_index = blockheader[3] self.sdna_index = blockheader[3]
self.count = blockheader[4] self.count = blockheader[4]
self.file_offset = fileobj.tell() self.file_offset = bfile.fileobj.tell()
def __str__(self): def __str__(self):
return "<%s.%s (%s), size=%d at %s>" % ( return "<%s.%s (%s), size=%d at %s>" % (
@ -308,22 +310,22 @@ class BlendFileBlock:
) )
@property @property
def dna_type(self): def dna_type(self) -> dna.Struct:
return self.file.structs[self.sdna_index] return self.bfile.structs[self.sdna_index]
@property @property
def dna_type_name(self): def dna_type_name(self) -> str:
return self.dna_type.dna_type_id.decode('ascii') return self.dna_type.dna_type_id.decode('ascii')
def refine_type_from_index(self, sdna_index_next): def refine_type_from_index(self, sdna_index_next):
assert (type(sdna_index_next) is int) assert (type(sdna_index_next) is int)
sdna_index_curr = self.sdna_index sdna_index_curr = self.sdna_index
self.file.ensure_subtype_smaller(sdna_index_curr, sdna_index_next) self.bfile.ensure_subtype_smaller(sdna_index_curr, sdna_index_next)
self.sdna_index = sdna_index_next self.sdna_index = sdna_index_next
def refine_type(self, dna_type_id): def refine_type(self, dna_type_id):
assert (type(dna_type_id) is bytes) assert (type(dna_type_id) is bytes)
self.refine_type_from_index(self.file.sdna_index_from_id[dna_type_id]) self.refine_type_from_index(self.bfile.sdna_index_from_id[dna_type_id])
def get_file_offset(self, path, def get_file_offset(self, path,
default=..., default=...,
@ -333,24 +335,24 @@ class BlendFileBlock:
""" """
Return (offset, length) Return (offset, length)
""" """
assert (type(path) is bytes) assert isinstance(path, bytes)
ofs = self.file_offset ofs = self.file_offset
if base_index != 0: if base_index != 0:
assert (base_index < self.count) assert (base_index < self.count)
ofs += (self.size // self.count) * base_index ofs += (self.size // self.count) * base_index
self.file.handle.seek(ofs, os.SEEK_SET) self.bfile.fileobj.seek(ofs, os.SEEK_SET)
if sdna_index_refine is None: if sdna_index_refine is None:
sdna_index_refine = self.sdna_index sdna_index_refine = self.sdna_index
else: else:
self.file.ensure_subtype_smaller(self.sdna_index, sdna_index_refine) self.bfile.ensure_subtype_smaller(self.sdna_index, sdna_index_refine)
dna_struct = self.file.structs[sdna_index_refine] dna_struct = self.bfile.structs[sdna_index_refine]
field = dna_struct.field_from_path( field = dna_struct.field_from_path(
self.file.header, self.file.handle, path) self.bfile.header, self.bfile.fileobj, path)
return (self.file.handle.tell(), field.dna_name.array_size) return self.bfile.fileobj.tell(), field.dna_name.array_size
def get(self, path, def get(self, path,
default=..., default=...,
@ -363,16 +365,16 @@ class BlendFileBlock:
if base_index != 0: if base_index != 0:
assert (base_index < self.count) assert (base_index < self.count)
ofs += (self.size // self.count) * base_index ofs += (self.size // self.count) * base_index
self.file.handle.seek(ofs, os.SEEK_SET) self.bfile.fileobj.seek(ofs, os.SEEK_SET)
if sdna_index_refine is None: if sdna_index_refine is None:
sdna_index_refine = self.sdna_index sdna_index_refine = self.sdna_index
else: else:
self.file.ensure_subtype_smaller(self.sdna_index, sdna_index_refine) self.bfile.ensure_subtype_smaller(self.sdna_index, sdna_index_refine)
dna_struct = self.file.structs[sdna_index_refine] dna_struct = self.bfile.structs[sdna_index_refine]
return dna_struct.field_get( return dna_struct.field_get(
self.file.header, self.file.handle, path, self.bfile.header, self.bfile.fileobj, path,
default=default, default=default,
nil_terminated=use_nil, as_str=use_str, nil_terminated=use_nil, as_str=use_str,
) )
@ -395,11 +397,11 @@ class BlendFileBlock:
self.get(path_full, default, sdna_index_refine, use_nil, use_str, base_index)) self.get(path_full, default, sdna_index_refine, use_nil, use_str, base_index))
except NotImplementedError as ex: except NotImplementedError as ex:
msg, dna_name, dna_type = ex.args msg, dna_name, dna_type = ex.args
struct_index = self.file.sdna_index_from_id.get(dna_type.dna_type_id, None) struct_index = self.bfile.sdna_index_from_id.get(dna_type.dna_type_id, None)
if struct_index is None: if struct_index is None:
yield (path_full, "<%s>" % dna_type.dna_type_id.decode('ascii')) yield (path_full, "<%s>" % dna_type.dna_type_id.decode('ascii'))
else: else:
struct = self.file.structs[struct_index] struct = self.bfile.structs[struct_index]
for f in struct.fields: for f in struct.fields:
yield from self.get_recursive_iter( yield from self.get_recursive_iter(
f.dna_name.name_only, path_full, default, None, use_nil, use_str, 0) f.dna_name.name_only, path_full, default, None, use_nil, use_str, 0)
@ -433,13 +435,13 @@ class BlendFileBlock:
if sdna_index_refine is None: if sdna_index_refine is None:
sdna_index_refine = self.sdna_index sdna_index_refine = self.sdna_index
else: else:
self.file.ensure_subtype_smaller(self.sdna_index, sdna_index_refine) self.bfile.ensure_subtype_smaller(self.sdna_index, sdna_index_refine)
dna_struct = self.file.structs[sdna_index_refine] dna_struct = self.bfile.structs[sdna_index_refine]
self.file.handle.seek(self.file_offset, os.SEEK_SET) self.bfile.handle.seek(self.file_offset, os.SEEK_SET)
self.file.is_modified = True self.bfile.is_modified = True
return dna_struct.field_set( return dna_struct.field_set(
self.file.header, self.file.handle, path, value) self.bfile.header, self.bfile.handle, path, value)
# --------------- # ---------------
# Utility get/set # Utility get/set
@ -459,13 +461,13 @@ class BlendFileBlock:
if type(result) is not int: if type(result) is not int:
return result return result
assert (self.file.structs[sdna_index_refine].field_from_path( assert (self.bfile.structs[sdna_index_refine].field_from_path(
self.file.header, self.file.handle, path).dna_name.is_pointer) self.bfile.header, self.bfile.handle, path).dna_name.is_pointer)
if result != 0: if result != 0:
# possible (but unlikely) # possible (but unlikely)
# that this fails and returns None # that this fails and returns None
# maybe we want to raise some exception in this case # maybe we want to raise some exception in this case
return self.file.find_block_from_offset(result) return self.bfile.find_block_from_offset(result)
else: else:
return None return None

View File

@ -76,6 +76,9 @@ class Field:
self.size = size self.size = size
self.offset = offset self.offset = offset
def __repr__(self):
return '<%r %r (%s)>' % (type(self).__qualname__, self.name, self.dna_type)
class Struct: class Struct:
"""dna.Struct is a C-type structure stored in the DNA.""" """dna.Struct is a C-type structure stored in the DNA."""

View File

@ -17,7 +17,10 @@ class EndianIO:
@classmethod @classmethod
def _read(cls, fileobj: typing.BinaryIO, typestruct: struct.Struct): def _read(cls, fileobj: typing.BinaryIO, typestruct: struct.Struct):
data = fileobj.read(typestruct.size) data = fileobj.read(typestruct.size)
return typestruct.unpack(data)[0] try:
return typestruct.unpack(data)[0]
except struct.error as ex:
raise struct.error('%s (read %d bytes)' % (ex, len(data))) from None
@classmethod @classmethod
def read_char(cls, fileobj: typing.BinaryIO): def read_char(cls, fileobj: typing.BinaryIO):

View File

@ -1,4 +1,5 @@
import logging import logging
import os
import pathlib import pathlib
import struct import struct
import typing import typing
@ -12,13 +13,14 @@ class BlendFileHeader:
""" """
BlendFileHeader represents the first 12 bytes of a blend file. BlendFileHeader represents the first 12 bytes of a blend file.
it contains information about the hardware architecture, which is relevant It contains information about the hardware architecture, which is relevant
to the structure of the rest of the file. to the structure of the rest of the file.
""" """
structure = struct.Struct(b'7s1s1s3s') structure = struct.Struct(b'7s1s1s3s')
def __init__(self, fileobj: typing.BinaryIO, path: pathlib.Path): def __init__(self, fileobj: typing.BinaryIO, path: pathlib.Path):
log.debug("reading blend-file-header %s", path) log.debug("reading blend-file-header %s", path)
fileobj.seek(0, os.SEEK_SET)
header = fileobj.read(self.structure.size) header = fileobj.read(self.structure.size)
values = self.structure.unpack(header) values = self.structure.unpack(header)

Binary file not shown.

View File

@ -0,0 +1,45 @@
import pathlib
import unittest
import os
from blender_asset_tracer import blendfile
class BlendLoadingTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.blendfiles = pathlib.Path(__file__).with_name('blendfiles')
def setUp(self):
self.bf = None
def tearDown(self):
if self.bf:
self.bf.close()
def test_some_properties(self):
self.bf = blendfile.BlendFile(self.blendfiles / 'basic_file.blend')
self.assertFalse(self.bf.is_compressed)
self.assertEqual(1, len(self.bf.code_index[b'OB']))
ob = self.bf.code_index[b'OB'][0]
self.assertEqual('Object', ob.dna_type_name)
# Try low level operation to read a property.
self.bf.fileobj.seek(ob.file_offset, os.SEEK_SET)
loc = ob.dna_type.field_get(self.bf.header, self.bf.fileobj, b'loc')
self.assertEqual(loc, [2.0, 3.0, 5.0])
# Try high level operation to read the same property.
loc = ob.get(b'loc')
self.assertEqual(loc, [2.0, 3.0, 5.0])
# Try getting a subproperty.
name = ob.get((b'id', b'name'))
self.assertEqual('OBümlaut', name)
# Try following a pointer.
mesh_ptr = ob.get(b'data')
mesh = self.bf.block_from_offset[mesh_ptr]
mname = mesh.get((b'id', b'name'))
self.assertEqual('MECube³', mname)