@deepblend/dsh-blender-provider-local 0.2.20 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/lib/index.js +50 -35
  2. package/package.json +2 -2
  3. package/python/bootstrap.py +4 -2
  4. package/python/deepblend_anisotropy.py +110 -0
  5. package/python/deepblend_asset_bundle.py +193 -0
  6. package/python/deepblend_avif_headers.py +222 -0
  7. package/python/deepblend_bridge.py +46 -4
  8. package/python/deepblend_capabilities.py +7 -0
  9. package/python/deepblend_frames.py +11 -1
  10. package/python/deepblend_geometry.py +210 -0
  11. package/python/deepblend_gltf_images.py +183 -0
  12. package/python/deepblend_gltf_import.py +95 -0
  13. package/python/deepblend_gltf_roles.py +56 -0
  14. package/python/deepblend_image_headers.py +294 -0
  15. package/python/deepblend_image_roles.py +2 -0
  16. package/python/deepblend_images.py +203 -0
  17. package/python/deepblend_imported_image_headers.py +364 -0
  18. package/python/deepblend_mesh_checks.py +145 -0
  19. package/python/deepblend_obj_images.py +54 -0
  20. package/python/deepblend_obj_import.py +58 -0
  21. package/python/deepblend_obj_resources.py +79 -0
  22. package/python/deepblend_parts.py +106 -0
  23. package/python/deepblend_render.py +53 -0
  24. package/python/deepblend_scene.py +307 -49
  25. package/python/deepblend_vessel.py +192 -0
  26. package/python/deepblend_vessel_math.py +116 -0
  27. package/python/deepblend_vessel_parameters.py +65 -0
  28. package/python/deepblend_views.py +18 -19
  29. package/python/__pycache__/bootstrap.cpython-313.pyc +0 -0
  30. package/python/__pycache__/bootstrap.cpython-314.pyc +0 -0
  31. package/python/__pycache__/deepblend_bridge.cpython-313.pyc +0 -0
  32. package/python/__pycache__/deepblend_bridge.cpython-314.pyc +0 -0
  33. package/python/__pycache__/deepblend_capabilities.cpython-313.pyc +0 -0
  34. package/python/__pycache__/deepblend_frames.cpython-313.pyc +0 -0
  35. package/python/__pycache__/deepblend_frames.cpython-314.pyc +0 -0
  36. package/python/__pycache__/deepblend_render.cpython-313.pyc +0 -0
  37. package/python/__pycache__/deepblend_render.cpython-314.pyc +0 -0
  38. package/python/__pycache__/deepblend_scene.cpython-313.pyc +0 -0
  39. package/python/__pycache__/deepblend_scene.cpython-314.pyc +0 -0
  40. package/python/__pycache__/deepblend_util.cpython-313.pyc +0 -0
  41. package/python/__pycache__/deepblend_util.cpython-314.pyc +0 -0
  42. package/python/__pycache__/deepblend_validate.cpython-313.pyc +0 -0
  43. package/python/__pycache__/deepblend_views.cpython-313.pyc +0 -0
