blender/scripts/modules/blend_render_info.py

148 lines
4.4 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2010-2023 Blender Foundation
#
# SPDX-License-Identifier: GPL-2.0-or-later
# This module can get render info without running from inside blender.
#
# This struct won't change according to Ton.
# Note that the size differs on 32/64bit
#
# typedef struct BHead {
# int code, len;
# void *old;
# int SDNAnr, nr;
# } BHead;
__all__ = (
"read_blend_rend_chunk",
)
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 _read_blend_rend_chunk_from_file(blendfile, filepath):
import struct
import sys
from os import SEEK_CUR
head = blendfile.read(7)
if head != b'BLENDER':
sys.stderr.write("Not a blend file: %s\n" % filepath)
return []
is_64_bit = (blendfile.read(1) == b'-')
# true for PPC, false for X86
is_big_endian = (blendfile.read(1) == b'V')
# Now read the bhead chunk!
blendfile.seek(3, SEEK_CUR) # Skip the version.
scenes = []
sizeof_bhead = 24 if is_64_bit else 20
# Should always be 4, but a malformed/corrupt file may be less.
while (bhead_id := blendfile.read(4)) != b'ENDB':
if len(bhead_id) != 4:
sys.stderr.write("Unable to read until ENDB block (corrupt file): %s\n" % filepath)
break
sizeof_data_left = struct.unpack('>i' if is_big_endian else '<i', blendfile.read(4))[0]
if sizeof_data_left < 0:
# Very unlikely, but prevent other errors.
sys.stderr.write("Negative block size found (corrupt file): %s\n" % filepath)
break
# 4 from the `head_id`, another 4 for the size of the BHEAD.
sizeof_bhead_left = sizeof_bhead - 8
# The remainder of the BHEAD struct is not used.
blendfile.seek(sizeof_bhead_left, SEEK_CUR)
if bhead_id == b'REND':
# Now we want the scene name, start and end frame. this is 32bits long.
start_frame, end_frame = struct.unpack('>2i' if is_big_endian else '<2i', blendfile.read(8))
sizeof_data_left -= 8
scene_name = blendfile.read(64)
sizeof_data_left -= 64
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))
if sizeof_data_left > 0:
blendfile.seek(sizeof_data_left, SEEK_CUR)
elif sizeof_data_left < 0:
# Very unlikely, but prevent attempting to further parse corrupt data.
sys.stderr.write("Error calculating next block (corrupt file): %s\n" % filepath)
break
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" % value)
if __name__ == '__main__':
main()