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
This commit is contained in:
@@ -340,6 +340,12 @@ if(TEST_SRC_DIR_EXISTS)
|
||||
--output-dir ${TEST_OUT_DIR}/blendfile_io/
|
||||
--test-dir "${TEST_SRC_DIR}/libraries_and_linking"
|
||||
)
|
||||
|
||||
add_blender_test(
|
||||
blendfile_header
|
||||
--python ${CMAKE_CURRENT_LIST_DIR}/bl_blendfile_header.py --
|
||||
--testdir "${TEST_SRC_DIR}/io_tests/blend_parsing"
|
||||
)
|
||||
endif()
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
168
tests/python/bl_blendfile_header.py
Normal file
168
tests/python/bl_blendfile_header.py
Normal file
@@ -0,0 +1,168 @@
|
||||
# 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()
|
||||
@@ -272,6 +272,12 @@ class TestBlendFileOpenLinkSaveAllTestFiles(TestHelper):
|
||||
(OSError, RuntimeError),
|
||||
"created by a Big Endian version of Blender, support for these files has been removed in Blender 5.0"
|
||||
),
|
||||
# io_tests/blend_parsing/BHead4_big_endian.blend
|
||||
# File generated from a big endian build of Blender.
|
||||
"BHead4_big_endian.blend": (
|
||||
(OSError, RuntimeError),
|
||||
"created by a Big Endian version of Blender, support for these files has been removed in Blender 5.0"
|
||||
),
|
||||
}
|
||||
|
||||
assert all(p.endswith("/") for p in self.excluded_open_link_dirs)
|
||||
|
||||
Reference in New Issue
Block a user