@@ -0,0 +1,203 @@
1
+ """Image PBR maps with explicit data/color handling and packed checkpoints."""
2
+ import os
3
+ import bpy
4
+ from deepblend_util import ActionError
5
+ from deepblend_image_headers import check_image_header, MAX_IMAGE_DECODE_BYTES
6
+ from deepblend_gltf_images import inspect_gltf_images
7
+ from deepblend_obj_images import inspect_obj_images
8
+ from deepblend_image_roles import SOURCE_SPACE
9
+
10
+ SOCKETS = {'baseColor': 'Base Color', 'roughness': 'Roughness', 'metallic': 'Metallic',
11
+ 'normal': 'Normal', 'alpha': 'Alpha', 'emissionColor': 'Emission Color'}
12
+
13
+
14
+ def image_asset_path(asset, project_root):
15
+ root = os.path.realpath(project_root or '.')
16
+ path = os.path.realpath(os.path.join(root, asset['path']))
17
+ if os.path.commonpath([root, path]) != root:
18
+ raise ActionError('PATH_OUTSIDE_WORKSPACE', 'image asset escapes the project directory')
19
+ return path
20
+
21
+
22
+ def check_scene_image_budget(spec, project_root):
23
+ """Count declared image datablocks before scene reset or native decode.
24
+
25
+ Color and data bindings create separate datablocks even for one source; a
26
+ cache of header facts must not collapse their allocation costs. glTF/GLB/OBJ
27
+ image declarations are conservatively counted per imported instance.
28
+ """
29
+ assets = {asset['id']: asset for asset in spec.get('assets') or []}
30
+ bindings = []
31
+ environment = (spec.get('world') or {}).get('environment')
32
+ if environment:
33
+ bindings.append((environment, ('png', 'jpg', 'jpeg', 'hdr', 'exr')))
34
+ for material in spec.get('materials') or []:
35
+ for binding in (material.get('images') or {}).values():
36
+ bindings.append((binding, ('png', 'jpg', 'jpeg')))
37
+ total, facts = 0, {}
38
+ for binding, formats in bindings:
39
+ asset = assets.get(binding['assetId'])
40
+ if asset is None or asset.get('type') not in formats:
41
+ raise ActionError('ASSET_FORMAT_UNAVAILABLE', 'image binding requires a declared image asset')
42
+ path = image_asset_path(asset, project_root)
43
+ if path not in facts:
44
+ facts[path] = check_image_header(path)
45
+ total += facts[path]['decodedBytes']
46
+ if total > MAX_IMAGE_DECODE_BYTES:
47
+ raise ActionError('ASSET_CONTENT_MISMATCH', 'declared scene images exceed the 1 GiB decoded pixel budget')
48
+ imported_images, imported_facts = 0, {}
49
+ for entity in spec.get('entities') or []:
50
+ if entity.get('type') != 'asset-instance':
51
+ continue
52
+ asset = assets.get(entity.get('assetId'))
53
+ if asset is None or asset.get('type') not in ('gltf', 'glb', 'obj'):
54
+ continue
55
+ key = (asset['path'], asset['type'])
56
+ if key not in imported_facts:
57
+ inspect = inspect_obj_images if asset['type'] == 'obj' else inspect_gltf_images
58
+ imported_facts[key] = inspect(project_root, asset)
59
+ total += imported_facts[key]['decodedBytes']
60
+ imported_images += imported_facts[key]['images']
61
+ if total > MAX_IMAGE_DECODE_BYTES:
62
+ raise ActionError('ASSET_CONTENT_MISMATCH', 'declared and imported scene images exceed the 1 GiB decoded pixel budget')
63
+ return {'bindings': len(bindings), 'importedImages': imported_images, 'decodedBytes': total}
64
+
65
+
66
+ def pack_imported_material_images(objects):
67
+ """Validate and embed authored image nodes after the native import."""
68
+ trees, visited, images = [], set(), set()
69
+ for obj in objects:
70
+ for slot in getattr(obj, 'material_slots', []):
71
+ material = slot.material
72
+ if material and material.use_nodes and material.node_tree:
73
+ trees.append(material.node_tree)
74
+ while trees:
75
+ tree = trees.pop()
76
+ if tree in visited:
77
+ continue
78
+ visited.add(tree)
79
+ for node in tree.nodes:
80
+ if node.type == 'GROUP' and node.node_tree:
81
+ trees.append(node.node_tree)
82
+ image = getattr(node, 'image', None)
83
+ if image is not None:
84
+ images.add(image)
85
+ for image in images:
86
+ try:
87
+ if min(image.size) <= 0 or max(image.size) > 8192:
88
+ raise ValueError('image dimensions must be between 1 and 8192 pixels')
89
+ if not image.packed_file:
90
+ image.pack()
91
+ if not image.packed_file:
92
+ raise ValueError('Blender did not embed the image')
93
+ except Exception as error:
94
+ raise ActionError('ASSET_CONTENT_MISMATCH', 'cannot embed imported material image: %s' % error)
95
+
96
+
97
+ def validate_image_uv_usage(spec, obj, layers):
98
+ """Validate named/default UV on a surface that actually uses this material."""
99
+ for binding in (spec.get('images') or {}).values():
100
+ uv_name = binding.get('uvMap')
101
+ if layers is None or not layers or (uv_name and layers.get(uv_name) is None):
102
+ raise ActionError('SCENE_VALIDATION_FAILED', 'image material requires an existing UV map on the used surface',
103
+ {'entityId': obj.get('deepblend_id'), 'partId': obj.get('deepblend_part_id'), 'uvMap': uv_name})
104
+
105
+
106
+ def validate_procedural_uv_usage(spec, obj, layers):
107
+ """Reject implicit UV fallback on an evaluated surface using this material."""
108
+ texture = spec.get('texture') or {}
109
+ if texture.get('coordinates') != 'uv':
110
+ return
111
+ name = texture.get('uvMap')
112
+ valid = layers is not None and bool(layers) and (
113
+ layers.get(name) is not None if name else any(layer.active_render for layer in layers))
114
+ if not valid:
115
+ raise ActionError('SCENE_VALIDATION_FAILED', 'procedural texture requires an existing UV map on the used surface',
116
+ {'materialId': spec['id'], 'entityId': obj.get('deepblend_id'),
117
+ 'partId': obj.get('deepblend_part_id'), 'uvMap': name})
118
+
119
+
120
+ def load_packed_image(asset, project_root, colorspace=None):
121
+ path = image_asset_path(asset, project_root)
122
+ facts = check_image_header(path)
123
+ image = None
124
+ try:
125
+ image = bpy.data.images.load(path, check_existing=False)
126
+ if min(image.size) <= 0 or max(image.size) > 8192:
127
+ raise ValueError('image dimensions must be between 1 and 8192 pixels')
128
+ if list(image.size) not in [[part['width'], part['height']] for part in facts['parts']]:
129
+ raise ValueError('decoded image dimensions disagree with the inspected header')
130
+ if colorspace is not None:
131
+ image[SOURCE_SPACE] = image.colorspace_settings.name
132
+ image.colorspace_settings.name = colorspace
133
+ image.pack()
134
+ return image
135
+ except Exception as error:
136
+ if image is not None:
137
+ bpy.data.images.remove(image)
138
+ raise ActionError('ASSET_CONTENT_MISMATCH', 'cannot load image asset: %s' % error)
139
+
140
+
141
+ def build_environment(world, background, environment, assets, project_root):
142
+ asset = assets.get(environment['assetId'])
143
+ if asset is None or asset.get('type') not in ('png', 'jpg', 'jpeg', 'hdr', 'exr'):
144
+ raise ActionError('ASSET_FORMAT_UNAVAILABLE', 'environment requires a declared image asset')
145
+ # Declare interpretation rather than inherit loader defaults: those can be
146
+ # sRGB even for float images. This contract expects linear Rec.709 HDR/EXR.
147
+ image = load_packed_image(asset, project_root,
148
+ 'sRGB' if asset['type'] in ('png', 'jpg', 'jpeg') else 'Linear Rec.709')
149
+ nodes, links = world.node_tree.nodes, world.node_tree.links
150
+ texture = nodes.new('ShaderNodeTexEnvironment')
151
+ texture.name = 'db_environment'
152
+ texture.image = image
153
+ texture.projection = 'EQUIRECTANGULAR'
154
+ coordinates = nodes.new('ShaderNodeTexCoord')
155
+ mapping = nodes.new('ShaderNodeMapping')
156
+ mapping.inputs['Rotation'].default_value[2] = environment.get('rotation', 0)
157
+ links.new(coordinates.outputs['Generated'], mapping.inputs['Vector'])
158
+ links.new(mapping.outputs['Vector'], texture.inputs['Vector'])
159
+ links.new(texture.outputs['Color'], background.inputs['Color'])
160
+ return {'width': int(image.size[0]), 'height': int(image.size[1]),
161
+ 'channels': int(image.channels), 'isFloat': bool(image.is_float),
162
+ 'colorSpace': image.colorspace_settings.name}
163
+
164
+
165
+ def build_image_maps(material, principled, bindings, assets, project_root):
166
+ nodes, links = material.node_tree.nodes, material.node_tree.links
167
+ for channel, binding in bindings.items():
168
+ asset = assets.get(binding['assetId'])
169
+ if asset is None or asset.get('type') not in ('png', 'jpg', 'jpeg'):
170
+ raise ActionError('ASSET_FORMAT_UNAVAILABLE', 'PBR maps require declared PNG or JPEG assets')
171
+ # Independent datablocks allow the same source to serve color and data.
172
+ image = load_packed_image(asset, project_root,
173
+ 'sRGB' if channel in ('baseColor', 'emissionColor') else 'Non-Color')
174
+ texture = nodes.new('ShaderNodeTexImage')
175
+ texture.name = 'db_image_' + channel
176
+ texture.image = image
177
+ texture.extension = 'REPEAT'
178
+ texture.interpolation = 'Linear'
179
+ uv = nodes.new('ShaderNodeUVMap')
180
+ uv.uv_map = binding.get('uvMap', '')
181
+ mapping = nodes.new('ShaderNodeMapping')
182
+ mapping.inputs['Scale'].default_value = binding.get('scale', [1, 1, 1])
183
+ mapping.inputs['Location'].default_value = binding.get('offset', [0, 0, 0])
184
+ links.new(uv.outputs['UV'], mapping.inputs['Vector'])
185
+ links.new(mapping.outputs['Vector'], texture.inputs['Vector'])
186
+ source = texture.outputs['Color']
187
+ if channel == 'normal':
188
+ normal = nodes.new('ShaderNodeNormalMap')
189
+ normal.space = 'TANGENT'
190
+ normal.uv_map = binding.get('uvMap', '')
191
+ normal.inputs['Strength'].default_value = binding.get('strength', 1)
192
+ links.new(source, normal.inputs['Color'])
193
+ source = normal.outputs['Normal']
194
+ elif channel in ('roughness', 'metallic', 'alpha'):
195
+ selected = binding.get('channel', 'r')
196
+ if selected == 'a':
197
+ source = texture.outputs['Alpha']
198
+ else:
199
+ separate = nodes.new('ShaderNodeSeparateColor')
200
+ separate.mode = 'RGB'
201
+ links.new(source, separate.inputs['Color'])
202
+ source = separate.outputs[{'r': 'Red', 'g': 'Green', 'b': 'Blue'}[selected]]
203
+ links.new(source, principled.inputs[SOCKETS[channel]])
@@ -0,0 +1,364 @@
1
+ """Bounded metadata for the additional raster formats used by native OBJ import.
2
+
3
+ PNG/JPEG/HDR/EXR/WebP retain the existing stream inspector. TIFF and container
4
+ formats seek to metadata rather than copying encoded pixels. This estimates
5
+ decoded image buffers; it does not bound the native decoder's entire peak RSS.
6
+ """
7
+ import os
8
+ import struct
9
+
10
+ from deepblend_image_headers import (inspect_image_header, dimensions,
11
+ MAX_IMAGE_HEADER_BYTES, MAX_IMAGE_DECODE_BYTES, MAX_IMAGE_CHANNELS,
12
+ MAX_IMAGE_PARTS)
13
+ from deepblend_util import ActionError
14
+ from deepblend_avif_headers import locations, item_info, decoded_item_size
15
+
16
+
17
+ class MetadataReader:
18
+ def __init__(self, source):
19
+ self.source = source
20
+ self.size = os.fstat(source.fileno()).st_size
21
+ self.consumed = 0
22
+
23
+ def read(self, offset, count):
24
+ if offset < 0 or count < 0 or offset + count > self.size:
25
+ raise ValueError('truncated or escaping image metadata')
26
+ if self.consumed + count > MAX_IMAGE_HEADER_BYTES:
27
+ raise ValueError('image header exceeds the 1 MiB metadata budget')
28
+ self.source.seek(offset)
29
+ data = self.source.read(count)
30
+ self.consumed += len(data)
31
+ if len(data) != count:
32
+ raise ValueError('truncated image metadata')
33
+ return data
34
+
35
+
36
+ class MetadataStream:
37
+ def __init__(self, reader):
38
+ self.reader = reader
39
+
40
+ def read(self, count):
41
+ return self.reader.read(self.reader.source.tell(), count)
42
+
43
+
44
+ def channels(value):
45
+ if not 1 <= value <= MAX_IMAGE_CHANNELS:
46
+ raise ValueError('image channel count exceeds its inspection limit')
47
+ return value
48
+
49
+
50
+ def bitmap(reader):
51
+ size = struct.unpack('<I', reader.read(14, 4))[0]
52
+ if size == 12:
53
+ width, height, planes, bits = struct.unpack('<HHHH', reader.read(18, 8))
54
+ elif size >= 40:
55
+ width, height, planes, bits = struct.unpack('<iiHH', reader.read(18, 12))
56
+ height = abs(height)
57
+ else:
58
+ raise ValueError('invalid BMP information header')
59
+ if planes != 1 or bits not in (1, 4, 8, 16, 24, 32):
60
+ raise ValueError('invalid BMP pixel layout')
61
+ return [dimensions(width, height)]
62
+
63
+
64
+ def targa(reader):
65
+ data = reader.read(0, 18)
66
+ color_map, kind = data[1], data[2]
67
+ if color_map not in (0, 1) or kind not in (1, 2, 3, 9, 10, 11):
68
+ raise ValueError('unsupported image header signature')
69
+ if data[16] not in (8, 15, 16, 24, 32):
70
+ raise ValueError('invalid TGA pixel layout')
71
+ width, height = struct.unpack('<HH', data[12:16])
72
+ return [dimensions(width, height)]
73
+
74
+
75
+ def iris(reader):
76
+ storage, depth, axes, width, height, count = struct.unpack('>BBHHHH', reader.read(2, 10))
77
+ if storage not in (0, 1) or depth not in (1, 2) or axes not in (1, 2, 3):
78
+ raise ValueError('invalid SGI image header')
79
+ return [dimensions(width, height, channels(count))]
80
+
81
+
82
+ def dpx(reader, endian):
83
+ orientation, elements, width, height = struct.unpack(endian + 'HHII', reader.read(768, 12))
84
+ if orientation > 7 or not 1 <= elements <= 8:
85
+ raise ValueError('invalid DPX image elements')
86
+ return [dimensions(width, height, max(4, elements * 4))]
87
+
88
+
89
+ def cineon(reader, endian):
90
+ count = reader.read(192, 2)[1]
91
+ if not 1 <= count <= 8:
92
+ raise ValueError('invalid Cineon channel count')
93
+ sizes = [struct.unpack(endian + 'II', reader.read(200 + index * 28, 8)) for index in range(count)]
94
+ for width, height in sizes:
95
+ dimensions(width, height)
96
+ return [dimensions(max(w for w, h in sizes), max(h for w, h in sizes), count)]
97
+
98
+
99
+ def photoshop(reader):
100
+ data = reader.read(4, 22)
101
+ version = struct.unpack('>H', data[:2])[0]
102
+ count, height, width, bits, mode = struct.unpack('>HIIHH', data[8:])
103
+ if version not in (1, 2) or data[2:8] != bytes(6) or bits not in (1, 8, 16, 32) or mode > 9:
104
+ raise ValueError('invalid PSD/PSB image header')
105
+ # Native loading selects the merged image, not every Photoshop layer.
106
+ return [dimensions(width, height, channels(count))]
107
+
108
+
109
+ def tiff(reader, endian):
110
+ version = struct.unpack(endian + 'H', reader.read(2, 2))[0]
111
+ if version == 42:
112
+ offset = struct.unpack(endian + 'I', reader.read(4, 4))[0]
113
+ count = struct.unpack(endian + 'H', reader.read(offset, 2))[0]
114
+ start, stride, inline, pointer_type = offset + 2, 12, 4, 'I'
115
+ elif version == 43:
116
+ size, reserved, offset = struct.unpack(endian + 'HHQ', reader.read(4, 12))
117
+ if size != 8 or reserved:
118
+ raise ValueError('invalid BigTIFF header')
119
+ count = struct.unpack(endian + 'Q', reader.read(offset, 8))[0]
120
+ start, stride, inline, pointer_type = offset + 8, 20, 8, 'Q'
121
+ else:
122
+ raise ValueError('invalid TIFF byte order or version')
123
+ if not offset or count > MAX_IMAGE_HEADER_BYTES // stride:
124
+ raise ValueError('TIFF directory exceeds its metadata budget')
125
+ entries = reader.read(start, count * stride)
126
+ facts = {}
127
+ for index in range(count):
128
+ entry = entries[index * stride:(index + 1) * stride]
129
+ tag, kind = struct.unpack(endian + 'HH', entry[:4])
130
+ if tag not in (256, 257, 277):
131
+ continue
132
+ if tag in facts:
133
+ raise ValueError('duplicate TIFF dimension or channel tag')
134
+ number = struct.unpack(endian + pointer_type, entry[4:stride-inline])[0]
135
+ value_type = {3: 'H', 4: 'I', 16: 'Q'}.get(kind)
136
+ if number != 1 or value_type is None:
137
+ raise ValueError('invalid TIFF dimension or channel tag')
138
+ size = struct.calcsize(value_type)
139
+ value = entry[-inline:]
140
+ if size > inline:
141
+ pointer = struct.unpack(endian + pointer_type, value)[0]
142
+ value = reader.read(pointer, size)
143
+ facts[tag] = struct.unpack(endian + value_type, value[:size])[0]
144
+ if 256 not in facts or 257 not in facts:
145
+ raise ValueError('TIFF requires image width and height')
146
+ # Match native first-image selection; unused later IFDs are not decoded.
147
+ return [dimensions(facts[256], facts[257], channels(facts.get(277, 1)))]
148
+
149
+
150
+ def dds(reader):
151
+ data = reader.read(4, 124)
152
+ values = struct.unpack('<31I', data)
153
+ if values[0] != 124 or values[18] != 32:
154
+ raise ValueError('invalid DDS header sizes')
155
+ height, width, depth, levels = values[2], values[3], max(1, values[5]), max(1, values[6])
156
+ dimensions(width, height)
157
+ if levels > max(width, height, depth).bit_length() or depth > 8192:
158
+ raise ValueError('invalid DDS mipmap or volume bounds')
159
+ faces = max(1, bin(values[27] & 0xfc00).count('1'))
160
+ arrays = 1
161
+ if data[80:84] == b'DX10':
162
+ pixel_format, kind, misc, arrays, flags = struct.unpack('<5I', reader.read(128, 20))
163
+ if not arrays or kind not in (2, 3, 4) or (kind == 4 and arrays != 1):
164
+ raise ValueError('invalid DDS texture array')
165
+ faces = 6 if misc & 4 else 1
166
+ parts = []
167
+ for _ in range(levels):
168
+ part = dimensions(width, height)
169
+ part['decodedBytes'] *= depth * faces * arrays
170
+ part['slices'] = depth * faces * arrays
171
+ parts.append(part)
172
+ width, height, depth = max(1, width // 2), max(1, height // 2), max(1, depth // 2)
173
+ return parts
174
+
175
+
176
+ def boxes(reader, start, end):
177
+ for _ in range(MAX_IMAGE_PARTS):
178
+ if start == end:
179
+ return
180
+ size, kind = struct.unpack('>I4s', reader.read(start, 8))
181
+ header = 8
182
+ if size == 1:
183
+ size = struct.unpack('>Q', reader.read(start + 8, 8))[0]
184
+ header = 16
185
+ if size == 0:
186
+ size = end - start
187
+ if size < header or start + size > end:
188
+ raise ValueError('invalid image container box bounds')
189
+ yield kind, start + header, start + size
190
+ start += size
191
+ raise ValueError('image container exceeds its metadata box limit')
192
+
193
+
194
+ def codestream(reader, start, end):
195
+ if reader.read(start, 4) != b'\xff\x4f\xff\x51':
196
+ raise ValueError('JPEG2000 requires an initial SIZ marker')
197
+ length = struct.unpack('>H', reader.read(start + 4, 2))[0]
198
+ if start + 4 + length > end or length < 38:
199
+ raise ValueError('invalid JPEG2000 SIZ bounds')
200
+ data = reader.read(start + 6, 36)
201
+ width, height, x, y = struct.unpack('>4I', data[2:18])
202
+ count = channels(struct.unpack('>H', data[34:36])[0])
203
+ if length != 38 + count * 3 or width <= x or height <= y:
204
+ raise ValueError('invalid JPEG2000 image extent')
205
+ return [dimensions(width - x, height - y, count)]
206
+
207
+
208
+ def jpeg2000(reader):
209
+ if reader.read(0, 2) == b'\xff\x4f':
210
+ return codestream(reader, 0, reader.size)
211
+ for kind, start, end in boxes(reader, 0, reader.size):
212
+ if kind == b'jp2c':
213
+ return codestream(reader, start, end)
214
+ raise ValueError('JPEG2000 requires an image codestream')
215
+
216
+
217
+ def avif(reader):
218
+ """Resolve primary image properties and its decoded image dependencies."""
219
+ primary, properties, associations, references = None, [], {}, []
220
+ item_locations, item_types, idat = {}, {}, None
221
+ for kind, start, end in boxes(reader, 0, reader.size):
222
+ if kind != b'meta':
223
+ continue
224
+ if reader.read(start, 4) != bytes(4):
225
+ raise ValueError('unsupported image meta version')
226
+ for field, pos, stop in boxes(reader, start + 4, end):
227
+ if field == b'iloc':
228
+ if item_locations:
229
+ raise ValueError('duplicate image item locations')
230
+ item_locations = locations(reader, pos, stop)
231
+ elif field == b'iinf':
232
+ if item_types:
233
+ raise ValueError('duplicate image item definitions')
234
+ item_types = item_info(reader, pos, stop, boxes)
235
+ elif field == b'idat':
236
+ if idat is not None:
237
+ raise ValueError('duplicate inline image data')
238
+ idat = (pos, stop)
239
+ elif field == b'pitm':
240
+ version = reader.read(pos, 4)[0]
241
+ if version not in (0, 1) or primary is not None:
242
+ raise ValueError('invalid primary image identity')
243
+ size, fmt = (2, '>H') if version == 0 else (4, '>I')
244
+ primary = struct.unpack(fmt, reader.read(pos + 4, size))[0]
245
+ elif field == b'iprp':
246
+ for group, p, e in boxes(reader, pos, stop):
247
+ if group == b'ipco':
248
+ if properties:
249
+ raise ValueError('duplicate image property container')
250
+ for prop, a, b in boxes(reader, p, e):
251
+ value = None
252
+ if prop == b'ispe':
253
+ if b - a != 12 or reader.read(a, 4) != bytes(4):
254
+ raise ValueError('invalid image spatial extent')
255
+ value = struct.unpack('>II', reader.read(a + 4, 8))
256
+ properties.append(value)
257
+ elif group == b'ipma':
258
+ header = reader.read(p, 8)
259
+ version, flags = header[0], int.from_bytes(header[1:4], 'big')
260
+ if version not in (0, 1) or flags & ~1:
261
+ raise ValueError('unsupported image property association')
262
+ count = struct.unpack('>I', header[4:])[0]
263
+ if count > MAX_IMAGE_PARTS:
264
+ raise ValueError('too many image property associations')
265
+ offset = p + 8
266
+ for _ in range(count):
267
+ size, fmt = (2, '>H') if version == 0 else (4, '>I')
268
+ item = struct.unpack(fmt, reader.read(offset, size))[0]
269
+ number = reader.read(offset + size, 1)[0]
270
+ offset += size + 1
271
+ indices = associations.setdefault(item, [])
272
+ for _ in range(number):
273
+ size = 2 if flags & 1 else 1
274
+ value = int.from_bytes(reader.read(offset, size), 'big') & (0x7fff if size == 2 else 0x7f)
275
+ indices.append(value)
276
+ offset += size
277
+ if offset > e:
278
+ raise ValueError('image associations escape their box')
279
+ if offset != e:
280
+ raise ValueError('invalid image association length')
281
+ elif field == b'iref':
282
+ version = reader.read(pos, 4)[0]
283
+ if version not in (0, 1):
284
+ raise ValueError('unsupported image reference version')
285
+ size, fmt = (2, '>H') if version == 0 else (4, '>I')
286
+ for relation, a, b in boxes(reader, pos + 4, stop):
287
+ if relation not in (b'dimg', b'auxl'):
288
+ continue
289
+ item = struct.unpack(fmt, reader.read(a, size))[0]
290
+ count = struct.unpack('>H', reader.read(a + size, 2))[0]
291
+ if count > MAX_IMAGE_PARTS or a + size + 2 + count * size != b:
292
+ raise ValueError('invalid image dependency references')
293
+ targets = [struct.unpack(fmt, reader.read(a + size + 2 + i * size, size))[0] for i in range(count)]
294
+ references.append((relation, item, targets))
295
+ if primary is None:
296
+ raise ValueError('image container requires a primary image')
297
+ selected = {primary}
298
+ for _ in range(MAX_IMAGE_PARTS):
299
+ previous = len(selected)
300
+ for relation, item, targets in references:
301
+ if relation == b'dimg' and item in selected:
302
+ selected.update(targets)
303
+ elif relation == b'auxl' and any(target in selected for target in targets):
304
+ selected.add(item)
305
+ if len(selected) > MAX_IMAGE_PARTS:
306
+ raise ValueError('image dependency graph exceeds its inspection limit')
307
+ if len(selected) == previous:
308
+ break
309
+ parts = []
310
+ for item in sorted(selected):
311
+ extents = set()
312
+ for index in associations.get(item, []):
313
+ if not index:
314
+ continue
315
+ if index > len(properties):
316
+ raise ValueError('image property index is missing')
317
+ if properties[index - 1] is not None:
318
+ extents.add(properties[index - 1])
319
+ if len(extents) != 1:
320
+ raise ValueError('decoded image requires one unambiguous spatial extent')
321
+ dependencies = [target for relation, origin, targets in references
322
+ if relation == b'dimg' and origin == item for target in targets]
323
+ parts.append(decoded_item_size(reader, item, item_types.get(item),
324
+ item_locations.get(item), idat, next(iter(extents)), dependencies))
325
+ return parts
326
+
327
+
328
+ def check_imported_image_header(path):
329
+ try:
330
+ with open(path, 'rb') as source:
331
+ reader = MetadataReader(source)
332
+ prefix = reader.read(0, min(16, reader.size))
333
+ if prefix.startswith((b'\x89PNG', b'\xff\xd8', b'#?', b'v/1\x01', b'RIFF')):
334
+ source.seek(0)
335
+ facts = inspect_image_header(MetadataStream(reader))
336
+ facts['headerBytes'] = reader.consumed
337
+ return facts
338
+ if prefix.startswith(b'BM'):
339
+ format_name, parts = 'bmp', bitmap(reader)
340
+ elif prefix.startswith(b'\x01\xda'):
341
+ format_name, parts = 'sgi', iris(reader)
342
+ elif prefix[:4] in (b'SDPX', b'XPDS'):
343
+ format_name, parts = 'dpx', dpx(reader, '>' if prefix[:4] == b'SDPX' else '<')
344
+ elif prefix[:4] in (b'\x80\x2a\x5f\xd7', b'\xd7\x5f\x2a\x80'):
345
+ format_name, parts = 'cineon', cineon(reader, '>' if prefix[0] == 0x80 else '<')
346
+ elif prefix.startswith(b'8BPS'):
347
+ format_name, parts = 'psd', photoshop(reader)
348
+ elif prefix[:2] in (b'II', b'MM'):
349
+ format_name, parts = 'tiff', tiff(reader, '<' if prefix[:2] == b'II' else '>')
350
+ elif prefix.startswith(b'DDS '):
351
+ format_name, parts = 'dds', dds(reader)
352
+ elif prefix.startswith(b'\xff\x4f') or prefix[:12] == b'\x00\x00\x00\x0cjP \r\n\x87\n':
353
+ format_name, parts = 'jpeg2000', jpeg2000(reader)
354
+ elif prefix[4:8] == b'ftyp':
355
+ format_name, parts = 'avif', avif(reader)
356
+ else:
357
+ format_name, parts = 'tga', targa(reader)
358
+ decoded = sum(part['decodedBytes'] for part in parts)
359
+ if decoded > MAX_IMAGE_DECODE_BYTES:
360
+ raise ValueError('image exceeds the 1 GiB decoded pixel budget')
361
+ return {'format': format_name, 'parts': parts, 'headerBytes': reader.consumed,
362
+ 'decodedBytes': decoded}
363
+ except (OSError, ValueError, struct.error) as error:
364
+ raise ActionError('ASSET_CONTENT_MISMATCH', 'cannot inspect imported image before decoding: %s' % error)