Files
test/scripts/modules/blend_render_info.py
Jacques Lucke f0c7e52ff2 Core: extract blendfile_header.py as common utility for parsing .blend files
This new file can parse the file header (first few bytes) as well as the block
headers.

Right now, this is used by two places:
* `blendfile.py` which is used by `blend2json.py`
* `blend_render_info.py`

This new module is shipped with Blender because it's needed for
`blend_render_info.py` which is shipped with Blender too. This makes using it in
`blendfile.py` (which is not shipped with Blender) a bit more annoying. However,
this is already not ideal, because e.g. `blend2json` also has to add to
`sys.path` already to be able to import `blendfile.py`.

This new file could also be used by blender-asset-tracer (BAT).

The new `BlendFileHeader` and `BlockHeader` types may be subclassed by code
using it, because it wants to store additional derived data (`blendfile.py` and
BAT need this).

New tests have been added that check that the file and block header is parsed
correctly for different kinds of .blend files.

Pull Request: https://projects.blender.org/blender/blender/pulls/140341
2025-06-23 12:53:55 +02:00

125 lines
3.8 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2010-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
# This module can get render info without running from inside blender.
__all__ = (
"read_blend_rend_chunk",
)
import blendfile_header
class RawBlendFileReader:
"""
Return a file handle to the raw blend file data (abstracting compressed formats).
"""
__slots__ = (
# The path to load.
"_filepath",
# The file base file handler or None (only set for compressed formats).
"_blendfile_base",
# The file handler to return to the caller (always uncompressed data).
"_blendfile",
)
def __init__(self, filepath):
self._filepath = filepath
self._blendfile_base = None
self._blendfile = None
def __enter__(self):
blendfile = open(self._filepath, "rb")
blendfile_base = None
head = blendfile.read(4)
blendfile.seek(0)
if head[0:2] == b'\x1f\x8b': # GZIP magic.
import gzip
blendfile_base = blendfile
blendfile = gzip.open(blendfile, "rb")
elif head[0:4] == b'\x28\xb5\x2f\xfd': # Z-standard magic.
import zstandard
blendfile_base = blendfile
blendfile = zstandard.open(blendfile, "rb")
self._blendfile_base = blendfile_base
self._blendfile = blendfile
return self._blendfile
def __exit__(self, _exc_type, _exc_value, _exc_traceback):
self._blendfile.close()
if self._blendfile_base is not None:
self._blendfile_base.close()
return False
def get_render_info_structure(endian_str, size):
import struct
# The maximum size of the scene name changed over time, so create a different
# structure depending on the size of the entire block.
if size == 2 * 4 + 24:
return struct.Struct(endian_str + b'ii24s')
if size == 2 * 4 + 64:
return struct.Struct(endian_str + b'ii64s')
if size == 2 * 4 + 256:
return struct.Struct(endian_str + b'ii256s')
raise ValueError("Unknown REND chunk size: {:d}".format(size))
def _read_blend_rend_chunk_from_file(blendfile, filepath):
import struct
import sys
from os import SEEK_CUR
try:
blender_header = blendfile_header.BlendFileHeader(blendfile)
except blendfile_header.BlendHeaderError:
sys.stderr.write("Not a blend file: {:s}\n".format(filepath))
return []
scenes = []
endian_str = b'<' if blender_header.is_little_endian else b'>'
block_header_struct = blender_header.create_block_header_struct()
while bhead := blendfile_header.BlockHeader(blendfile, block_header_struct):
if bhead.code == b'ENDB':
break
remaining_bytes = bhead.size
if bhead.code == b'REND':
rend_block_struct = get_render_info_structure(endian_str, bhead.size)
start_frame, end_frame, scene_name = rend_block_struct.unpack(blendfile.read(rend_block_struct.size))
remaining_bytes -= rend_block_struct.size
scene_name = scene_name[:scene_name.index(b'\0')]
# It's possible old blend files are not UTF8 compliant, use `surrogateescape`.
scene_name = scene_name.decode("utf8", errors="surrogateescape")
scenes.append((start_frame, end_frame, scene_name))
blendfile.seek(remaining_bytes, SEEK_CUR)
return scenes
def read_blend_rend_chunk(filepath):
with RawBlendFileReader(filepath) as blendfile:
return _read_blend_rend_chunk_from_file(blendfile, filepath)
def main():
import sys
for filepath in sys.argv[1:]:
for value in read_blend_rend_chunk(filepath):
print("{:d} {:d} {:s}".format(*value))
if __name__ == '__main__':
main()