Added block walker implementations + tests

This commit is contained in:
Sybren A. Stüvel
2018-02-27 17:30:28 +01:00
parent 6b9c0a3f95
commit 65b690e998
29 changed files with 337 additions and 75 deletions
+3 -2
View File
@@ -1,9 +1,10 @@
from . import BlendFileBlock
from .dna import FieldPath
def listbase(block: BlendFileBlock) -> BlendFileBlock:
def listbase(block: BlendFileBlock, next_path: FieldPath=b'next') -> BlendFileBlock:
"""Generator, yields all blocks in the ListBase linked list."""
while block:
yield block
next_ptr = block[b'next']
next_ptr = block[next_path]
block = block.bfile.find_block_from_address(next_ptr)
+3 -3
View File
@@ -2,8 +2,8 @@ import logging
import pathlib
import typing
from blender_asset_tracer import bpathlib, blendfile
from . import result, blocks
from blender_asset_tracer import blendfile
from . import result, block_walkers
log = logging.getLogger(__name__)
@@ -18,7 +18,7 @@ def deps(bfilepath: pathlib.Path) -> typing.Iterator[result.BlockUsage]:
with blendfile.BlendFile(bfilepath) as bfile:
for block in asset_holding_blocks(bfile):
yield from blocks.from_block(block)
yield from block_walkers.from_block(block)
# TODO: handle library blocks for recursion.
@@ -0,0 +1,174 @@
"""Block walkers.
From a BlendFileBlock, the block walker functions yield BlockUsage objects.
The top-level block walkers are implemented as _from_block_XX() function,
where XX is the DNA code of the block.
"""
import functools
import logging
import sys
import typing
from blender_asset_tracer import blendfile, bpathlib
from blender_asset_tracer.blendfile import iterators
from . import result, cdefs
log = logging.getLogger(__name__)
_warned_about_types = set()
def from_block(block: blendfile.BlendFileBlock) -> typing.Iterator[result.BlockUsage]:
assert block.code != b'DATA'
module = sys.modules[__name__]
funcname = '_from_block_' + block.code.decode().lower()
try:
block_reader = getattr(module, funcname)
except AttributeError:
if block.code not in _warned_about_types:
log.warning('No reader implemented for block type %r', block.code.decode())
_warned_about_types.add(block.code)
return
log.debug('Tracing block %r', block)
yield from block_reader(block)
def skip_packed(wrapped):
"""Decorator, skip blocks where 'packedfile' is set to true."""
@functools.wraps(wrapped)
def wrapper(block: blendfile.BlendFileBlock, *args, **kwargs):
if block.get(b'packedfile', default=False):
return
yield from wrapped(block, *args, **kwargs)
return wrapper
def _from_block_cf(block: blendfile.BlendFileBlock) -> typing.Iterator[result.BlockUsage]:
"""Cache file data blocks."""
path, field = block.get(b'filepath', return_field=True)
yield result.BlockUsage(block, path, path_full_field=field)
@skip_packed
def _from_block_im(block: blendfile.BlendFileBlock) -> typing.Iterator[result.BlockUsage]:
"""Image data blocks."""
# old files miss this
image_source = block.get(b'source', default=cdefs.IMA_SRC_FILE)
if image_source not in {cdefs.IMA_SRC_FILE, cdefs.IMA_SRC_SEQUENCE, cdefs.IMA_SRC_MOVIE}:
return
pathname, field = block.get(b'name', return_field=True)
is_sequence = image_source == cdefs.IMA_SRC_SEQUENCE
yield result.BlockUsage(block, pathname, is_sequence, path_full_field=field)
def _from_block_me(block: blendfile.BlendFileBlock) -> typing.Iterator[result.BlockUsage]:
"""Mesh data blocks."""
block_external = block.get_pointer((b'ldata', b'external'), None)
if block_external is None:
block_external = block.get_pointer((b'fdata', b'external'), None)
if block_external is None:
return
path, field = block_external.get(b'filename', return_field=True)
yield result.BlockUsage(block, path, path_full_field=field)
def _from_block_mc(block: blendfile.BlendFileBlock) -> typing.Iterator[result.BlockUsage]:
"""MovieClip data blocks."""
path, field = block.get(b'name', return_field=True)
# TODO: The assumption that this is not a sequence may not be true for all modifiers.
yield result.BlockUsage(block, path, is_sequence=False, path_full_field=field)
def _from_block_ob(block: blendfile.BlendFileBlock) -> typing.Iterator[result.BlockUsage]:
"""Object data blocks."""
# 'ob->modifiers[...].filepath'
ob_idname = block[b'id', b'name']
mods = block.get_pointer((b'modifiers', b'first'))
for mod_idx, block_mod in enumerate(iterators.listbase(mods, next_path=(b'modifier', b'next'))):
block_name = b'%s.modifiers[%d]' % (ob_idname, mod_idx)
log.debug('Tracing modifier %s', block_name.decode())
mod_type = block_mod[b'modifier', b'type']
if mod_type == cdefs.eModifierType_Ocean:
if block_mod[b'cached']:
path, field = block_mod.get(b'cachepath', return_field=True)
# The path indicates the directory containing the cached files.
yield result.BlockUsage(block_mod, path, is_sequence=True, path_full_field=field,
block_name=block_name)
elif mod_type == cdefs.eModifierType_MeshCache:
path, field = block_mod.get(b'filepath', return_field=True)
yield result.BlockUsage(block_mod, path, is_sequence=False, path_full_field=field,
block_name=block_name)
def _from_block_sc(block: blendfile.BlendFileBlock) -> typing.Iterator[result.BlockUsage]:
"""Scene data blocks."""
# Sequence editor is the only interesting bit.
block_ed = block.get_pointer(b'ed')
if block_ed is None:
return
single_asset_types = {cdefs.SEQ_TYPE_MOVIE, cdefs.SEQ_TYPE_SOUND_RAM, cdefs.SEQ_TYPE_SOUND_HD}
asset_types = single_asset_types.union({cdefs.SEQ_TYPE_IMAGE})
def iter_seqbase(seqbase) -> typing.Iterator[result.BlockUsage]:
"""Generate results from a ListBase of sequencer strips."""
for seq in iterators.listbase(seqbase):
seq.refine_type(b'Sequence')
seq_type = seq[b'type']
if seq_type == cdefs.SEQ_TYPE_META:
# Recurse into this meta-sequence.
subseq = seq.get_pointer((b'seqbase', b'first'))
yield from iter_seqbase(subseq)
continue
if seq_type not in asset_types:
continue
seq_strip = seq.get_pointer(b'strip')
if seq_strip is None:
continue
seq_stripdata = seq_strip.get_pointer(b'stripdata')
if seq_stripdata is None:
continue
dirname, dn_field = seq_strip.get(b'dir', return_field=True)
basename, bn_field = seq_stripdata.get(b'name', return_field=True)
asset_path = bpathlib.BlendPath(dirname) / basename
is_sequence = seq_type not in single_asset_types
yield result.BlockUsage(seq, asset_path,
is_sequence=is_sequence,
path_dir_field=dn_field,
path_base_field=bn_field)
sbase = block_ed.get_pointer((b'seqbase', b'first'))
yield from iter_seqbase(sbase)
@skip_packed
def _from_block_so(block: blendfile.BlendFileBlock) -> typing.Iterator[result.BlockUsage]:
"""Sound data blocks."""
path, field = block.get(b'name', return_field=True)
yield result.BlockUsage(block, path, path_full_field=field)
@skip_packed
def _from_block_vf(block: blendfile.BlendFileBlock) -> typing.Iterator[result.BlockUsage]:
"""Vector Font data blocks."""
path, field = block.get(b'name', return_field=True)
if path == b'<builtin>': # builtin font
return
yield result.BlockUsage(block, path, path_full_field=field)
-49
View File
@@ -1,49 +0,0 @@
import logging
import sys
import typing
from blender_asset_tracer import blendfile
from . import result, cdefs
log = logging.getLogger(__name__)
_warned_about_types = set()
class NoReaderImplemented(NotImplementedError):
"""There is no reader implementation for a specific block code."""
def __init__(self, message: str, code: bytes):
super().__init__(message)
self.code = code
def from_block(block: blendfile.BlendFileBlock) -> typing.Iterator[result.BlockUsage]:
assert block.code != b'DATA'
module = sys.modules[__name__]
funcname = '_from_block_' + block.code.decode().lower()
try:
block_reader = getattr(module, funcname)
except AttributeError:
if block.code not in _warned_about_types:
log.warning('No reader implemented for block type %r', block.code.decode())
_warned_about_types.add(block.code)
return
yield from block_reader(block)
def _from_block_im(block: blendfile.BlendFileBlock) -> typing.Iterator[result.BlockUsage]:
# old files miss this
image_source = block.get(b'source', default=cdefs.IMA_SRC_FILE)
if image_source not in {cdefs.IMA_SRC_FILE, cdefs.IMA_SRC_SEQUENCE, cdefs.IMA_SRC_MOVIE}:
return
if block[b'packedfile']:
return
pathname, field = block.get(b'name', return_field=True)
# TODO: the receiver should inspect the 'source' property too, and if set
# to cdefs.IMA_SRC_SEQUENCE yield the entire sequence of files.
yield result.BlockUsage(block, field, pathname)
+1
View File
@@ -34,6 +34,7 @@ IMA_SRC_SEQUENCE = 2
IMA_SRC_MOVIE = 3
# DNA_modifier_types.h
eModifierType_Ocean = 39
eModifierType_MeshCache = 46
# DNA_particle_types.h
+52 -14
View File
@@ -1,35 +1,73 @@
from blender_asset_tracer import blendfile, bpathlib
from blender_asset_tracer.blendfile import dna
class BlockUsage:
"""Represents the use of an asset by a data block.
:ivar block_name: an identifying name for this block. Defaults to the ID
name of the block.
:ivar block:
:type block: blendfile.BlendFileBlock
:ivar field:
:type field: dna.Field
:ivar asset_path:
:type asset_path: bpathlib.BlendPath
:ivar is_sequence: Indicates whether this file is alone (False), the
first of a sequence (True, and the path points to a file), or a
directory containing a sequence (True, and path points to a directory).
In certain cases such files should be reported once (f.e. when
rewriting the source field to another path), and in other cases the
sequence should be expanded (f.e. when copying all assets to a BAT
Pack).
:ivar path_full_field: field containing the full path of this asset.
:ivar path_dir_field: field containing the parent path (i.e. the
directory) of this asset.
:ivar path_base_field: field containing the basename of this asset.
"""
def __init__(self, block, field, asset_path):
self.block_idname = block[b'id', b'name']
from blender_asset_tracer import blendfile, bpathlib
from blender_asset_tracer.blendfile import dna
def __init__(self,
block: blendfile.BlendFileBlock,
asset_path: bpathlib.BlendPath,
is_sequence: bool = False,
path_full_field: dna.Field = None,
path_dir_field: dna.Field = None,
path_base_field: dna.Field = None,
block_name: bytes = '',
):
if block_name:
self.block_name = block_name
else:
try:
self.block_name = block[b'id', b'name']
except KeyError:
try:
self.block_name = block[b'name']
except KeyError:
self.block_name = b'-unnamed-'
assert isinstance(block, blendfile.BlendFileBlock)
assert isinstance(field, dna.Field), 'field should be dna.Field, not %r' % type(field)
assert isinstance(asset_path, (bytes, bpathlib.BlendPath)), \
'asset_path should be BlendPath, not %r' % type(asset_path)
if path_full_field is None:
assert isinstance(path_dir_field, dna.Field), \
'path_dir_field should be dna.Field, not %r' % type(path_dir_field)
assert isinstance(path_base_field, dna.Field), \
'path_base_field should be dna.Field, not %r' % type(path_base_field)
else:
assert isinstance(path_full_field, dna.Field), \
'path_full_field should be dna.Field, not %r' % type(path_full_field)
if isinstance(asset_path, bytes):
asset_path = bpathlib.BlendPath(asset_path)
self.block = block
self.field = field
self.asset_path = asset_path
self.is_sequence = bool(is_sequence)
self.path_full_field = path_full_field
self.path_dir_field = path_dir_field
self.path_base_field = path_base_field
def __repr__(self):
return '<BlockUsage name=%r type=%r field=%r asset=%r>' % (
self.block_idname, self.block.dna_type_name,
self.field.name.name_full.decode(), self.asset_path)
return '<BlockUsage name=%r type=%r field=%r asset=%r%s>' % (
self.block_name, self.block.dna_type_name,
self.path_full_field.name.name_full.decode(), self.asset_path,
' sequence' if self.is_sequence else ''
)