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
169 lines
6.1 KiB
Python
169 lines
6.1 KiB
Python
# SPDX-FileCopyrightText: 2025 Blender Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
import blendfile_header
|
|
import blend_render_info
|
|
import bpy
|
|
import pathlib
|
|
import sys
|
|
import unittest
|
|
import gzip
|
|
import tempfile
|
|
|
|
args = None
|
|
|
|
|
|
class BlendFileHeaderTest(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.testdir = args.testdir
|
|
|
|
def setUp(self):
|
|
self.assertTrue(self.testdir.exists(),
|
|
"Test dir {0} should exist".format(self.testdir))
|
|
|
|
def test_small_bhead_8(self):
|
|
path = self.testdir / "SmallBHead8.blend"
|
|
with gzip.open(path, "rb") as f:
|
|
header = blendfile_header.BlendFileHeader(f)
|
|
self.assertEqual(header.magic, b"BLENDER")
|
|
self.assertEqual(header.file_format_version, 0)
|
|
self.assertEqual(header.pointer_size, 8)
|
|
self.assertTrue(header.is_little_endian)
|
|
self.assertEqual(header.version, 300)
|
|
|
|
header_struct = header.create_block_header_struct()
|
|
self.assertIs(header_struct.type, blendfile_header.SmallBHead8)
|
|
|
|
buffer = f.read(header_struct.struct.size)
|
|
block = header_struct.parse(buffer)
|
|
self.assertEqual(block.code, b"REND")
|
|
self.assertEqual(block.len, 72)
|
|
self.assertEqual(block.old, 140732920740000)
|
|
self.assertEqual(block.SDNAnr, 0)
|
|
self.assertEqual(block.nr, 1)
|
|
|
|
self.assertEqual(blend_render_info.read_blend_rend_chunk(path), [(1, 250, "Scene")])
|
|
|
|
def test_large_bhead_8(self):
|
|
path = self.testdir / "LargeBHead8.blend"
|
|
with open(path, "rb") as f:
|
|
header = blendfile_header.BlendFileHeader(f)
|
|
self.assertEqual(header.magic, b"BLENDER")
|
|
self.assertEqual(header.file_format_version, 1)
|
|
self.assertEqual(header.pointer_size, 8)
|
|
self.assertTrue(header.is_little_endian)
|
|
self.assertEqual(header.version, 500)
|
|
|
|
header_struct = header.create_block_header_struct()
|
|
self.assertIs(header_struct.type, blendfile_header.LargeBHead8)
|
|
|
|
buffer = f.read(header_struct.struct.size)
|
|
block = header_struct.parse(buffer)
|
|
self.assertEqual(block.code, b"REND")
|
|
self.assertEqual(block.len, 72)
|
|
self.assertEqual(block.old, 140737488337232)
|
|
self.assertEqual(block.SDNAnr, 0)
|
|
self.assertEqual(block.nr, 1)
|
|
|
|
self.assertEqual(blend_render_info.read_blend_rend_chunk(path), [(1, 250, "Scene")])
|
|
|
|
def test_bhead_4(self):
|
|
path = self.testdir / "BHead4.blend"
|
|
with gzip.open(path, "rb") as f:
|
|
header = blendfile_header.BlendFileHeader(f)
|
|
self.assertEqual(header.magic, b"BLENDER")
|
|
self.assertEqual(header.file_format_version, 0)
|
|
self.assertEqual(header.pointer_size, 4)
|
|
self.assertTrue(header.is_little_endian)
|
|
self.assertEqual(header.version, 260)
|
|
|
|
header_struct = header.create_block_header_struct()
|
|
self.assertIs(header_struct.type, blendfile_header.BHead4)
|
|
|
|
buffer = f.read(header_struct.struct.size)
|
|
block = header_struct.parse(buffer)
|
|
self.assertEqual(block.code, b"REND")
|
|
self.assertEqual(block.len, 32)
|
|
self.assertEqual(block.old, 2684488)
|
|
self.assertEqual(block.SDNAnr, 0)
|
|
self.assertEqual(block.nr, 1)
|
|
|
|
self.assertEqual(blend_render_info.read_blend_rend_chunk(path), [(1, 250, "Space types")])
|
|
|
|
def test_bhead_4_big_endian(self):
|
|
path = self.testdir / "BHead4_big_endian.blend"
|
|
with gzip.open(path, "rb") as f:
|
|
header = blendfile_header.BlendFileHeader(f)
|
|
self.assertEqual(header.magic, b"BLENDER")
|
|
self.assertEqual(header.file_format_version, 0)
|
|
self.assertEqual(header.pointer_size, 4)
|
|
self.assertFalse(header.is_little_endian)
|
|
self.assertEqual(header.version, 170)
|
|
|
|
header_struct = header.create_block_header_struct()
|
|
self.assertIs(header_struct.type, blendfile_header.BHead4)
|
|
|
|
buffer = f.read(header_struct.struct.size)
|
|
block = header_struct.parse(buffer)
|
|
self.assertEqual(block.code, b"REND")
|
|
self.assertEqual(block.len, 32)
|
|
self.assertEqual(block.old, 2147428916)
|
|
self.assertEqual(block.SDNAnr, 0)
|
|
self.assertEqual(block.nr, 1)
|
|
|
|
self.assertEqual(blend_render_info.read_blend_rend_chunk(path), [(1, 150, "1")])
|
|
|
|
def test_current(self):
|
|
directory = tempfile.mkdtemp()
|
|
path = pathlib.Path(directory) / "test.blend"
|
|
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
|
|
scene = bpy.data.scenes[0]
|
|
scene.name = "Test Scene"
|
|
scene.frame_start = 10
|
|
scene.frame_end = 20
|
|
bpy.ops.wm.save_as_mainfile(filepath=str(path), compress=False, copy=True)
|
|
|
|
version = bpy.app.version
|
|
version_int = version[0] * 100 + version[1]
|
|
|
|
with open(path, "rb") as f:
|
|
header = blendfile_header.BlendFileHeader(f)
|
|
self.assertEqual(header.magic, b"BLENDER")
|
|
self.assertEqual(header.file_format_version, 1)
|
|
self.assertEqual(header.pointer_size, 8)
|
|
self.assertTrue(header.is_little_endian)
|
|
self.assertEqual(header.version, version_int)
|
|
|
|
header_struct = header.create_block_header_struct()
|
|
self.assertIs(header_struct.type, blendfile_header.LargeBHead8)
|
|
|
|
buffer = f.read(header_struct.struct.size)
|
|
block = header_struct.parse(buffer)
|
|
self.assertEqual(block.code, b"REND")
|
|
|
|
self.assertEqual(blend_render_info.read_blend_rend_chunk(path), [(10, 20, "Test Scene")])
|
|
|
|
|
|
def main():
|
|
global args
|
|
import argparse
|
|
|
|
if '--' in sys.argv:
|
|
argv = [sys.argv[0]] + sys.argv[sys.argv.index('--') + 1:]
|
|
else:
|
|
argv = sys.argv
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('--testdir', required=True, type=pathlib.Path)
|
|
args, remaining = parser.parse_known_args(argv)
|
|
|
|
unittest.main(argv=remaining)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|