更新 HEIC 文件处理逻辑,修复 ID 映射并优化属性关联处理
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+12
-5
@@ -163,7 +163,10 @@ class HEICFile:
|
||||
return
|
||||
|
||||
# 4a. 找到要删除的属性索引 (1-based)
|
||||
props_to_remove = set(ipma.entries[item_id_to_remove].associations)
|
||||
props_to_remove = {
|
||||
assoc.property_index
|
||||
for assoc in ipma.entries[item_id_to_remove].associations
|
||||
}
|
||||
|
||||
# 4b. 从 ipma 中删除该 Item
|
||||
del ipma.entries[item_id_to_remove]
|
||||
@@ -173,7 +176,9 @@ class HEICFile:
|
||||
# (即,它们在 props_to_remove 中,但*不在*任何*剩余*的 item 关联中)
|
||||
all_remaining_props = set()
|
||||
for entry in ipma.entries.values():
|
||||
all_remaining_props.update(entry.associations)
|
||||
all_remaining_props.update(
|
||||
assoc.property_index for assoc in entry.associations
|
||||
)
|
||||
|
||||
orphaned_props = props_to_remove - all_remaining_props
|
||||
|
||||
@@ -218,9 +223,11 @@ class HEICFile:
|
||||
# 4g. 应用重映射
|
||||
for item_id, entry in ipma.entries.items():
|
||||
new_associations = []
|
||||
for old_prop_index in entry.associations:
|
||||
for assoc in entry.associations:
|
||||
old_prop_index = assoc.property_index
|
||||
if old_prop_index in remap_table:
|
||||
new_associations.append(remap_table[old_prop_index])
|
||||
assoc.property_index = remap_table[old_prop_index]
|
||||
new_associations.append(assoc)
|
||||
# else:
|
||||
# 该属性已被删除 (不应发生,因为我们只删除了孤儿)
|
||||
|
||||
@@ -351,7 +358,7 @@ class HEICFile:
|
||||
|
||||
item_associations = self._iprp_box.ipma.entries[target_id].associations
|
||||
for assoc in item_associations:
|
||||
property_index = assoc - 1
|
||||
property_index = assoc.property_index - 1
|
||||
if 0 <= property_index < len(self._iprp_box.ipco.children):
|
||||
prop = self._iprp_box.ipco.children[property_index]
|
||||
if isinstance(prop, ImageSpatialExtentsBox):
|
||||
|
||||
+97
-37
@@ -30,6 +30,11 @@ def _write_int(value: int, size: int) -> bytes:
|
||||
class ItemLocation:
|
||||
def __init__(self, item_id):
|
||||
self.item_id = item_id
|
||||
self.data_reference_index = 0
|
||||
self.base_offset = 0
|
||||
self.construction_method = 0
|
||||
self.extent_indices = []
|
||||
self.raw_extents = [] # (relative_offset, length)
|
||||
# extents 存储 (absolute_offset, length)
|
||||
self.extents = []
|
||||
|
||||
@@ -81,9 +86,11 @@ class ItemLocationBox(FullBox):
|
||||
loc = ItemLocation(item_id)
|
||||
|
||||
if (self.version == 1 or self.version == 2) and current_pos + 2 <= len(stream):
|
||||
loc.construction_method = struct.unpack('>H', stream[current_pos:current_pos+2])[0]
|
||||
current_pos += 2
|
||||
|
||||
if current_pos + 2 > len(stream): break
|
||||
loc.data_reference_index = struct.unpack('>H', stream[current_pos:current_pos+2])[0]
|
||||
current_pos += 2
|
||||
|
||||
base_offset = 0
|
||||
@@ -91,6 +98,7 @@ class ItemLocationBox(FullBox):
|
||||
if current_pos + self.base_offset_size > len(stream): break
|
||||
base_offset = _read_int(stream, current_pos, self.base_offset_size)
|
||||
current_pos += self.base_offset_size
|
||||
loc.base_offset = base_offset
|
||||
|
||||
if current_pos + 2 > len(stream): break
|
||||
extent_count = struct.unpack('>H', stream[current_pos : current_pos+2])[0]
|
||||
@@ -99,6 +107,7 @@ class ItemLocationBox(FullBox):
|
||||
for __ in range(extent_count):
|
||||
if (self.version == 1 or self.version == 2) and self.index_size > 0:
|
||||
if current_pos + self.index_size > len(stream): break
|
||||
loc.extent_indices.append(_read_int(stream, current_pos, self.index_size))
|
||||
current_pos += self.index_size
|
||||
|
||||
if current_pos + self.offset_size > len(stream): break
|
||||
@@ -109,6 +118,7 @@ class ItemLocationBox(FullBox):
|
||||
extent_length = _read_int(stream, current_pos, self.length_size)
|
||||
current_pos += self.length_size
|
||||
|
||||
loc.raw_extents.append((extent_offset, extent_length))
|
||||
loc.extents.append((base_offset + extent_offset, extent_length))
|
||||
self.locations.append(loc)
|
||||
|
||||
@@ -134,59 +144,82 @@ class ItemLocationBox(FullBox):
|
||||
else:
|
||||
content_stream.write(struct.pack('>I', len(self.locations)))
|
||||
|
||||
original_mdat_end_offset = original_mdat_offset + original_mdat_size
|
||||
original_meta_end_offset = original_meta_offset + original_meta_size
|
||||
|
||||
def _adjust_absolute_offset(original_offset: int) -> int:
|
||||
if original_offset == 0:
|
||||
return 0
|
||||
if original_mdat_offset <= original_offset < original_mdat_end_offset:
|
||||
return original_offset + mdat_offset_delta
|
||||
if original_meta_offset <= original_offset < original_meta_end_offset:
|
||||
return original_offset + meta_offset_delta
|
||||
return original_offset
|
||||
|
||||
for loc in self.locations:
|
||||
item_id_size = 2 if self.version < 2 else 4
|
||||
content_stream.write(_write_int(loc.item_id, item_id_size))
|
||||
|
||||
if (self.version == 1 or self.version == 2):
|
||||
content_stream.write(struct.pack('>H', 0))
|
||||
content_stream.write(struct.pack('>H', loc.construction_method & 0xFFFF))
|
||||
|
||||
content_stream.write(struct.pack('>H', 0))
|
||||
content_stream.write(struct.pack('>H', loc.data_reference_index))
|
||||
|
||||
if self.base_offset_size > 0:
|
||||
content_stream.write(_write_int(0, self.base_offset_size))
|
||||
new_base_offset = _adjust_absolute_offset(loc.base_offset)
|
||||
content_stream.write(_write_int(new_base_offset, self.base_offset_size))
|
||||
else:
|
||||
new_base_offset = 0
|
||||
|
||||
content_stream.write(struct.pack('>H', len(loc.extents)))
|
||||
extents_to_process = loc.raw_extents if loc.raw_extents else [
|
||||
(max(absolute_offset - loc.base_offset, 0), length)
|
||||
for absolute_offset, length in loc.extents
|
||||
]
|
||||
content_stream.write(struct.pack('>H', len(extents_to_process)))
|
||||
|
||||
for (original_offset, length) in loc.extents:
|
||||
new_extents_absolute = []
|
||||
new_extents_relative = []
|
||||
new_extent_indices = []
|
||||
|
||||
for idx, (original_relative_offset, length) in enumerate(extents_to_process):
|
||||
if (self.version == 1 or self.version == 2) and self.index_size > 0:
|
||||
content_stream.write(_write_int(0, self.index_size))
|
||||
extent_index = loc.extent_indices[idx] if idx < len(loc.extent_indices) else 0
|
||||
content_stream.write(_write_int(extent_index, self.index_size))
|
||||
new_extent_indices.append(extent_index)
|
||||
|
||||
# !!! 魔法发生的地方 !!!
|
||||
new_absolute_offset = original_offset
|
||||
original_mdat_end_offset = original_mdat_offset + original_mdat_size
|
||||
original_meta_end_offset = original_meta_offset + original_meta_size
|
||||
original_absolute_offset = loc.base_offset + original_relative_offset
|
||||
new_absolute_offset = _adjust_absolute_offset(original_absolute_offset)
|
||||
|
||||
if original_offset == 0:
|
||||
if original_absolute_offset == 0:
|
||||
new_absolute_offset = 0 # 保持 0 偏移量
|
||||
|
||||
# 检查偏移量是否落在 *原始* mdat 盒区域内
|
||||
elif (original_mdat_offset <= original_offset < original_mdat_end_offset):
|
||||
# 这是一个指向 mdat 的偏移量,应用 mdat 增量
|
||||
new_absolute_offset = original_offset + mdat_offset_delta
|
||||
|
||||
# pyheic_struct_副本/heic_types.py
|
||||
|
||||
# 检查偏移量是否落在 *原始* meta 盒区域内
|
||||
elif (original_meta_offset <= original_offset < original_meta_end_offset):
|
||||
# 这是一个指向 meta 内部的偏移量 (例如 EXIF)
|
||||
# (*** 错误修正 ***)
|
||||
# 它只应该被 *meta 盒之前* 的数据增长所平移。
|
||||
# 在我们的 builder.py 逻辑中, 'meta_offset_delta' 正是这个值 (即 ftyp 盒的增长)。
|
||||
new_absolute_offset = original_offset + meta_offset_delta
|
||||
|
||||
# else:
|
||||
# 这是一个指向其他地方 (如 ftyp) 的偏移量,我们假设它不动
|
||||
# new_absolute_offset 保持等于 original_offset
|
||||
|
||||
# 最后的安全检查,防止打包负数
|
||||
if new_absolute_offset < 0:
|
||||
print(f" Warning: Calculated a negative offset ({new_absolute_offset}) for item {loc.item_id}. Setting to 0.")
|
||||
new_absolute_offset = 0
|
||||
|
||||
content_stream.write(_write_int(new_absolute_offset, self.offset_size))
|
||||
if self.base_offset_size > 0:
|
||||
new_relative_offset = new_absolute_offset - new_base_offset
|
||||
else:
|
||||
new_relative_offset = new_absolute_offset
|
||||
|
||||
if new_relative_offset < 0:
|
||||
print(f" Warning: Calculated a negative relative offset ({new_relative_offset}) for item {loc.item_id}. Setting to 0.")
|
||||
new_relative_offset = 0
|
||||
new_absolute_offset = new_base_offset
|
||||
|
||||
content_stream.write(_write_int(new_relative_offset, self.offset_size))
|
||||
content_stream.write(_write_int(length, self.length_size))
|
||||
|
||||
new_extents_absolute.append((new_absolute_offset, length))
|
||||
new_extents_relative.append((new_relative_offset, length))
|
||||
|
||||
loc.base_offset = new_base_offset
|
||||
loc.extents = new_extents_absolute
|
||||
loc.raw_extents = new_extents_relative
|
||||
if new_extent_indices:
|
||||
loc.extent_indices = new_extent_indices
|
||||
|
||||
self.raw_data = content_stream.getvalue()
|
||||
print(" 'iloc' box content successfully rebuilt.")
|
||||
# --- END FIX ---
|
||||
@@ -214,6 +247,7 @@ class ItemInfoEntryBox(FullBox):
|
||||
self.item_name: str = "" # UTF-8 string
|
||||
self.content_type: Optional[str] = None
|
||||
self.content_encoding: Optional[str] = None
|
||||
self._has_protection_field: bool = True
|
||||
super().__init__(size, box_type, offset, raw_data)
|
||||
|
||||
def _post_parse_initialization(self):
|
||||
@@ -250,6 +284,7 @@ class ItemInfoEntryBox(FullBox):
|
||||
pos += 4
|
||||
|
||||
remaining = len(stream) - pos
|
||||
self._has_protection_field = False
|
||||
if remaining >= 6:
|
||||
candidate_protection = struct.unpack('>H', stream[pos:pos+2])[0]
|
||||
candidate_type = stream[pos+2:pos+6]
|
||||
@@ -257,17 +292,20 @@ class ItemInfoEntryBox(FullBox):
|
||||
if candidate_protection <= 0x00FF and b'\x00' not in candidate_type:
|
||||
# 标准顺序: 2 字节保护索引 + 4 字节类型
|
||||
self.item_protection_index = candidate_protection
|
||||
self._has_protection_field = True
|
||||
pos += 2
|
||||
type_bytes = candidate_type
|
||||
pos += 4
|
||||
else:
|
||||
# 三星文件: 直接写入类型 (如 'hvc1'),缺少保护索引
|
||||
self.item_protection_index = 0
|
||||
self._has_protection_field = False
|
||||
type_bytes = stream[pos:pos+4]
|
||||
pos += 4
|
||||
elif remaining >= 4:
|
||||
# 某些三星文件缺少 2 字节的保护索引字段, 直接紧跟 4 字节类型
|
||||
self.item_protection_index = 0
|
||||
self._has_protection_field = False
|
||||
type_bytes = stream[pos:pos+4]
|
||||
pos += 4
|
||||
else:
|
||||
@@ -336,7 +374,8 @@ class ItemInfoEntryBox(FullBox):
|
||||
|
||||
elif self.version == 2:
|
||||
content.write(struct.pack('>I', self.item_id))
|
||||
content.write(struct.pack('>H', self.item_protection_index))
|
||||
if self._has_protection_field:
|
||||
content.write(struct.pack('>H', self.item_protection_index))
|
||||
# item_type 必须是 4 字节的 4CC,若不足则以 NUL 填充
|
||||
content.write(self.item_type.encode('ascii', errors='ignore')[:4].ljust(4, b'\x00'))
|
||||
content.write(item_name_bytes_to_write)
|
||||
@@ -458,13 +497,25 @@ class ImageSpatialExtentsBox(FullBox):
|
||||
return content.getvalue()
|
||||
|
||||
# --- ItemPropertyAssociationEntry (DataClass) ---
|
||||
class ItemPropertyAssociation:
|
||||
def __init__(self, property_index: int, essential: bool):
|
||||
self.property_index = property_index
|
||||
self.essential = essential
|
||||
|
||||
def __repr__(self):
|
||||
flag = '!' if self.essential else ''
|
||||
return f"{flag}{self.property_index}"
|
||||
|
||||
|
||||
class ItemPropertyAssociationEntry:
|
||||
def __init__(self, item_id, association_count):
|
||||
self.item_id = item_id
|
||||
self.association_count = association_count
|
||||
self.associations = []
|
||||
self.associations: List[ItemPropertyAssociation] = []
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ItemPropertyAssociationEntry item_id={self.item_id} associations={self.associations}>"
|
||||
return (f"<ItemPropertyAssociationEntry item_id={self.item_id} "
|
||||
f"associations={[repr(a) for a in self.associations]}>")
|
||||
|
||||
# --- ItemPropertyAssociationBox ---
|
||||
# ('ipma')
|
||||
@@ -500,10 +551,14 @@ class ItemPropertyAssociationBox(FullBox):
|
||||
prop_size = 2 if is_large_property_index else 1
|
||||
if pos + prop_size > len(stream): break
|
||||
assoc_value = _read_int(stream, pos, prop_size)
|
||||
essential_flag = 0x8000 if prop_size == 2 else 0x80
|
||||
property_index = assoc_value & (0x7FFF if prop_size == 2 else 0x7F)
|
||||
is_essential = (assoc_value & essential_flag) != 0
|
||||
pos += prop_size
|
||||
if property_index > 0:
|
||||
entry.associations.append(property_index)
|
||||
entry.associations.append(
|
||||
ItemPropertyAssociation(property_index, is_essential)
|
||||
)
|
||||
self.entries[item_id] = entry
|
||||
except struct.error:
|
||||
print("Warning: Failed to parse 'ipma' box. Content may be truncated.")
|
||||
@@ -521,9 +576,14 @@ class ItemPropertyAssociationBox(FullBox):
|
||||
content.write(_write_int(item_id, item_id_size))
|
||||
content.write(struct.pack('>B', len(entry.associations)))
|
||||
|
||||
for assoc_index in entry.associations:
|
||||
for assoc in entry.associations:
|
||||
prop_size = 2 if is_large_property_index else 1
|
||||
content.write(_write_int(assoc_index, prop_size))
|
||||
mask = 0x7FFF if prop_size == 2 else 0x7F
|
||||
essential_flag = 0x8000 if prop_size == 2 else 0x80
|
||||
assoc_value = assoc.property_index & mask
|
||||
if assoc.essential:
|
||||
assoc_value |= essential_flag
|
||||
content.write(_write_int(assoc_value, prop_size))
|
||||
|
||||
return content.getvalue()
|
||||
|
||||
|
||||
@@ -132,19 +132,13 @@ def convert_samsung_to_apple(samsung_file_path: str):
|
||||
shifted_id_map = {}
|
||||
|
||||
if iinf_children_to_fix:
|
||||
print(f" Found {len(iinf_children_to_fix)} shifted 'infe' boxes. Fixing them...")
|
||||
print(f" Found {len(iinf_children_to_fix)} shifted 'infe' boxes. Mapping IDs for reference...")
|
||||
for infe_box in iinf_children_to_fix:
|
||||
unshifted_id = infe_box.item_id >> 16
|
||||
# 检查 unshifted_id 是否真的在 correct_ids 中,以防万一
|
||||
if unshifted_id in correct_ids:
|
||||
shifted_id_map[infe_box.item_id] = unshifted_id # 存映射
|
||||
print(f" - Fixing 'infe' ID {infe_box.item_id} -> {unshifted_id}")
|
||||
infe_box.item_id = unshifted_id
|
||||
|
||||
# 同时修复 iinf_box.entries 中的缓存
|
||||
for entry in flat_heic_file._iinf_box.entries:
|
||||
if entry.item_id in shifted_id_map:
|
||||
entry.item_id = shifted_id_map[entry.item_id]
|
||||
print(f" - Mapping 'infe' ID {infe_box.item_id} -> {unshifted_id}")
|
||||
else:
|
||||
print(" 'infe' boxes seem correct. No shift detected.")
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user