heic重构建
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.
|
Before Width: | Height: | Size: 4.2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 14 MiB |
@@ -1,15 +1,21 @@
|
||||
# pyheic_struct/base.py
|
||||
|
||||
from typing import List
|
||||
import struct
|
||||
from io import BytesIO
|
||||
|
||||
class Box:
|
||||
"""
|
||||
Represents a generic ISOBMFF box. This is a simple data container.
|
||||
Represents a generic ISOBMFF box.
|
||||
Now supports finding children and building (writing) data back.
|
||||
"""
|
||||
def __init__(self, size: int, box_type: str, offset: int, raw_data: bytes):
|
||||
self.size = size
|
||||
self.type = box_type
|
||||
self.offset = offset
|
||||
self.raw_data = raw_data
|
||||
self.children: List['Box'] = [] # Initially empty
|
||||
self.children: List['Box'] = []
|
||||
self.is_full_box = False # 默认不是 FullBox
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Box '{self.type}' size={self.size} offset={self.offset}>"
|
||||
@@ -17,3 +23,120 @@ class Box:
|
||||
def _post_parse_initialization(self):
|
||||
"""Called by the parser after children have been assigned."""
|
||||
pass
|
||||
|
||||
def build_header(self, content_size: int) -> bytes:
|
||||
"""Builds the 8-byte (or 16-byte) box header."""
|
||||
header = BytesIO()
|
||||
|
||||
# FullBox (version/flags) 数据属于 'content', 而不是 'header'
|
||||
full_box_header_size = 0
|
||||
if self.is_full_box:
|
||||
full_box_header_size = 4
|
||||
|
||||
final_size = 8 + content_size
|
||||
|
||||
if final_size > 4294967295:
|
||||
# 64-bit 'largesize'
|
||||
header.write(struct.pack('>I', 1))
|
||||
header.write(self.type.encode('ascii'))
|
||||
header.write(struct.pack('>Q', final_size + 8)) # 16-byte header
|
||||
else:
|
||||
# 32-bit standard size
|
||||
header.write(struct.pack('>I', final_size))
|
||||
header.write(self.type.encode('ascii'))
|
||||
|
||||
return header.getvalue()
|
||||
|
||||
def build_content(self) -> bytes:
|
||||
"""
|
||||
序列化此盒的 *内容* (不包括头部)。
|
||||
对于一个通用的容器盒 (container boxes),它会递归构建所有子盒。
|
||||
"""
|
||||
if not self.children:
|
||||
# 对于一个没有子盒的简单数据盒 (如 ftyp, mdat, 或未解析的盒)
|
||||
# 只需返回它持有的原始数据。
|
||||
return self.raw_data
|
||||
|
||||
# 对于一个容器盒 (如 'meta', 'iprp', 'ipco')
|
||||
# 递归构建每一个子盒 (完整的,包括头部) 并拼接它们
|
||||
content_stream = BytesIO()
|
||||
for child in self.children:
|
||||
child_data = child.build_box()
|
||||
content_stream.write(child_data)
|
||||
return content_stream.getvalue()
|
||||
|
||||
def build_box(self) -> bytes:
|
||||
"""
|
||||
构建完整的盒 (头部 + 内容),并返回其二进制数据。
|
||||
"""
|
||||
# 1. "自底向上" 构建内容
|
||||
content_data = self.build_content()
|
||||
|
||||
# 2. 构建头部
|
||||
header_data = self.build_header(len(content_data))
|
||||
|
||||
# 3. 更新 self.size 属性 (8字节标准头 + 内容)
|
||||
self.size = len(header_data) + len(content_data)
|
||||
|
||||
return header_data + content_data
|
||||
|
||||
def find_box(self, box_type: str, recursive: bool = True) -> 'Box' | None:
|
||||
"""在子盒中查找指定类型的第一个盒子"""
|
||||
for child in self.children:
|
||||
if child.type == box_type:
|
||||
return child
|
||||
if recursive and child.children:
|
||||
found = child.find_box(box_type, recursive=True)
|
||||
if found:
|
||||
return found
|
||||
return None
|
||||
|
||||
class FullBox(Box):
|
||||
"""
|
||||
FullBox 是一种特殊的 Box,它在内容开头包含 4 字节的 version 和 flags
|
||||
"""
|
||||
def __init__(self, size: int, box_type: str, offset: int, raw_data: bytes):
|
||||
super().__init__(size, box_type, offset, raw_data)
|
||||
self.is_full_box = True
|
||||
self.version: int = 0
|
||||
self.flags: int = 0
|
||||
self._parse_full_box_header()
|
||||
|
||||
def _parse_full_box_header(self):
|
||||
"""从 raw_data 中解析 version 和 flags"""
|
||||
if len(self.raw_data) >= 4:
|
||||
version_flags = struct.unpack('>I', self.raw_data[:4])[0]
|
||||
self.version = (version_flags >> 24) & 0xFF
|
||||
self.flags = version_flags & 0xFFFFFF
|
||||
|
||||
def build_full_box_header(self) -> bytes:
|
||||
"""构建 4 字节的 version/flags 头部"""
|
||||
version_flags = (self.version << 24) | self.flags
|
||||
return struct.pack('>I', version_flags)
|
||||
|
||||
def build_content(self) -> bytes:
|
||||
"""
|
||||
序列化此 FullBox 的内容。
|
||||
它首先写入 4 字节的 version/flags,然后
|
||||
再写入子盒 (如果是容器) 或 version/flags 之后的
|
||||
原始数据 (如果不是容器)。
|
||||
"""
|
||||
content_stream = BytesIO()
|
||||
|
||||
# 1. 写入 FullBox 特有的 4 字节头部
|
||||
content_stream.write(self.build_full_box_header())
|
||||
|
||||
# 2. 写入剩余的内容
|
||||
if not self.children:
|
||||
# 对于一个没有子盒的简单 FullBox (如 'hdlr')
|
||||
# 写入 self.raw_data 中 *跳过* 4 字节 v/f 之后的部分
|
||||
if len(self.raw_data) >= 4:
|
||||
content_stream.write(self.raw_data[4:])
|
||||
else:
|
||||
# 对于一个容器 FullBox (如 'meta')
|
||||
# 递归构建每一个子盒 (与 Box.build_content 相同)
|
||||
for child in self.children:
|
||||
child_data = child.build_box()
|
||||
content_stream.write(child_data)
|
||||
|
||||
return content_stream.getvalue()
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
# pyheic_struct/builder.py
|
||||
|
||||
import struct
|
||||
from heic_file import HEICFile
|
||||
from heic_types import ItemLocationBox
|
||||
|
||||
class HEICBuilder:
|
||||
def __init__(self, heic_file: HEICFile):
|
||||
self.heic_file = heic_file
|
||||
self.mdat_box = heic_file.get_mdat_box()
|
||||
self.meta_box = heic_file.find_box('meta')
|
||||
self.iloc_box = heic_file._iloc_box
|
||||
|
||||
if not self.mdat_box:
|
||||
raise ValueError("Cannot build: 'mdat' box not found.")
|
||||
if not self.meta_box:
|
||||
raise ValueError("Cannot build: 'meta' box not found.")
|
||||
if not self.iloc_box:
|
||||
raise ValueError("Cannot build: 'iloc' box not found.")
|
||||
|
||||
# 存储 'mdat' 的 *原始* 偏移量。这是至关重要的。
|
||||
self.original_mdat_offset = self.mdat_box.offset
|
||||
|
||||
# 分离元数据和数据
|
||||
self.top_level_meta_boxes = [b for b in self.heic_file.boxes if b.type != 'mdat']
|
||||
|
||||
def _calculate_meta_offset_delta(self) -> int:
|
||||
"""(V14) 辅助函数:计算 'meta' 盒的偏移增量"""
|
||||
ftyp_box = self.heic_file._ftyp_box
|
||||
if not ftyp_box:
|
||||
ftyp_box = next((b for b in self.top_level_meta_boxes if b.type == 'ftyp'), None)
|
||||
|
||||
new_meta_offset = ftyp_box.size if ftyp_box else 0
|
||||
original_meta_offset = self.meta_box.offset
|
||||
return new_meta_offset - original_meta_offset
|
||||
|
||||
def _rebuild_iloc_with_delta(self, mdat_offset_delta: int):
|
||||
"""(V14) 辅助函数:使用给定的 delta 重建 iloc"""
|
||||
print(f" ... Rebuilding 'iloc' using mdat_delta: {mdat_offset_delta}")
|
||||
meta_offset_delta = self._calculate_meta_offset_delta()
|
||||
try:
|
||||
self.iloc_box.rebuild_iloc_content(
|
||||
mdat_offset_delta=mdat_offset_delta,
|
||||
original_mdat_offset=self.original_mdat_offset,
|
||||
original_mdat_size=self.mdat_box.size,
|
||||
meta_offset_delta=meta_offset_delta,
|
||||
original_meta_offset=self.meta_box.offset,
|
||||
original_meta_size=self.meta_box.size
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"CRITICAL: Failed to rebuild 'iloc' box content: {e}")
|
||||
raise # 抛出异常以停止构建
|
||||
|
||||
def _calculate_final_meta_size(self) -> int:
|
||||
"""(V14) 辅助函数:递归构建所有元数据盒以获取其最终大小"""
|
||||
current_offset = 0
|
||||
for box in self.top_level_meta_boxes:
|
||||
# build_box() 会递归计算所有子盒的最终大小
|
||||
final_box_data = box.build_box()
|
||||
current_offset += len(final_box_data)
|
||||
return current_offset
|
||||
|
||||
def write(self, output_path: str):
|
||||
"""
|
||||
(V14) 使用一个多遍系统来解决 'iloc' 重建引起的循环依赖问题。
|
||||
"""
|
||||
|
||||
# --- Pass 1: 初步布局计算 ---
|
||||
# (在 'iloc' 重建 *之前* 计算一次大小)
|
||||
print("--- Builder Pass 1: Preliminary Layout ---")
|
||||
preliminary_meta_size = self._calculate_final_meta_size()
|
||||
preliminary_mdat_offset = preliminary_meta_size
|
||||
preliminary_mdat_delta = preliminary_mdat_offset - self.original_mdat_offset
|
||||
|
||||
print(f"Original mdat offset: {self.original_mdat_offset}")
|
||||
print(f"Preliminary mdat offset: {preliminary_mdat_offset}")
|
||||
print(f"Preliminary offset delta: {preliminary_mdat_delta}")
|
||||
|
||||
# --- Pass 2: 初步重建 'iloc' ---
|
||||
# (使用 *初步的* delta 重建 iloc,这会改变 'iloc' 的大小)
|
||||
print("\n--- Builder Pass 2: Preliminary 'iloc' Rebuild ---")
|
||||
self._rebuild_iloc_with_delta(preliminary_mdat_delta)
|
||||
print(" 'iloc' preliminary rebuild complete.")
|
||||
|
||||
# --- Pass 3: 最终布局计算 ---
|
||||
# (现在 'iloc' 有了最终大小,我们 *再次* 计算 meta 的总大小)
|
||||
print("\n--- Builder Pass 3: Final Layout Calculation ---")
|
||||
final_meta_size = self._calculate_final_meta_size()
|
||||
final_mdat_offset = final_meta_size
|
||||
final_mdat_delta = final_mdat_offset - self.original_mdat_offset
|
||||
|
||||
print(f"Final meta size (actual): {final_meta_size}")
|
||||
print(f"Final mdat offset (actual): {final_mdat_offset}")
|
||||
print(f"Final offset delta (actual): {final_mdat_delta}")
|
||||
|
||||
# --- Pass 4: 最终重建 'iloc' ---
|
||||
# (使用 *最终的、正确*的 delta 再次重建 iloc)
|
||||
if final_mdat_delta != preliminary_mdat_delta:
|
||||
print("\n--- Builder Pass 4: Final 'iloc' Rebuild (Correcting Delta) ---")
|
||||
self._rebuild_iloc_with_delta(final_mdat_delta)
|
||||
print(" 'iloc' final rebuild complete.")
|
||||
else:
|
||||
print("\n--- Builder Pass 4: Skipped (Preliminary delta was correct) ---")
|
||||
|
||||
# --- Pass 5: 写入文件 ---
|
||||
print("\n--- Builder Pass 5: Rebuild & Write ---")
|
||||
|
||||
# 只有在 'iloc' 重建成功后,我们才打开(并创建)输出文件
|
||||
with open(output_path, 'wb') as f:
|
||||
|
||||
current_offset = 0
|
||||
for box in self.top_level_meta_boxes:
|
||||
final_box_data = box.build_box()
|
||||
f.write(final_box_data)
|
||||
current_offset += len(final_box_data)
|
||||
print(f"Wrote '{box.type}' (final size: {len(final_box_data)})")
|
||||
|
||||
# 完整性检查
|
||||
if current_offset != final_mdat_offset:
|
||||
print(f"WARNING: Final meta size ({current_offset}) does not match"
|
||||
f" calculated mdat offset ({final_mdat_offset})!")
|
||||
|
||||
# 写入 'mdat' 盒 (头部 + 数据)
|
||||
mdat_data = self.mdat_box.raw_data
|
||||
mdat_size = 8 + len(mdat_data) # 8-byte header
|
||||
|
||||
print(f"Writing 'mdat' (final size: {mdat_size}) at offset {current_offset}...")
|
||||
|
||||
if mdat_size > 4294967295:
|
||||
f.write(struct.pack('>I', 1))
|
||||
f.write(b'mdat')
|
||||
f.write(struct.pack('>Q', mdat_size))
|
||||
else:
|
||||
f.write(struct.pack('>I', mdat_size))
|
||||
f.write(b'mdat')
|
||||
|
||||
f.write(mdat_data)
|
||||
|
||||
print(f"\nSuccessfully rebuilt file at: {output_path}")
|
||||
Vendored
BIN
Binary file not shown.
Binary file not shown.
@@ -7,11 +7,14 @@ class SamsungHandler(VendorHandler):
|
||||
"""
|
||||
Searches for the 'mpvd' box which contains the video data.
|
||||
"""
|
||||
for box in heic_file.boxes:
|
||||
if box.type == 'mpvd':
|
||||
print(f"Samsung 'mpvd' box found at offset {box.offset}")
|
||||
# The video data starts right after the box header.
|
||||
return box.offset + 8
|
||||
# --- START FIX ---
|
||||
# 使用 heic_file.find_box 进行递归搜索,因为 mpvd 嵌套在 meta 中
|
||||
mpvd_box = heic_file.find_box('mpvd')
|
||||
if mpvd_box:
|
||||
print(f"Samsung 'mpvd' box found at offset {mpvd_box.offset}")
|
||||
# 视频数据在 8 字节头部之后开始
|
||||
return mpvd_box.offset + 8
|
||||
# --- END FIX ---
|
||||
|
||||
print("Samsung HEIC detected, but no 'mpvd' box found.")
|
||||
return None
|
||||
+295
-27
@@ -12,7 +12,8 @@ from base import Box
|
||||
from parser import parse_boxes
|
||||
from heic_types import (
|
||||
ItemLocationBox, PrimaryItemBox, ItemInfoBox, ItemPropertiesBox,
|
||||
ImageSpatialExtentsBox, ItemReferenceBox
|
||||
ImageSpatialExtentsBox, ItemReferenceBox, ItemInfoEntryBox,
|
||||
_read_int
|
||||
)
|
||||
from handlers.base_handler import VendorHandler
|
||||
from handlers.apple_handler import AppleHandler
|
||||
@@ -28,7 +29,6 @@ class Grid:
|
||||
class HEICFile:
|
||||
def __init__(self, filepath: str):
|
||||
self.filepath = filepath
|
||||
# 注册 HEIF/HEIC 文件格式解码器
|
||||
pillow_heif.register_heif_opener()
|
||||
|
||||
self._iloc_box: ItemLocationBox | None = None
|
||||
@@ -38,14 +38,20 @@ class HEICFile:
|
||||
self._iprp_box: ItemPropertiesBox | None = None
|
||||
self._iref_box: ItemReferenceBox | None = None
|
||||
self.handler: VendorHandler | None = None
|
||||
self.boxes: list[Box] = [] # 顶层盒子
|
||||
|
||||
try:
|
||||
with open(self.filepath, 'rb') as f:
|
||||
file_size = os.fstat(f.fileno()).st_size
|
||||
self.boxes = parse_boxes(f, file_size)
|
||||
self._find_essential_boxes(self.boxes)
|
||||
self._detect_vendor()
|
||||
except Exception as e:
|
||||
print(f"CRITICAL ERROR during file parsing: {e}")
|
||||
raise
|
||||
|
||||
def _find_essential_boxes(self, boxes: list[Box]):
|
||||
"""在解析树中查找关键盒子的快捷方式"""
|
||||
for box in boxes:
|
||||
if isinstance(box, ItemLocationBox): self._iloc_box = box
|
||||
if isinstance(box, ItemInfoBox): self._iinf_box = box
|
||||
@@ -55,19 +61,236 @@ class HEICFile:
|
||||
if box.type == 'ftyp': self._ftyp_box = box
|
||||
if box.children: self._find_essential_boxes(box.children)
|
||||
|
||||
# --- 2. 使用 pillow-heif 大大简化的图像重建方法 ---
|
||||
# --- 辅助方法 (用于修改和构建) ---
|
||||
|
||||
def find_box(self, box_type: str, root_box_list: list[Box] | None = None) -> Box | None:
|
||||
"""递归查找第一个匹配类型的盒子"""
|
||||
if root_box_list is None:
|
||||
root_box_list = self.boxes
|
||||
|
||||
for box in root_box_list:
|
||||
if box.type == box_type:
|
||||
return box
|
||||
if box.children:
|
||||
found = self.find_box(box_type, root_box_list=box.children)
|
||||
if found:
|
||||
return found
|
||||
return None
|
||||
|
||||
def get_mdat_box(self) -> Box | None:
|
||||
"""获取顶层的 'mdat' 盒"""
|
||||
for box in self.boxes:
|
||||
if box.type == 'mdat':
|
||||
return box
|
||||
return None
|
||||
|
||||
def _remove_box_recursive(self, box_type: str, box_list: list[Box]) -> bool:
|
||||
"""Helper for remove_box_by_type"""
|
||||
for i, box in enumerate(box_list):
|
||||
if box.type == box_type:
|
||||
box_list.pop(i)
|
||||
return True
|
||||
if box.children:
|
||||
if self._remove_box_recursive(box_type, box.children):
|
||||
return True
|
||||
return False
|
||||
|
||||
def remove_box_by_type(self, box_type: str) -> bool:
|
||||
"""递归查找并移除第一个匹配类型的盒子"""
|
||||
return self._remove_box_recursive(box_type, self.boxes)
|
||||
|
||||
# --- START V16 FIX ---
|
||||
def remove_item_by_id(self, item_id_to_remove: int):
|
||||
"""
|
||||
(V16 - 垃圾回收版)
|
||||
彻底从 iinf, iloc, ipma, iref 中删除一个 Item ID,
|
||||
并从 ipco 中删除孤立的属性,然后重写所有剩余的 ipma 索引。
|
||||
"""
|
||||
print(f"Attempting to remove Item ID {item_id_to_remove} from all references (V16)...")
|
||||
|
||||
# 1. 从 iinf (Item Info) 中删除
|
||||
if self._iinf_box:
|
||||
self._iinf_box.children = [
|
||||
c for c in self._iinf_box.children
|
||||
if not (isinstance(c, ItemInfoEntryBox) and c.item_id == item_id_to_remove)
|
||||
]
|
||||
self._iinf_box.entries = [
|
||||
e for e in self._iinf_box.entries
|
||||
if e.item_id != item_id_to_remove
|
||||
]
|
||||
print(f" - Removed from 'iinf' box.")
|
||||
|
||||
# 2. 从 iloc (Item Location) 中删除
|
||||
if self._iloc_box:
|
||||
self._iloc_box.locations = [
|
||||
loc for loc in self._iloc_box.locations
|
||||
if loc.item_id != item_id_to_remove
|
||||
]
|
||||
print(f" - Removed from 'iloc' box.")
|
||||
|
||||
# 3. (新) 从 iref (Item Reference) 中删除
|
||||
if self._iref_box:
|
||||
for i in range(len(self._iref_box.children) - 1, -1, -1):
|
||||
ref_box = self._iref_box.children[i]
|
||||
from_id_size = 4 if self._iref_box.version == 1 else 2
|
||||
if len(ref_box.raw_data) >= 4 + from_id_size:
|
||||
from_id = _read_int(ref_box.raw_data, 4, from_id_size)
|
||||
if from_id == item_id_to_remove:
|
||||
self._iref_box.children.pop(i)
|
||||
print(f" - Removed 'iref' child box (type '{ref_box.type}') with from_id {from_id}.")
|
||||
|
||||
ref_types_to_clean = list(self._iref_box.references.keys())
|
||||
for ref_type in ref_types_to_clean:
|
||||
if item_id_to_remove in self._iref_box.references[ref_type]:
|
||||
del self._iref_box.references[ref_type][item_id_to_remove]
|
||||
print(f" - Removed from_id {item_id_to_remove} from 'iref.references[{ref_type}]'.")
|
||||
|
||||
from_ids_to_clean = list(self._iref_box.references[ref_type].keys())
|
||||
for from_id in from_ids_to_clean:
|
||||
self._iref_box.references[ref_type][from_id] = [
|
||||
to_id for to_id in self._iref_box.references[ref_type][from_id]
|
||||
if to_id != item_id_to_remove
|
||||
]
|
||||
print(f" - Cleaned 'iref.references' of to_id {item_id_to_remove}.")
|
||||
|
||||
# 4. 从 ipma 和 ipco (属性) 中删除
|
||||
if self._iprp_box and self._iprp_box.ipma and self._iprp_box.ipco:
|
||||
ipma = self._iprp_box.ipma
|
||||
ipco = self._iprp_box.ipco
|
||||
|
||||
if item_id_to_remove not in ipma.entries:
|
||||
print(f" - Item {item_id_to_remove} not in 'ipma'. No properties to clean.")
|
||||
return
|
||||
|
||||
# 4a. 找到要删除的属性索引 (1-based)
|
||||
props_to_remove = set(ipma.entries[item_id_to_remove].associations)
|
||||
|
||||
# 4b. 从 ipma 中删除该 Item
|
||||
del ipma.entries[item_id_to_remove]
|
||||
print(f" - Removed Item {item_id_to_remove} from 'ipma'.")
|
||||
|
||||
# 4c. 确定哪些属性是“孤儿”
|
||||
# (即,它们在 props_to_remove 中,但*不在*任何*剩余*的 item 关联中)
|
||||
all_remaining_props = set()
|
||||
for entry in ipma.entries.values():
|
||||
all_remaining_props.update(entry.associations)
|
||||
|
||||
orphaned_props = props_to_remove - all_remaining_props
|
||||
|
||||
if not orphaned_props:
|
||||
print(f" - No orphaned properties found in 'ipco' to remove.")
|
||||
return
|
||||
|
||||
print(f" - Found orphaned properties to remove from 'ipco': {orphaned_props}")
|
||||
|
||||
# 4d. 创建一个“重映射表”
|
||||
# 我们从后往前遍历,以安全地删除
|
||||
# (索引是 1-based, 但列表是 0-based)
|
||||
orphaned_indices_0based = sorted([p - 1 for p in orphaned_props], reverse=True)
|
||||
|
||||
# 原始 1-based 索引 -> 新 1-based 索引
|
||||
remap_table = {}
|
||||
original_prop_count = len(ipco.children)
|
||||
|
||||
# 4e. 从 'ipco.children' 列表中删除孤儿
|
||||
for index_0 in orphaned_indices_0based:
|
||||
if 0 <= index_0 < len(ipco.children):
|
||||
removed_prop = ipco.children.pop(index_0)
|
||||
print(f" - Removed property at index {index_0+1} ({removed_prop.type}) from 'ipco'.")
|
||||
else:
|
||||
print(f" - Warning: Orphaned index {index_0+1} out of bounds for 'ipco'.")
|
||||
|
||||
# 4f. 构建重映射表
|
||||
# (只有在 ipco 实际发生变化时才需要)
|
||||
new_prop_count = len(ipco.children)
|
||||
if new_prop_count != original_prop_count:
|
||||
print(" - Re-indexing 'ipma' associations...")
|
||||
current_new_index = 1
|
||||
current_old_index = 1
|
||||
orphaned_props_1based = set(i + 1 for i in orphaned_indices_0based)
|
||||
|
||||
while current_old_index <= original_prop_count:
|
||||
if current_old_index not in orphaned_props_1based:
|
||||
remap_table[current_old_index] = current_new_index
|
||||
current_new_index += 1
|
||||
current_old_index += 1
|
||||
|
||||
# 4g. 应用重映射
|
||||
for item_id, entry in ipma.entries.items():
|
||||
new_associations = []
|
||||
for old_prop_index in entry.associations:
|
||||
if old_prop_index in remap_table:
|
||||
new_associations.append(remap_table[old_prop_index])
|
||||
# else:
|
||||
# 该属性已被删除 (不应发生,因为我们只删除了孤儿)
|
||||
|
||||
# print(f" - Item {item_id} associations: {entry.associations} -> {new_associations}")
|
||||
entry.associations = new_associations
|
||||
print(" - 'ipma' re-indexing complete.")
|
||||
else:
|
||||
print(" - No 'ipma' re-indexing needed.")
|
||||
# --- END V16 FIX ---
|
||||
|
||||
def set_content_identifier(self, new_content_id: str) -> bool:
|
||||
"""
|
||||
(FIXED) 在 'iinf' 盒中查找主图像的 'infe' 盒,并设置其 item_name
|
||||
"""
|
||||
primary_id = self.get_primary_item_id()
|
||||
if not primary_id:
|
||||
print("Error: Cannot find primary item ID.")
|
||||
return False
|
||||
|
||||
if not self._iinf_box:
|
||||
print("Error: Cannot find 'iinf' box shortcut (_iinf_box).")
|
||||
return False
|
||||
|
||||
print(f"Searching for 'infe' box with primary_id = {primary_id}")
|
||||
|
||||
# --- START FIX ---
|
||||
# 侦测我们日志中看到的 "shifted ID" 格式
|
||||
|
||||
target_id = primary_id
|
||||
found_ids = [box.item_id for box in self._iinf_box.children if isinstance(box, ItemInfoEntryBox)]
|
||||
|
||||
if primary_id not in found_ids:
|
||||
print(f"Warning: Primary ID {primary_id} not found directly in 'infe' list.")
|
||||
shifted_id = primary_id << 16
|
||||
if shifted_id in found_ids:
|
||||
print(f"Info: Found vendor-specific shifted ID: {shifted_id} (for {primary_id})")
|
||||
target_id = shifted_id
|
||||
else:
|
||||
print(f"Error: Could not find primary ID {primary_id} OR shifted ID {shifted_id}.")
|
||||
print(f"Available 'infe' item IDs found were: {found_ids}")
|
||||
return False
|
||||
# --- END FIX ---
|
||||
|
||||
# 'iinf' 盒的子盒是 'infe' 盒
|
||||
for box in self._iinf_box.children:
|
||||
if isinstance(box, ItemInfoEntryBox):
|
||||
if box.item_id == target_id:
|
||||
print(f"Success: Found 'infe' box for target ID {target_id}. Setting item_name...")
|
||||
box.item_name = new_content_id
|
||||
|
||||
# 同时更新 self._iinf_box.entries 中的数据以保持一致
|
||||
for entry in self._iinf_box.entries:
|
||||
if entry.item_id == target_id:
|
||||
entry.name = new_content_id
|
||||
break
|
||||
return True
|
||||
|
||||
print(f"Error: Logic failed to find 'infe' box for target ID {target_id} even after check.")
|
||||
return False
|
||||
|
||||
# --- 现有的只读方法 (无需修改) ---
|
||||
|
||||
def reconstruct_primary_image(self) -> Image.Image | None:
|
||||
"""
|
||||
使用 pillow-heif 重建主图像,它能自动处理网格图像。
|
||||
"""
|
||||
try:
|
||||
print("Reconstructing primary image using pillow-heif...")
|
||||
# pillow-heif 让我们能像打开普通图片一样直接打开 HEIC 文件
|
||||
image = Image.open(self.filepath)
|
||||
|
||||
# 确保图像数据被加载
|
||||
image.load()
|
||||
|
||||
print("Successfully reconstructed image.")
|
||||
return image
|
||||
except Exception as e:
|
||||
@@ -96,15 +319,37 @@ class HEICFile:
|
||||
if not primary_id: return None
|
||||
if not self._iref_box: return None
|
||||
|
||||
# 'dimg' (derived image) 是网格布局的标准引用类型
|
||||
if 'dimg' in self._iref_box.references:
|
||||
# 尝试直接匹配
|
||||
if 'dimg' in self._iref_box.references and primary_id in self._iref_box.references['dimg']:
|
||||
return self._iref_box.references['dimg'].get(primary_id)
|
||||
|
||||
# 尝试匹配 "shifted" ID
|
||||
shifted_id = primary_id << 16
|
||||
if 'dimg' in self._iref_box.references and shifted_id in self._iref_box.references['dimg']:
|
||||
print("Info: Using shifted primary ID to find grid layout.")
|
||||
return self._iref_box.references['dimg'].get(shifted_id)
|
||||
|
||||
return None
|
||||
|
||||
def get_image_size(self, item_id: int) -> tuple[int, int] | None:
|
||||
if not (self._iprp_box and self._iprp_box.ipma and self._iprp_box.ipco): return None
|
||||
if item_id not in self._iprp_box.ipma.entries: return None
|
||||
item_associations = self._iprp_box.ipma.entries[item_id].associations
|
||||
|
||||
target_id = item_id
|
||||
if item_id not in self._iprp_box.ipma.entries:
|
||||
# 尝试 "shifted" ID
|
||||
shifted_id = item_id << 16
|
||||
if shifted_id in self._iprp_box.ipma.entries:
|
||||
target_id = shifted_id
|
||||
else:
|
||||
# 尝试 "unshifted" ID (以防 item_id 本身就是 shifted 的)
|
||||
unshifted_id = item_id & 0x0000FFFF
|
||||
if unshifted_id in self._iprp_box.ipma.entries:
|
||||
target_id = unshifted_id
|
||||
else:
|
||||
# print(f"Warning: Cannot find size associations for item ID {item_id} or variants.")
|
||||
return None
|
||||
|
||||
item_associations = self._iprp_box.ipma.entries[target_id].associations
|
||||
for assoc in item_associations:
|
||||
property_index = assoc - 1
|
||||
if 0 <= property_index < len(self._iprp_box.ipco.children):
|
||||
@@ -114,7 +359,9 @@ class HEICFile:
|
||||
return None
|
||||
|
||||
def get_primary_item_id(self) -> int | None:
|
||||
if not self._pitm_box: return None
|
||||
if not self._pitm_box:
|
||||
print("Warning: 'pitm' box not found.")
|
||||
return None
|
||||
return self._pitm_box.item_id
|
||||
|
||||
def list_items(self):
|
||||
@@ -134,12 +381,32 @@ class HEICFile:
|
||||
if not self._iloc_box:
|
||||
print("Error: 'iloc' box not found.")
|
||||
return None
|
||||
location = next((loc for loc in self._iloc_box.locations if loc.item_id == item_id), None)
|
||||
|
||||
target_id = item_id
|
||||
location = next((loc for loc in self._iloc_box.locations if loc.item_id == target_id), None)
|
||||
|
||||
if not location:
|
||||
print(f"Error: Item with ID {item_id} not found in 'iloc' box.")
|
||||
# 尝试检查 "shifted" ID
|
||||
shifted_id = item_id << 16
|
||||
location = next((loc for loc in self._iloc_box.locations if loc.item_id == shifted_id), None)
|
||||
if location:
|
||||
print(f"Info: Located item {item_id} using shifted ID {shifted_id} in 'iloc'.")
|
||||
target_id = shifted_id
|
||||
|
||||
if not location and (item_id & 0xFFFF0000):
|
||||
# 尝试反向检查 (如果传入的 ID 已经是 shifted 的)
|
||||
unshifted_id = item_id & 0x0000FFFF
|
||||
location = next((loc for loc in self._iloc_box.locations if loc.item_id == unshifted_id), None)
|
||||
if location:
|
||||
print(f"Info: Located item {item_id} using un-shifted ID {unshifted_id} in 'iloc'.")
|
||||
target_id = unshifted_id
|
||||
|
||||
if not location:
|
||||
print(f"Error: Item with ID {item_id} (or variants) not found in 'iloc' box.")
|
||||
return None
|
||||
|
||||
if not location.extents:
|
||||
print(f"Warning: Item with ID {item_id} has no extents.")
|
||||
print(f"Warning: Item with ID {target_id} has no extents.")
|
||||
return b''
|
||||
|
||||
data_chunks = []
|
||||
@@ -153,16 +420,13 @@ class HEICFile:
|
||||
if not self.handler: self._detect_vendor()
|
||||
offset = self.handler.find_motion_photo_offset(self)
|
||||
if offset is not None:
|
||||
print("Found motion photo data at offset.")
|
||||
with open(self.filepath, 'rb') as f:
|
||||
f.seek(offset)
|
||||
return f.read()
|
||||
return None
|
||||
|
||||
def get_thumbnail_data(self) -> bytes | None:
|
||||
"""
|
||||
查找并提取缩略图图像数据 (如果存在)。
|
||||
它会查找一个 'iref' 盒子,其中包含 'thmb' (thumbnail) 引用。
|
||||
"""
|
||||
print("Attempting to extract thumbnail data...")
|
||||
primary_id = self.get_primary_item_id()
|
||||
if not primary_id:
|
||||
@@ -173,26 +437,30 @@ class HEICFile:
|
||||
print("Info: No 'iref' box found, cannot search for thumbnail.")
|
||||
return None
|
||||
|
||||
# 检查 'thmb' (thumbnail) 引用类型是否存在
|
||||
if 'thmb' not in self._iref_box.references:
|
||||
print("Info: No 'thmb' references found in 'iref' box.")
|
||||
return None
|
||||
|
||||
# 检查主图像是否有与之关联的缩略图
|
||||
target_id = primary_id
|
||||
if primary_id not in self._iref_box.references['thmb']:
|
||||
print(f"Info: Primary item ID {primary_id} has no 'thmb' reference.")
|
||||
# Lпробуйте проверить "shifted" ID
|
||||
shifted_primary_id = primary_id << 16
|
||||
if shifted_primary_id not in self._iref_box.references['thmb']:
|
||||
print(f"Info: Primary item ID {primary_id} (or shifted) has no 'thmb' reference.")
|
||||
return None
|
||||
|
||||
thumbnail_ids = self._iref_box.references['thmb'][primary_id]
|
||||
print("Info: Using shifted primary ID to find thumbnail.")
|
||||
target_id = shifted_primary_id
|
||||
|
||||
|
||||
thumbnail_ids = self._iref_box.references['thmb'][target_id]
|
||||
if not thumbnail_ids:
|
||||
print(f"Info: Primary item ID {primary_id} has 'thmb' reference, but no target IDs.")
|
||||
print(f"Info: Primary item ID {target_id} has 'thmb' reference, but no target IDs.")
|
||||
return None
|
||||
|
||||
# 获取第一个缩略图 ID
|
||||
thumbnail_id = thumbnail_ids[0]
|
||||
print(f"Found thumbnail reference: Primary ID {primary_id} -> Thumbnail ID {thumbnail_id}")
|
||||
print(f"Found thumbnail reference: Primary ID {target_id} -> Thumbnail ID {thumbnail_id}")
|
||||
|
||||
# 使用现有的 get_item_data 来提取它
|
||||
thumbnail_data = self.get_item_data(thumbnail_id)
|
||||
|
||||
if thumbnail_data:
|
||||
|
||||
+464
-162
@@ -1,92 +1,14 @@
|
||||
# pyheic_struct/heic_types.py
|
||||
|
||||
import struct
|
||||
from base import Box
|
||||
from base import Box, FullBox
|
||||
from io import BytesIO
|
||||
from typing import List # <-- 添加 List 导入
|
||||
|
||||
# --- A new helper class to handle multiple data locations ('extents') ---
|
||||
class ItemLocation:
|
||||
def __init__(self, item_id):
|
||||
self.item_id = item_id
|
||||
self.extents = [] # Will be a list of (offset, length) tuples
|
||||
# --- 帮助函数 ---
|
||||
|
||||
def __repr__(self):
|
||||
total_length = sum(ext[1] for ext in self.extents)
|
||||
return f"<ItemLocation ID={self.item_id} extents={len(self.extents)} total_size={total_length}>"
|
||||
|
||||
# --- REWRITTEN ItemLocationBox to correctly parse all item locations ---
|
||||
class ItemLocationBox(Box):
|
||||
def __init__(self, size: int, box_type: str, offset: int, raw_data: bytes):
|
||||
super().__init__(size, box_type, offset, raw_data)
|
||||
self.locations = []
|
||||
self._parse_locations()
|
||||
|
||||
def _parse_locations(self):
|
||||
stream = self.raw_data
|
||||
|
||||
version_flags = struct.unpack('>I', stream[:4])[0]
|
||||
version = version_flags >> 24
|
||||
|
||||
sizes = struct.unpack('>H', stream[4:6])[0]
|
||||
offset_size = (sizes >> 12) & 0x0F
|
||||
length_size = (sizes >> 8) & 0x0F
|
||||
base_offset_size = (sizes >> 4) & 0x0F
|
||||
|
||||
index_size = 0
|
||||
if version == 1 or version == 2:
|
||||
index_size = sizes & 0x0F
|
||||
|
||||
item_count = 0
|
||||
if version < 2:
|
||||
item_count = struct.unpack('>H', stream[6:8])[0]
|
||||
current_pos = 8
|
||||
else: # version == 2
|
||||
item_count = struct.unpack('>I', stream[6:10])[0]
|
||||
current_pos = 10
|
||||
|
||||
for _ in range(item_count):
|
||||
item_id = 0
|
||||
# Item ID size depends on the box version
|
||||
if version < 2:
|
||||
if current_pos + 2 > len(stream): break
|
||||
item_id = struct.unpack('>H', stream[current_pos : current_pos+2])[0]
|
||||
current_pos += 2
|
||||
else: # version == 2
|
||||
if current_pos + 4 > len(stream): break
|
||||
item_id = struct.unpack('>I', stream[current_pos : current_pos+4])[0]
|
||||
current_pos += 4
|
||||
|
||||
if (version == 1 or version == 2) and current_pos + 2 <= len(stream):
|
||||
current_pos += 2 # Skip construction method
|
||||
|
||||
if current_pos + 2 > len(stream): break
|
||||
current_pos += 2 # Skip data_reference_index
|
||||
|
||||
base_offset = 0
|
||||
if base_offset_size > 0:
|
||||
if current_pos + base_offset_size > len(stream): break
|
||||
base_offset = self._read_int(stream, current_pos, base_offset_size)
|
||||
current_pos += base_offset_size
|
||||
|
||||
if current_pos + 2 > len(stream): break
|
||||
extent_count = struct.unpack('>H', stream[current_pos : current_pos+2])[0]
|
||||
current_pos += 2
|
||||
|
||||
loc = ItemLocation(item_id)
|
||||
for __ in range(extent_count):
|
||||
if (version == 1 or version == 2) and index_size > 0:
|
||||
if current_pos + index_size > len(stream): break
|
||||
current_pos += index_size # Skip extent_index
|
||||
|
||||
extent_offset = self._read_int(stream, current_pos, offset_size)
|
||||
current_pos += offset_size
|
||||
|
||||
extent_length = self._read_int(stream, current_pos, length_size)
|
||||
current_pos += length_size
|
||||
|
||||
loc.extents.append((base_offset + extent_offset, extent_length))
|
||||
self.locations.append(loc)
|
||||
|
||||
def _read_int(self, data, pos, size):
|
||||
def _read_int(data: bytes, pos: int, size: int) -> int:
|
||||
"""Helper to read an integer of variable size."""
|
||||
if pos + size > len(data): return 0
|
||||
if size == 0: return 0
|
||||
if size == 1: return data[pos]
|
||||
@@ -95,50 +17,380 @@ class ItemLocationBox(Box):
|
||||
if size == 8: return struct.unpack('>Q', data[pos:pos+8])[0]
|
||||
return 0
|
||||
|
||||
# --- All other classes below remain unchanged ---
|
||||
def _write_int(value: int, size: int) -> bytes:
|
||||
"""Helper to write an integer of variable size."""
|
||||
if size == 0: return b''
|
||||
if size == 1: return struct.pack('>B', value)
|
||||
if size == 2: return struct.pack('>H', value)
|
||||
if size == 4: return struct.pack('>I', value)
|
||||
if size == 8: return struct.pack('>Q', value)
|
||||
return b''
|
||||
|
||||
# --- ItemLocation ---
|
||||
class ItemLocation:
|
||||
def __init__(self, item_id):
|
||||
self.item_id = item_id
|
||||
# extents 存储 (absolute_offset, length)
|
||||
self.extents = []
|
||||
|
||||
def __repr__(self):
|
||||
total_length = sum(ext[1] for ext in self.extents)
|
||||
return f"<ItemLocation ID={self.item_id} extents={len(self.extents)} total_size={total_length}>"
|
||||
|
||||
# --- ItemLocationBox ---
|
||||
# ('iloc')
|
||||
class ItemLocationBox(FullBox):
|
||||
def __init__(self, size: int, box_type: str, offset: int, raw_data: bytes):
|
||||
self.locations: List[ItemLocation] = []
|
||||
self.offset_size = 0
|
||||
self.length_size = 0
|
||||
self.base_offset_size = 0
|
||||
self.index_size = 0
|
||||
self.item_count = 0
|
||||
super().__init__(size, box_type, offset, raw_data)
|
||||
|
||||
def _post_parse_initialization(self):
|
||||
self._parse_locations()
|
||||
|
||||
def _parse_locations(self):
|
||||
stream = self.raw_data[4:]
|
||||
|
||||
sizes = struct.unpack('>H', stream[0:2])[0]
|
||||
self.offset_size = (sizes >> 12) & 0x0F
|
||||
self.length_size = (sizes >> 8) & 0x0F
|
||||
self.base_offset_size = (sizes >> 4) & 0x0F
|
||||
|
||||
if self.version == 1 or self.version == 2:
|
||||
self.index_size = sizes & 0x0F
|
||||
|
||||
current_pos = 2
|
||||
if self.version < 2:
|
||||
self.item_count = struct.unpack('>H', stream[2:4])[0]
|
||||
current_pos = 4
|
||||
else:
|
||||
self.item_count = struct.unpack('>I', stream[2:6])[0]
|
||||
current_pos = 6
|
||||
|
||||
for _ in range(self.item_count):
|
||||
item_id = 0
|
||||
item_id_size = 2 if self.version < 2 else 4
|
||||
if current_pos + item_id_size > len(stream): break
|
||||
item_id = _read_int(stream, current_pos, item_id_size)
|
||||
current_pos += item_id_size
|
||||
|
||||
loc = ItemLocation(item_id)
|
||||
|
||||
if (self.version == 1 or self.version == 2) and current_pos + 2 <= len(stream):
|
||||
current_pos += 2
|
||||
|
||||
if current_pos + 2 > len(stream): break
|
||||
current_pos += 2
|
||||
|
||||
base_offset = 0
|
||||
if self.base_offset_size > 0:
|
||||
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
|
||||
|
||||
if current_pos + 2 > len(stream): break
|
||||
extent_count = struct.unpack('>H', stream[current_pos : current_pos+2])[0]
|
||||
current_pos += 2
|
||||
|
||||
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
|
||||
current_pos += self.index_size
|
||||
|
||||
if current_pos + self.offset_size > len(stream): break
|
||||
extent_offset = _read_int(stream, current_pos, self.offset_size)
|
||||
current_pos += self.offset_size
|
||||
|
||||
if current_pos + self.length_size > len(stream): break
|
||||
extent_length = _read_int(stream, current_pos, self.length_size)
|
||||
current_pos += self.length_size
|
||||
|
||||
loc.extents.append((base_offset + extent_offset, extent_length))
|
||||
self.locations.append(loc)
|
||||
|
||||
# --- START FIX ---
|
||||
def rebuild_iloc_content(self, mdat_offset_delta: int, original_mdat_offset: int, original_mdat_size: int,
|
||||
meta_offset_delta: int, original_meta_offset: int, original_meta_size: int):
|
||||
"""
|
||||
使用新的 mdat 和 meta 偏移增量 (deltas) 更新此盒的 extents,
|
||||
并重建 self.raw_data
|
||||
"""
|
||||
print(f"Applying mdat delta ({mdat_offset_delta}) and meta delta ({meta_offset_delta}) to 'iloc' box...")
|
||||
content_stream = BytesIO()
|
||||
|
||||
content_stream.write(self.build_full_box_header())
|
||||
|
||||
sizes = (self.offset_size << 12) | (self.length_size << 8) | (self.base_offset_size << 4)
|
||||
if self.version == 1 or self.version == 2:
|
||||
sizes |= self.index_size
|
||||
content_stream.write(struct.pack('>H', sizes))
|
||||
|
||||
if self.version < 2:
|
||||
content_stream.write(struct.pack('>H', len(self.locations)))
|
||||
else:
|
||||
content_stream.write(struct.pack('>I', len(self.locations)))
|
||||
|
||||
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', 0))
|
||||
|
||||
if self.base_offset_size > 0:
|
||||
content_stream.write(_write_int(0, self.base_offset_size))
|
||||
|
||||
content_stream.write(struct.pack('>H', len(loc.extents)))
|
||||
|
||||
for (original_offset, length) in loc.extents:
|
||||
if (self.version == 1 or self.version == 2) and self.index_size > 0:
|
||||
content_stream.write(_write_int(0, self.index_size))
|
||||
|
||||
# !!! 魔法发生的地方 !!!
|
||||
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
|
||||
|
||||
if original_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))
|
||||
content_stream.write(_write_int(length, self.length_size))
|
||||
|
||||
self.raw_data = content_stream.getvalue()
|
||||
print(" 'iloc' box content successfully rebuilt.")
|
||||
# --- END FIX ---
|
||||
|
||||
def build_content(self) -> bytes:
|
||||
return self.raw_data
|
||||
|
||||
# --- ItemInfoEntry (DataClass) ---
|
||||
class ItemInfoEntry:
|
||||
def __init__(self, item_id, item_type, item_name):
|
||||
self.item_id = item_id
|
||||
self.type = item_type
|
||||
self.name = item_name
|
||||
self.type = item_type # 4-char code like 'hvc1'
|
||||
self.name = item_name # UTF-8 string
|
||||
def __repr__(self):
|
||||
return f"<ItemInfoEntry ID={self.item_id} type='{self.type}' name='{self.name}'>"
|
||||
|
||||
class ItemInfoBox(Box):
|
||||
# --- ItemInfoEntryBox ---
|
||||
# ('infe')
|
||||
class ItemInfoEntryBox(FullBox):
|
||||
"""代表一个 'infe' 盒"""
|
||||
def __init__(self, size: int, box_type: str, offset: int, raw_data: bytes):
|
||||
super().__init__(size, box_type, offset, raw_data)
|
||||
self.entries: list[ItemInfoEntry] = []
|
||||
self._parse_entries()
|
||||
def _parse_entries(self):
|
||||
if len(self.raw_data) < 6: return
|
||||
item_count = struct.unpack('>H', self.raw_data[4:6])[0]
|
||||
for infe_box in self.children:
|
||||
if infe_box.type == 'infe':
|
||||
if len(infe_box.raw_data) < 12: continue
|
||||
item_id = struct.unpack('>H', infe_box.raw_data[4:6])[0]
|
||||
item_type = infe_box.raw_data[8:12].decode('ascii').strip('\x00')
|
||||
item_name_bytes = infe_box.raw_data[12:]
|
||||
item_name = item_name_bytes.decode('utf-8', errors='ignore').strip('\x00')
|
||||
self.entries.append(ItemInfoEntry(item_id, item_type, item_name))
|
||||
|
||||
class PrimaryItemBox(Box):
|
||||
def __init__(self, size: int, box_type: str, offset: int, raw_data: bytes):
|
||||
super().__init__(size, box_type, offset, raw_data)
|
||||
self.item_id: int = 0
|
||||
self._parse_item_id()
|
||||
def _parse_item_id(self):
|
||||
if len(self.raw_data) < 6: return
|
||||
self.item_id = struct.unpack('>H', self.raw_data[4:6])[0]
|
||||
|
||||
class ImageSpatialExtentsBox(Box):
|
||||
def __init__(self, size: int, box_type: str, offset: int, raw_data: bytes):
|
||||
self.item_protection_index: int = 0
|
||||
self.item_type: str = "" # 4-char code
|
||||
self.item_name: str = "" # UTF-8 string
|
||||
super().__init__(size, box_type, offset, raw_data)
|
||||
self.image_width = struct.unpack('>I', self.raw_data[4:8])[0]
|
||||
self.image_height = struct.unpack('>I', self.raw_data[8:12])[0]
|
||||
|
||||
def _post_parse_initialization(self):
|
||||
stream = self.raw_data[4:]
|
||||
if not stream: return
|
||||
|
||||
pos = 0
|
||||
try:
|
||||
if self.version == 0 or self.version == 1:
|
||||
self.item_id = struct.unpack('>H', stream[pos:pos+2])[0]
|
||||
pos += 2
|
||||
self.item_protection_index = struct.unpack('>H', stream[pos:pos+2])[0]
|
||||
pos += 2
|
||||
self.item_type = ""
|
||||
name_end = stream.find(b'\x00', pos)
|
||||
if name_end == -1: name_end = len(stream)
|
||||
self.item_name = stream[pos:name_end].decode('utf-8', errors='ignore')
|
||||
|
||||
elif self.version == 2:
|
||||
# --- START FIX 3 (Parser) ---
|
||||
# 恢复为原始顺序 (ID, ProtIdx, Type) 以正确 *读取* samsung.heic
|
||||
self.item_id = struct.unpack('>I', stream[pos:pos+4])[0]
|
||||
pos += 4
|
||||
self.item_protection_index = struct.unpack('>H', stream[pos:pos+2])[0]
|
||||
pos += 2
|
||||
self.item_type = stream[pos:pos+4].decode('ascii').strip('\x00')
|
||||
pos += 4
|
||||
# --- END FIX 3 ---
|
||||
|
||||
name_end = stream.find(b'\x00', pos)
|
||||
if name_end == -1: name_end = len(stream)
|
||||
self.item_name = stream[pos:name_end].decode('utf-8', errors='ignore')
|
||||
|
||||
elif self.version == 3:
|
||||
self.item_id = struct.unpack('>H', stream[pos:pos+2])[0]
|
||||
pos += 2
|
||||
self.item_protection_index = struct.unpack('>H', stream[pos:pos+2])[0]
|
||||
pos += 2
|
||||
self.item_type = stream[pos:pos+4].decode('ascii').strip('\x00')
|
||||
pos += 4
|
||||
name_end = stream.find(b'\x00', pos)
|
||||
if name_end == -1: name_end = len(stream)
|
||||
self.item_name = stream[pos:name_end].decode('utf-8', errors='ignore')
|
||||
|
||||
except (struct.error, IndexError) as e:
|
||||
print(f"Warning: Failed to parse 'infe' box (v{self.version}). Content may be truncated. Error: {e}")
|
||||
self.item_id = 0
|
||||
self.item_type = ""
|
||||
self.item_name = ""
|
||||
|
||||
def build_content(self) -> bytes:
|
||||
content = BytesIO()
|
||||
content.write(self.build_full_box_header())
|
||||
|
||||
item_name_bytes_to_write = self.item_name.encode('utf-8') + b'\x00'
|
||||
|
||||
if self.version == 0 or self.version == 1:
|
||||
content.write(struct.pack('>H', self.item_id))
|
||||
content.write(struct.pack('>H', self.item_protection_index))
|
||||
content.write(item_name_bytes_to_write)
|
||||
|
||||
elif self.version == 2:
|
||||
# --- START FIX 4 (Builder) ---
|
||||
# 保持为苹果顺序 (ID, Type, ProtIdx) 以正确 *写入* 兼容文件
|
||||
content.write(struct.pack('>I', self.item_id))
|
||||
content.write(self.item_type.encode('ascii').ljust(4, b'\x00'))
|
||||
content.write(struct.pack('>H', self.item_protection_index))
|
||||
# --- END FIX 4 ---
|
||||
content.write(item_name_bytes_to_write)
|
||||
|
||||
elif self.version == 3:
|
||||
content.write(struct.pack('>H', self.item_id))
|
||||
content.write(struct.pack('>H', self.item_protection_index))
|
||||
content.write(self.item_type.encode('ascii').ljust(4, b'\x00'))
|
||||
content.write(item_name_bytes_to_write)
|
||||
|
||||
return content.getvalue()
|
||||
|
||||
# --- ItemInfoBox ---
|
||||
# ('iinf')
|
||||
class ItemInfoBox(FullBox):
|
||||
def __init__(self, size: int, box_type: str, offset: int, raw_data: bytes):
|
||||
self.entries: list[ItemInfoEntry] = []
|
||||
self.item_count: int = 0
|
||||
super().__init__(size, box_type, offset, raw_data)
|
||||
|
||||
def _post_parse_initialization(self):
|
||||
for child_box in self.children:
|
||||
if isinstance(child_box, ItemInfoEntryBox):
|
||||
self.entries.append(
|
||||
ItemInfoEntry(child_box.item_id, child_box.item_type, child_box.item_name)
|
||||
)
|
||||
|
||||
stream = self.raw_data[4:]
|
||||
try:
|
||||
if self.version == 0:
|
||||
if len(stream) < 2: return
|
||||
self.item_count = struct.unpack('>H', stream[0:2])[0]
|
||||
else:
|
||||
if len(stream) < 4: return
|
||||
self.item_count = struct.unpack('>I', stream[0:4])[0]
|
||||
except struct.error:
|
||||
print("Warning: Could not parse 'iinf' header. Content may be truncated.")
|
||||
|
||||
def build_content(self) -> bytes:
|
||||
header = BytesIO()
|
||||
header.write(self.build_full_box_header())
|
||||
|
||||
infe_children = [c for c in self.children if c.type == 'infe']
|
||||
|
||||
if self.version == 0:
|
||||
header.write(struct.pack('>H', len(infe_children)))
|
||||
else:
|
||||
header.write(struct.pack('>I', len(infe_children)))
|
||||
|
||||
# --- START FIX 2 ---
|
||||
# 之前是: children_data = super().build_content()
|
||||
# 我们需要调用 Box.build_content (祖父级)
|
||||
children_data = super(FullBox, self).build_content()
|
||||
# --- END FIX 2 ---
|
||||
|
||||
return header.getvalue() + children_data
|
||||
|
||||
# --- PrimaryItemBox ---
|
||||
# ('pitm')
|
||||
class PrimaryItemBox(FullBox):
|
||||
def __init__(self, size: int, box_type: str, offset: int, raw_data: bytes):
|
||||
self.item_id: int = 0
|
||||
super().__init__(size, box_type, offset, raw_data)
|
||||
|
||||
def _post_parse_initialization(self):
|
||||
stream = self.raw_data[4:]
|
||||
if not stream: return
|
||||
|
||||
try:
|
||||
if self.version == 0:
|
||||
self.item_id = struct.unpack('>H', stream[0:2])[0]
|
||||
else:
|
||||
self.item_id = struct.unpack('>I', stream[0:4])[0]
|
||||
except struct.error:
|
||||
print("Warning: Could not parse 'pitm' box.")
|
||||
|
||||
def build_content(self) -> bytes:
|
||||
content = BytesIO()
|
||||
content.write(self.build_full_box_header())
|
||||
if self.version == 0:
|
||||
content.write(struct.pack('>H', self.item_id))
|
||||
else:
|
||||
content.write(struct.pack('>I', self.item_id))
|
||||
return content.getvalue()
|
||||
|
||||
# --- ImageSpatialExtentsBox ---
|
||||
# ('ispe')
|
||||
class ImageSpatialExtentsBox(FullBox):
|
||||
def __init__(self, size: int, box_type: str, offset: int, raw_data: bytes):
|
||||
self.image_width: int = 0
|
||||
self.image_height: int = 0
|
||||
super().__init__(size, box_type, offset, raw_data)
|
||||
|
||||
def _post_parse_initialization(self):
|
||||
stream = self.raw_data[4:]
|
||||
if len(stream) < 8: return
|
||||
try:
|
||||
self.image_width = struct.unpack('>I', stream[0:4])[0]
|
||||
self.image_height = struct.unpack('>I', stream[4:8])[0]
|
||||
except struct.error:
|
||||
print("Warning: Could not parse 'ispe' box.")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ImageSpatialExtentsBox width={self.image_width} height={self.image_height}>"
|
||||
|
||||
def build_content(self) -> bytes:
|
||||
content = BytesIO()
|
||||
content.write(self.build_full_box_header())
|
||||
content.write(struct.pack('>I', self.image_width))
|
||||
content.write(struct.pack('>I', self.image_height))
|
||||
return content.getvalue()
|
||||
|
||||
# --- ItemPropertyAssociationEntry (DataClass) ---
|
||||
class ItemPropertyAssociationEntry:
|
||||
def __init__(self, item_id, association_count):
|
||||
self.item_id = item_id
|
||||
@@ -147,44 +399,79 @@ class ItemPropertyAssociationEntry:
|
||||
def __repr__(self):
|
||||
return f"<ItemPropertyAssociationEntry item_id={self.item_id} associations={self.associations}>"
|
||||
|
||||
class ItemPropertyAssociationBox(Box):
|
||||
# --- ItemPropertyAssociationBox ---
|
||||
# ('ipma')
|
||||
class ItemPropertyAssociationBox(FullBox):
|
||||
def __init__(self, size: int, box_type: str, offset: int, raw_data: bytes):
|
||||
super().__init__(size, box_type, offset, raw_data)
|
||||
self.entries: dict[int, ItemPropertyAssociationEntry] = {}
|
||||
super().__init__(size, box_type, offset, raw_data)
|
||||
|
||||
def _post_parse_initialization(self):
|
||||
self._parse_associations()
|
||||
|
||||
def _parse_associations(self):
|
||||
stream = self.raw_data
|
||||
version_flags = struct.unpack('>I', stream[:4])[0]
|
||||
version = version_flags >> 24
|
||||
flags = version_flags & 0xFFFFFF
|
||||
entry_count = struct.unpack('>I', stream[4:8])[0]
|
||||
pos = 8
|
||||
item_id_size = 4 if version >= 1 else 2
|
||||
is_large_property_index = (flags & 1) == 1
|
||||
stream = self.raw_data[4:]
|
||||
if len(stream) < 4: return
|
||||
|
||||
try:
|
||||
entry_count = struct.unpack('>I', stream[0:4])[0]
|
||||
pos = 4
|
||||
item_id_size = 4 if self.version >= 1 else 2
|
||||
is_large_property_index = (self.flags & 1) == 1
|
||||
|
||||
for _ in range(entry_count):
|
||||
if pos + item_id_size > len(stream): break
|
||||
item_id = struct.unpack('>I' if item_id_size == 4 else '>H', stream[pos:pos+item_id_size])[0]
|
||||
item_id = _read_int(stream, pos, item_id_size)
|
||||
pos += item_id_size
|
||||
|
||||
if pos + 1 > len(stream): break
|
||||
association_count = stream[pos]
|
||||
pos += 1
|
||||
|
||||
entry = ItemPropertyAssociationEntry(item_id, association_count)
|
||||
for __ in range(association_count):
|
||||
prop_size = 2 if is_large_property_index else 1
|
||||
if pos + prop_size > len(stream): break
|
||||
assoc_value = struct.unpack('>H' if prop_size == 2 else '>B', stream[pos:pos+prop_size])[0]
|
||||
assoc_value = _read_int(stream, pos, prop_size)
|
||||
property_index = assoc_value & (0x7FFF if prop_size == 2 else 0x7F)
|
||||
pos += prop_size
|
||||
if property_index > 0:
|
||||
entry.associations.append(property_index)
|
||||
self.entries[item_id] = entry
|
||||
except struct.error:
|
||||
print("Warning: Failed to parse 'ipma' box. Content may be truncated.")
|
||||
|
||||
def build_content(self) -> bytes:
|
||||
content = BytesIO()
|
||||
content.write(self.build_full_box_header())
|
||||
|
||||
content.write(struct.pack('>I', len(self.entries)))
|
||||
|
||||
item_id_size = 4 if self.version >= 1 else 2
|
||||
is_large_property_index = (self.flags & 1) == 1
|
||||
|
||||
for item_id, entry in self.entries.items():
|
||||
content.write(_write_int(item_id, item_id_size))
|
||||
content.write(struct.pack('>B', len(entry.associations)))
|
||||
|
||||
for assoc_index in entry.associations:
|
||||
prop_size = 2 if is_large_property_index else 1
|
||||
content.write(_write_int(assoc_index, prop_size))
|
||||
|
||||
return content.getvalue()
|
||||
|
||||
# --- ItemPropertyContainerBox ---
|
||||
# ('ipco')
|
||||
class ItemPropertyContainerBox(Box):
|
||||
def _post_parse_initialization(self):
|
||||
pass
|
||||
|
||||
# --- ItemPropertiesBox ---
|
||||
# ('iprp')
|
||||
class ItemPropertiesBox(Box):
|
||||
def _post_parse_initialization(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def ipco(self) -> ItemPropertyContainerBox | None:
|
||||
for child in self.children:
|
||||
@@ -196,6 +483,7 @@ class ItemPropertiesBox(Box):
|
||||
if isinstance(child, ItemPropertyAssociationBox): return child
|
||||
return None
|
||||
|
||||
# --- ItemReferenceEntry (DataClass) ---
|
||||
class ItemReferenceEntry:
|
||||
def __init__(self, from_id, to_ids):
|
||||
self.from_item_id = from_id
|
||||
@@ -203,52 +491,66 @@ class ItemReferenceEntry:
|
||||
def __repr__(self):
|
||||
return f"<ItemReferenceEntry from={self.from_item_id} to={self.to_item_ids}>"
|
||||
|
||||
class ItemReferenceBox(Box):
|
||||
# --- ItemReferenceBox ---
|
||||
# ('iref')
|
||||
class ItemReferenceBox(FullBox):
|
||||
def __init__(self, size: int, box_type: str, offset: int, raw_data: bytes):
|
||||
super().__init__(size, box_type, offset, raw_data)
|
||||
# 结构: {'thmb': {from_id: [to_id, ...]}, 'dimg': {from_id: [to_id, ...]}}
|
||||
self.references: dict[str, dict[int, list[int]]] = {}
|
||||
self._parse_references()
|
||||
super().__init__(size, box_type, offset, raw_data)
|
||||
|
||||
def _parse_references(self):
|
||||
stream = self.raw_data
|
||||
version_flags = struct.unpack('>I', stream[:4])[0]
|
||||
version = version_flags >> 24
|
||||
pos = 4
|
||||
item_id_size = 4 if version == 1 else 2
|
||||
while pos < len(stream):
|
||||
if pos + 8 > len(stream): break # 需要空间来读取 size 和 type
|
||||
ref_box_size = struct.unpack('>I', stream[pos:pos+4])[0]
|
||||
ref_box_type = stream[pos+4:pos+8].decode('ascii', errors='ignore')
|
||||
def _post_parse_initialization(self):
|
||||
self._parse_references_from_children()
|
||||
|
||||
if pos + ref_box_size > len(stream): break
|
||||
def _parse_references_from_children(self):
|
||||
item_id_size = 4 if self.version == 1 else 2
|
||||
|
||||
# 为这种引用类型创建字典 (如果它还不存在)
|
||||
if ref_box_type not in self.references:
|
||||
for ref_box in self.children:
|
||||
ref_box_type = ref_box.type
|
||||
self.references[ref_box_type] = {}
|
||||
|
||||
if item_id_size == 4:
|
||||
if pos + 12 > len(stream): break
|
||||
from_item_id = struct.unpack('>I', stream[pos+8:pos+12])[0]
|
||||
ref_count_pos = pos + 12
|
||||
else:
|
||||
if pos + 10 > len(stream): break
|
||||
from_item_id = struct.unpack('>H', stream[pos+8:pos+10])[0]
|
||||
ref_count_pos = pos + 10
|
||||
stream = ref_box.raw_data[4:]
|
||||
pos = 0
|
||||
|
||||
if ref_count_pos + 2 > len(stream): break
|
||||
reference_count = struct.unpack('>H', stream[ref_count_pos:ref_count_pos+2])[0]
|
||||
to_ids_pos = ref_count_pos + 2
|
||||
if pos + item_id_size > len(stream):
|
||||
print(f"Warning: Truncated 'iref' child box '{ref_box_type}'. Skipping.")
|
||||
continue
|
||||
|
||||
try:
|
||||
if item_id_size == 4:
|
||||
from_item_id = struct.unpack('>I', stream[pos:pos+4])[0]
|
||||
pos += 4
|
||||
else:
|
||||
from_item_id = struct.unpack('>H', stream[pos:pos+2])[0]
|
||||
pos += 2
|
||||
|
||||
if pos + 2 > len(stream):
|
||||
print(f"Info: 'iref' child box '{ref_box_type}' for ID {from_item_id} has no references. Skipping.")
|
||||
self.references[ref_box_type][from_item_id] = []
|
||||
continue
|
||||
|
||||
reference_count = struct.unpack('>H', stream[pos:pos+2])[0]
|
||||
pos += 2
|
||||
except struct.error as e:
|
||||
print(f"Error parsing 'iref' child box '{ref_box_type}': {e}. Skipping.")
|
||||
continue
|
||||
|
||||
to_item_ids = []
|
||||
for _ in range(reference_count):
|
||||
if to_ids_pos + item_id_size > len(stream): break
|
||||
if item_id_size == 4:
|
||||
to_id = struct.unpack('>I', stream[to_ids_pos:to_ids_pos+4])[0]
|
||||
else:
|
||||
to_id = struct.unpack('>H', stream[to_ids_pos:to_ids_pos+2])[0]
|
||||
if pos + item_id_size > len(stream): break
|
||||
to_id = _read_int(stream, pos, item_id_size)
|
||||
to_item_ids.append(to_id)
|
||||
to_ids_pos += item_id_size
|
||||
pos += item_id_size
|
||||
|
||||
self.references[ref_box_type][from_item_id] = to_item_ids
|
||||
pos += ref_box_size
|
||||
|
||||
def build_content(self) -> bytes:
|
||||
header = BytesIO()
|
||||
header.write(self.build_full_box_header())
|
||||
|
||||
# --- START FIX 2 ---
|
||||
# 之前是: children_data = super().build_content()
|
||||
# 我们需要调用 Box.build_content (祖父级)
|
||||
children_data = super(FullBox, self).build_content()
|
||||
# --- END FIX 2 ---
|
||||
|
||||
return header.getvalue() + children_data
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
# pyheic_struct/inspect_heic.py
|
||||
|
||||
import sys
|
||||
from heic_file import HEICFile
|
||||
import pprint # 用于漂亮地打印字典
|
||||
|
||||
def inspect_file(filename: str):
|
||||
print(f"--- 正在侦察: {filename} ---")
|
||||
|
||||
try:
|
||||
heic_file = HEICFile(filename)
|
||||
except Exception as e:
|
||||
print(f"CRITICAL: 文件解析失败: {e}")
|
||||
return
|
||||
|
||||
# 1. 打印厂商信息 (ftyp)
|
||||
if heic_file._ftyp_box:
|
||||
print(f"\n[ftyp] 厂商信息:")
|
||||
print(f" {heic_file._ftyp_box.raw_data}")
|
||||
|
||||
# 2. 打印主图像 ID (pitm)
|
||||
primary_id = heic_file.get_primary_item_id()
|
||||
print(f"\n[pitm] 主图像 Item ID:")
|
||||
print(f" {primary_id}")
|
||||
|
||||
# 3. 打印所有项目 (iinf)
|
||||
print(f"\n[iinf] 文件中的所有项目:")
|
||||
heic_file.list_items() # 这个方法会自己打印
|
||||
|
||||
# 4. 打印项目位置 (iloc) - 这是我们调试的关键! (已修改)
|
||||
if heic_file._iloc_box:
|
||||
print(f"\n[iloc] 项目位置 (片段) - 包含绝对偏移量:")
|
||||
locations = heic_file._iloc_box.locations
|
||||
|
||||
# 打印所有位置及其详细的 extents
|
||||
for loc in locations:
|
||||
# 打印摘要
|
||||
print(f" {loc}")
|
||||
# 打印关键的 extents (偏移量, 长度)
|
||||
if loc.extents:
|
||||
for extent in loc.extents:
|
||||
print(f" -> (offset={extent[0]}, length={extent[1]})")
|
||||
else:
|
||||
print(" -> (No extents found)")
|
||||
|
||||
# 5. 打印项目关联 (iref)
|
||||
if heic_file._iref_box:
|
||||
print(f"\n[iref] 项目关联:")
|
||||
pprint.pprint(heic_file._iref_box.references)
|
||||
|
||||
# 6. 查找 'mpvd' 盒 (三星特定)
|
||||
mpvd_box = heic_file.find_box('mpvd') #
|
||||
if mpvd_box:
|
||||
print(f"\n[mpvd] 动态照片盒 (三星):")
|
||||
print(f" 找到了 'mpvd' 盒,位于偏移量: {mpvd_box.offset}")
|
||||
else:
|
||||
print(f"\n[mpvd] 未找到 'mpvd' 盒 (这是 Apple 文件的预期行为)")
|
||||
|
||||
# --- START NEW INSPECTION ---
|
||||
# 7. 打印项目属性 (iprp / ipma) - 这是我们缺失的知识!
|
||||
if heic_file._iprp_box and heic_file._iprp_box.ipma:
|
||||
print(f"\n[ipma] 项目属性关联:")
|
||||
|
||||
ipma = heic_file._iprp_box.ipma
|
||||
|
||||
# 打印主 ID 的属性
|
||||
if primary_id and primary_id in ipma.entries:
|
||||
print(f" 主 ID ({primary_id}) 关联的属性索引:")
|
||||
pprint.pprint(ipma.entries[primary_id].associations)
|
||||
|
||||
# 尝试打印 "shifted" ID (三星)
|
||||
if primary_id:
|
||||
shifted_id = primary_id << 16
|
||||
if shifted_id in ipma.entries:
|
||||
print(f" 三星 Shifted ID ({shifted_id}) 关联的属性索引:")
|
||||
pprint.pprint(ipma.entries[shifted_id].associations)
|
||||
|
||||
print(f"\n (ipma 完整转储 - 前 5 项):")
|
||||
count = 0
|
||||
for item_id, entry in ipma.entries.items():
|
||||
if count >= 5:
|
||||
print(" ...")
|
||||
break
|
||||
print(f" - Item {item_id}: {entry.associations}")
|
||||
count += 1
|
||||
else:
|
||||
print(f"\n[ipma] 未找到 'ipma' 盒。")
|
||||
|
||||
if heic_file._iprp_box and heic_file._iprp_box.ipco:
|
||||
print(f"\n[ipco] 属性容器 (前 10 个属性盒):")
|
||||
count = 0
|
||||
for prop_box in heic_file._iprp_box.ipco.children:
|
||||
if count >= 10:
|
||||
print(" ...")
|
||||
break
|
||||
print(f" - (索引 {count+1}) : {prop_box}") # 索引是 1-based
|
||||
count += 1
|
||||
else:
|
||||
print(f"\n[ipco] 未找到 'ipco' 盒。")
|
||||
# --- END NEW INSPECTION ---
|
||||
|
||||
print(f"\n--- 侦察完毕: {filename} ---")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("用法: python inspect_heic.py <文件名>")
|
||||
print("例如: python inspect_heic.py samsung.heic")
|
||||
else:
|
||||
inspect_file(sys.argv[1])
|
||||
@@ -1,71 +1,233 @@
|
||||
# pyheic_struct/main.py
|
||||
|
||||
from heic_file import HEICFile
|
||||
import os
|
||||
import uuid
|
||||
import subprocess
|
||||
import pillow_heif # <--- (1) 导入 pillow_heif 用于转码
|
||||
from heic_file import HEICFile
|
||||
from heic_types import ItemInfoEntryBox # <--- 添加这一行
|
||||
from builder import HEICBuilder
|
||||
from handlers.base_handler import VendorHandler
|
||||
from handlers.samsung_handler import SamsungHandler
|
||||
|
||||
def analyze_and_reconstruct(filename: str):
|
||||
print(f"--- Analyzing {filename} ---")
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(filename):
|
||||
print(f"Error: File not found at {filename}")
|
||||
print("="*40 + "\n")
|
||||
def convert_samsung_to_apple(samsung_file_path: str):
|
||||
"""
|
||||
(V17) 执行一个完整的两步转码:
|
||||
1. [转码] 使用 pillow_heif 将三星的 "网格" 图像转码为一个 "扁平" 图像。
|
||||
2. [注入] 使用我们的 HEICBuilder 将 UUID 和 *正确* 的 Apple ftyp 注入到这个新的扁平文件中。
|
||||
"""
|
||||
if not os.path.exists(samsung_file_path):
|
||||
print(f"Error: File not found: {samsung_file_path}")
|
||||
return
|
||||
|
||||
heic_file = HEICFile(filename)
|
||||
base_filename = os.path.splitext(samsung_file_path)[0]
|
||||
output_heic_path = f"{base_filename}_apple_compatible.HEIC"
|
||||
output_mov_path = f"{base_filename}_apple_compatible.MOV"
|
||||
temp_flat_heic_path = f"{base_filename}_temp_flat.HEIC" # <--- 临时文件
|
||||
|
||||
# 重建主图像
|
||||
reconstructed_image = heic_file.reconstruct_primary_image()
|
||||
print(f"--- Converting {samsung_file_path} to Apple format (V17 Fix) ---")
|
||||
|
||||
if reconstructed_image:
|
||||
# 获取不带扩展名的基本文件名
|
||||
base_filename = os.path.splitext(filename)[0]
|
||||
# --- 步骤 1: 从原始三星文件中提取所有需要的数据 ---
|
||||
|
||||
print(f"Loading original file: {samsung_file_path}")
|
||||
original_heic_file = HEICFile(samsung_file_path)
|
||||
|
||||
# 1a. 提取视频数据
|
||||
video_data = original_heic_file.get_motion_photo_data()
|
||||
# (如果找不到,手动检查 mpvd)
|
||||
if not video_data:
|
||||
if not original_heic_file.handler or isinstance(original_heic_file.handler, VendorHandler):
|
||||
print("Vendor not auto-detected. Manually checking for Samsung 'mpvd' box...")
|
||||
mpvd_box = original_heic_file.find_box('mpvd')
|
||||
if mpvd_box:
|
||||
original_heic_file.handler = SamsungHandler()
|
||||
video_data = original_heic_file.get_motion_photo_data() # 再次尝试
|
||||
|
||||
# 1b. 提取图像像素 (转码网格)
|
||||
print("Reconstructing grid image from Samsung file...")
|
||||
pil_image = original_heic_file.reconstruct_primary_image()
|
||||
if not pil_image:
|
||||
print("CRITICAL: Failed to reconstruct primary image using pillow_heif.")
|
||||
return
|
||||
|
||||
# 1c. 生成 UUID
|
||||
new_content_id = str(uuid.uuid4()).upper()
|
||||
print(f"Generated ContentIdentifier: {new_content_id}")
|
||||
|
||||
# --- 步骤 2: 处理 .MOV 文件 (这部分已经可以工作) ---
|
||||
if video_data:
|
||||
print(f"Saving extracted video data to {output_mov_path}...")
|
||||
with open(output_mov_path, 'wb') as f_mov:
|
||||
f_mov.write(video_data)
|
||||
|
||||
# 1. 保存为高质量的有损格式 (JPEG)
|
||||
try:
|
||||
jpeg_filename = f"{base_filename}_reconstructed.jpg"
|
||||
reconstructed_image.save(jpeg_filename, "JPEG", quality=95)
|
||||
print(f"Successfully saved lossy JPEG to: {jpeg_filename}")
|
||||
print("Attempting to inject ContentIdentifier into .MOV file (requires exiftool)...")
|
||||
subprocess.run([
|
||||
'exiftool',
|
||||
f'-QuickTime:ContentIdentifier={new_content_id}',
|
||||
'-overwrite_original',
|
||||
output_mov_path
|
||||
], check=True, capture_output=True, text=True)
|
||||
print("Successfully injected ContentIdentifier into .MOV.")
|
||||
except Exception as e:
|
||||
print(f"Could not save JPEG file: {e}")
|
||||
|
||||
# 2. 保存为无损格式 (PNG)
|
||||
try:
|
||||
png_filename = f"{base_filename}_reconstructed.png"
|
||||
reconstructed_image.save(png_filename, "PNG")
|
||||
print(f"Successfully saved lossless PNG to: {png_filename}")
|
||||
except Exception as e:
|
||||
print(f"Could not save PNG file: {e}")
|
||||
|
||||
print(f"Warning: Could not inject ContentIdentifier into .MOV. ")
|
||||
print(f" (This is normal if 'exiftool' is not installed.) Error: {e}")
|
||||
else:
|
||||
print("Failed to reconstruct image.")
|
||||
print("Info: No motion photo data found. Only a still HEIC will be created.")
|
||||
|
||||
# 3. 尝试提取并保存缩略图
|
||||
thumbnail_data = heic_file.get_thumbnail_data()
|
||||
if thumbnail_data:
|
||||
# --- 步骤 3: [转码] 创建一个临时的、扁平的 HEIC 文件 ---
|
||||
try:
|
||||
# 缩略图数据通常是 JPEG 或 HEIC 格式
|
||||
# 我们先假设它是 JPEG 并以此后缀保存
|
||||
thumb_filename = f"{base_filename}_thumbnail.jpg"
|
||||
with open(thumb_filename, "wb") as f_thumb:
|
||||
f_thumb.write(thumbnail_data)
|
||||
print(f"Successfully saved thumbnail data to: {thumb_filename}")
|
||||
print(f" (Note: This file might be a JPEG or a small HEIC. Try opening it.)")
|
||||
print(f"Saving temporary flat HEIC file to {temp_flat_heic_path}...")
|
||||
pillow_heif.register_heif_opener()
|
||||
|
||||
# 将内存中的 PIL 图像保存为一个新的、扁平的 HEIC 文件
|
||||
pil_image.save(
|
||||
temp_flat_heic_path,
|
||||
format="HEIF",
|
||||
quality=95,
|
||||
save_as_brand="mif1" # 存为一个基础的 'mif1' 品牌
|
||||
)
|
||||
print("Successfully created temporary flat HEIC.")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Could not save thumbnail file: {e}")
|
||||
print(f"CRITICAL: Failed to save temporary HEIC file: {e}")
|
||||
if os.path.exists(temp_flat_heic_path):
|
||||
os.remove(temp_flat_heic_path)
|
||||
return
|
||||
|
||||
# --- 步骤 4: [注入] 加载扁平文件,并执行我们的元数据手术 ---
|
||||
try:
|
||||
print(f"Loading temporary flat HEIC for metadata injection...")
|
||||
flat_heic_file = HEICFile(temp_flat_heic_path)
|
||||
|
||||
# 4a. (重要!) 修改 'ftyp' 盒以 *完全匹配* 苹果 Live Photo
|
||||
if flat_heic_file._ftyp_box:
|
||||
# --- START V15 FIX ---
|
||||
# 使用从 apple.HEIC 侦察日志中提取的 *正确* 字符串
|
||||
# 它包含了我们缺失的 'MiHB' 品牌
|
||||
print("Modifying 'ftyp' box to be Apple compatible (heic, MiHB, MiHE...)...")
|
||||
apple_ftyp_raw_data = b'heic\x00\x00\x00\x00mif1MiHBMiHEMiPrmiafheictmap'
|
||||
flat_heic_file._ftyp_box.raw_data = apple_ftyp_raw_data
|
||||
flat_heic_file._ftyp_box.size = len(apple_ftyp_raw_data) + 8 # (40 + 8 = 48)
|
||||
# --- END V15 FIX ---
|
||||
else:
|
||||
print("No thumbnail data found or extracted.")
|
||||
print("Error: Temporary HEIC file has no 'ftyp' box. Aborting.")
|
||||
os.remove(temp_flat_heic_path)
|
||||
return
|
||||
|
||||
# 4b. (V12 / V17 FIX) 纠正 pillow_heif 产生的 'shifted' ID
|
||||
print("Checking for shifted IDs in temporary file (V17 Full Fix)...")
|
||||
if flat_heic_file._iinf_box and flat_heic_file._iloc_box and flat_heic_file._iprp_box:
|
||||
|
||||
# 1. 找出 iloc 中的 "正确" ID (e.g., {1, 2, 3, 4})
|
||||
# (我们假设 iloc 是正确的,因为 pillow-heif 似乎只搞错了其他盒)
|
||||
correct_ids = {loc.item_id for loc in flat_heic_file._iloc_box.locations}
|
||||
|
||||
# 2. 修复 'iinf' (infe boxes)
|
||||
iinf_children_to_fix = [
|
||||
c for c in flat_heic_file._iinf_box.children
|
||||
if isinstance(c, ItemInfoEntryBox) and (c.item_id >> 16) in correct_ids
|
||||
]
|
||||
|
||||
# 我们需要建立一个映射表, e.g., {65536: 1}
|
||||
shifted_id_map = {}
|
||||
|
||||
if iinf_children_to_fix:
|
||||
print(f" Found {len(iinf_children_to_fix)} shifted 'infe' boxes. Fixing them...")
|
||||
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]
|
||||
else:
|
||||
print(" 'infe' boxes seem correct. No shift detected.")
|
||||
|
||||
# --- START V17 FIX ---
|
||||
|
||||
# 3. 修复 'ipma' (属性)
|
||||
if flat_heic_file._iprp_box.ipma:
|
||||
ipma_entries = flat_heic_file._iprp_box.ipma.entries
|
||||
# 找出所有 "shifted" 的字典键
|
||||
keys_to_fix = [k for k in ipma_entries if k in shifted_id_map]
|
||||
|
||||
if keys_to_fix:
|
||||
print(f" Found {len(keys_to_fix)} shifted 'ipma' entries. Fixing them...")
|
||||
for shifted_key in keys_to_fix:
|
||||
correct_key = shifted_id_map[shifted_key]
|
||||
print(f" - Fixing 'ipma' key {shifted_key} -> {correct_key}")
|
||||
|
||||
# 替换字典的键 (e.g., {65536: ...} -> {1: ...})
|
||||
entry_data = ipma_entries.pop(shifted_key)
|
||||
entry_data.item_id = correct_key # 确保条目内部的 ID 也被更新
|
||||
ipma_entries[correct_key] = entry_data
|
||||
else:
|
||||
print(f" 'ipma' entries seem correct. (Keys: {list(ipma_entries.keys())})")
|
||||
|
||||
# 4. 修复 'iref' (引用)
|
||||
if flat_heic_file._iref_box:
|
||||
iref_refs = flat_heic_file._iref_box.references
|
||||
refs_fixed = 0
|
||||
for ref_type in iref_refs: # e.g., 'thmb', 'dimg'
|
||||
# 找出所有 "shifted" 的字典键
|
||||
keys_to_fix = [k for k in iref_refs[ref_type] if k in shifted_id_map]
|
||||
if keys_to_fix:
|
||||
refs_fixed += len(keys_to_fix)
|
||||
for shifted_key in keys_to_fix:
|
||||
correct_key = shifted_id_map[shifted_key]
|
||||
print(f" - Fixing 'iref' key [{ref_type}] {shifted_key} -> {correct_key}")
|
||||
# 替换字典的键
|
||||
iref_refs[ref_type][correct_key] = iref_refs[ref_type].pop(shifted_key)
|
||||
|
||||
if refs_fixed > 0:
|
||||
print(f" Fixed {refs_fixed} 'iref' entries.")
|
||||
else:
|
||||
print(" 'iref' entries seem correct.")
|
||||
|
||||
# --- END V17 FIX ---
|
||||
|
||||
# 4c. 注入 ContentIdentifier (UUID)
|
||||
if flat_heic_file.set_content_identifier(new_content_id):
|
||||
print(f"Successfully set ContentIdentifier in flat HEIC.")
|
||||
else:
|
||||
print("Error: Failed to set ContentIdentifier in flat HEIC.")
|
||||
os.remove(temp_flat_heic_path)
|
||||
return
|
||||
|
||||
# 4d. (V13 FIX) 移除与 Apple 结构冲突的项
|
||||
# (保持注释状态,因为这可能是导致损坏的原因,或者 V17 已使其不再必要)
|
||||
# items_to_remove = [2, 3, 4]
|
||||
# print(f"Applying V13 Fix: Removing conflicting items {items_to_remove}...")
|
||||
# for item_id in items_to_remove:
|
||||
# flat_heic_file.remove_item_by_id(item_id)
|
||||
|
||||
# 4e. [构建] 使用我们的 HEICBuilder 重建这个*扁平*文件
|
||||
print("Rebuilding flat HEIC with new metadata...")
|
||||
builder = HEICBuilder(flat_heic_file)
|
||||
builder.write(output_heic_path)
|
||||
|
||||
except Exception as e:
|
||||
print(f"CRITICAL: HEICBuilder failed during final rebuild: {e}")
|
||||
finally:
|
||||
# 5. 清理
|
||||
if os.path.exists(temp_flat_heic_path):
|
||||
os.remove(temp_flat_heic_path)
|
||||
print(f"Cleaned up temporary file: {temp_flat_heic_path}")
|
||||
|
||||
print(f"--- Conversion complete ---")
|
||||
print(f"New HEIC: {output_heic_path}")
|
||||
if video_data:
|
||||
print(f"New MOV: {output_mov_path}") # <-- 修复了一个小的打印错误,使其指向 .MOV
|
||||
print("\n" + "="*40 + "\n")
|
||||
|
||||
def main():
|
||||
# 确保文件路径正确
|
||||
apple_file = 'apple.HEIC'
|
||||
samsung_file = 'samsung.heic'
|
||||
|
||||
analyze_and_reconstruct(apple_file)
|
||||
analyze_and_reconstruct(samsung_file)
|
||||
convert_samsung_to_apple(samsung_file)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,12 +1,14 @@
|
||||
# pyheic_struct/parser.py
|
||||
|
||||
import struct
|
||||
from typing import List, BinaryIO
|
||||
from io import BytesIO
|
||||
|
||||
from base import Box
|
||||
from base import Box, FullBox
|
||||
from heic_types import (
|
||||
ItemLocationBox, PrimaryItemBox, ItemInfoBox, ItemPropertiesBox,
|
||||
ItemPropertyContainerBox, ItemPropertyAssociationBox, ImageSpatialExtentsBox,
|
||||
ItemReferenceBox # <-- Import the new class
|
||||
ItemReferenceBox, ItemInfoEntryBox # <-- 导入新的 'infe' 盒
|
||||
)
|
||||
|
||||
BOX_TYPE_MAP = {
|
||||
@@ -17,12 +19,15 @@ BOX_TYPE_MAP = {
|
||||
'ipco': ItemPropertyContainerBox,
|
||||
'ipma': ItemPropertyAssociationBox,
|
||||
'ispe': ImageSpatialExtentsBox,
|
||||
'iref': ItemReferenceBox, # <-- Register the new class
|
||||
'iref': ItemReferenceBox,
|
||||
'infe': ItemInfoEntryBox, # <-- 注册新的 'infe' 盒
|
||||
}
|
||||
|
||||
# 'iref' is also a container.
|
||||
# 'iinf' IS a container (它包含 'infe' 盒)
|
||||
CONTAINER_BOXES = {'meta', 'moov', 'trak', 'iprp', 'ipco', 'dinf', 'fiinf', 'ipro', 'iinf', 'iref'}
|
||||
FULL_BOXES = {'meta', 'hdlr', 'pitm', 'iinf', 'iloc', 'ipma', 'ispe', 'iref'}
|
||||
# 几乎所有语义盒都是 FullBox
|
||||
FULL_BOXES = {'meta', 'hdlr', 'pitm', 'iinf', 'iloc', 'ipma', 'ispe', 'iref', 'infe'}
|
||||
|
||||
def parse_boxes(stream: BinaryIO, max_size: int) -> List[Box]:
|
||||
"""
|
||||
@@ -59,17 +64,41 @@ def parse_boxes(stream: BinaryIO, max_size: int) -> List[Box]:
|
||||
if len(raw_data) < content_size: break
|
||||
|
||||
box_class = BOX_TYPE_MAP.get(box_type, Box)
|
||||
|
||||
if box_class == Box and box_type in FULL_BOXES:
|
||||
box_class = FullBox
|
||||
|
||||
box = box_class(size, box_type, current_offset_in_stream, raw_data)
|
||||
|
||||
if box.type in CONTAINER_BOXES:
|
||||
child_stream = BytesIO(box.raw_data)
|
||||
parse_size = len(box.raw_data)
|
||||
if box.type in FULL_BOXES:
|
||||
# Skip version/flags from the beginning of the raw_data
|
||||
|
||||
if box.is_full_box:
|
||||
# 跳过 4 字节的 version/flags
|
||||
if parse_size >= 4:
|
||||
child_stream.read(4)
|
||||
parse_size -= 4
|
||||
|
||||
# --- START FIX ---
|
||||
# 'iinf' 盒在 version/flags 之后还有一个 item_count 字段
|
||||
# 我们必须在解析它的子盒之前跳过它
|
||||
if box.type == 'iinf':
|
||||
if box.version == 0:
|
||||
if parse_size >= 2:
|
||||
child_stream.read(2) # 跳过 16-bit item_count
|
||||
parse_size -= 2
|
||||
else:
|
||||
if parse_size >= 4:
|
||||
child_stream.read(4) # 跳过 32-bit item_count
|
||||
parse_size -= 4
|
||||
# --- END FIX ---
|
||||
|
||||
box.children = parse_boxes(child_stream, parse_size)
|
||||
|
||||
# 调用钩子
|
||||
box._post_parse_initialization()
|
||||
|
||||
boxes.append(box)
|
||||
stream.seek(current_offset_in_stream + size)
|
||||
|
||||
|
||||
Executable → Regular
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.3 MiB |
@@ -0,0 +1,41 @@
|
||||
import os
|
||||
|
||||
# --- Hex Dump Utility ---
|
||||
def hexdump(data, length=16):
|
||||
"""Creates a formatted hex dump of a byte string."""
|
||||
results = []
|
||||
for i in range(0, len(data), length):
|
||||
chunk = data[i:i + length]
|
||||
|
||||
# 1. Offset
|
||||
offset = f"{i:08x} "
|
||||
|
||||
# 2. Hex values
|
||||
hex_part = " ".join(f"{b:02x}" for b in chunk)
|
||||
hex_part = hex_part.ljust(length * 3 - 1) # Pad to align
|
||||
|
||||
# 3. ASCII representation
|
||||
ascii_part = "".join(chr(b) if 32 <= b <= 126 else "." for b in chunk)
|
||||
|
||||
results.append(f"{offset}{hex_part} |{ascii_part}|")
|
||||
return "\n".join(results)
|
||||
# --- End Utility ---
|
||||
|
||||
|
||||
filename = 'samsung_apple_compatible.HEIC'
|
||||
|
||||
if not os.path.exists(filename):
|
||||
print(f"Error: File not found: {filename}")
|
||||
print("Please make sure you are running this in the same directory as the generated file.")
|
||||
else:
|
||||
try:
|
||||
with open(filename, 'rb') as f:
|
||||
# Read the first 512 bytes (enough to see the ftyp and meta headers)
|
||||
file_header_data = f.read(512)
|
||||
|
||||
print(f"--- Hex Dump for {filename} (First 512 Bytes) ---")
|
||||
print(hexdump(file_header_data))
|
||||
print("="*60)
|
||||
|
||||
except Exception as e:
|
||||
print(f"An error occurred while reading the file: {e}")
|
||||
@@ -0,0 +1,72 @@
|
||||
您完全正确,我们必须停下来复盘。
|
||||
|
||||
感谢您提供这些侦察日志。它们**完美地**揭示了我们失败的根本原因,这也是我们所有人(包括我)都忽略的一个核心知识。
|
||||
|
||||
我们之前的假设一直是错的。我们一直试图修复*偏移量*(offsets),而真正的问题在于文件的*核心结构*(core structure)。
|
||||
|
||||
---
|
||||
|
||||
### 核心发现:我们缺失的知识
|
||||
|
||||
通过并排比较 `[ipma]` (属性) 和 `[iref]` (引用) 日志,真相大白:
|
||||
|
||||
**1. Apple HEIC 是 "扁平" (Flat) 结构**
|
||||
* **`pitm` (主 ID):** `46`
|
||||
* **`iref` (引用):** `dimg` (网格/派生图像) 列表是**空**的 (`'dimg': {46: []}`)。
|
||||
* **`ipma` (属性):** 主 ID `46` **直接关联**到它自己的所有属性: `[1, 3, 5, 4]`。
|
||||
* **`ipco` (属性库):** 索引 `[1, 3, 4, 5]` 对应 `colr` (色彩), `ispe` (**5712x4284** 的*完整*尺寸), `irot` (旋转), `pixi` (像素信息)。
|
||||
* **结论:** 在 Apple 文件中,主 Item ID `46` *就是*那张 5712x4284 的图像。它是一个 "扁平" 文件。
|
||||
|
||||
**2. Samsung HEIC 是 "网格" (Grid) 结构**
|
||||
* **`pitm` (主 ID):** `49`
|
||||
* **`iref` (引用):** `dimg` 列表**不是空**的 (`'dimg': {1: [3, 4]}`)。这定义了一个网格,例如 "项目 1 是由图块 3 和 4 构成"。
|
||||
* **`ipma` (属性):** 主 ID `49` **只关联**到一个属性:`[4]`。
|
||||
* **`ipco` (属性库):** 索引 `[4]` 对应 `ispe` ( **4000x3000** 的*完整*尺寸)。
|
||||
* **结论:** 在 Samsung 文件中,主 Item ID `49` **不是图像**,它只是一个*虚拟的*“描述符”,它只负责告诉解码器“最终图像是 4000x3000”。
|
||||
* **致命缺陷:** `samsung.heic` 的 `[iloc]` 日志显示:`<ItemLocation ID=49 extents=1 total_size=8>`。
|
||||
* 这意味着主 ID `49` 的**实际数据只有 8 字节**。真正的图像数据存储在*其他* Item ID 中(如 1, 3, 4...),然后通过 `iref` 盒的 `dimg` 网格蓝图拼凑起来。
|
||||
|
||||
---
|
||||
|
||||
### 复盘:我们为什么一直失败
|
||||
|
||||
我们所有的修复(V8, V9, V10)都在做同一件事,而这件事是注定要失败的:
|
||||
|
||||
1. 我们拿了**三星的“网格”文件** (它依赖 `iref['dimg']` 来组合图像)。
|
||||
2. 我们强行给它贴上了**苹果的 `ftyp` 标签** (例如 `...MiHBMiHE...`)。这个标签告诉解码器:“这是一个**扁平**文件,请勿查找 `iref['dimg']`”。
|
||||
3. 我们正确地重建了所有偏移量,并注入了 `ContentIdentifier`。
|
||||
|
||||
**失败的精确时刻:**
|
||||
|
||||
1. Apple 的 "预览" App 打开我们的 `samsung_apple_compatible.HEIC`。
|
||||
2. 它读取了 `ftyp` (苹果标签):“好的,这是一个扁平文件。”
|
||||
3. 它读取了 `pitm`:“好的,主图像是 ID `49`。”
|
||||
4. 它读取了 `ipma`:“好的,ID `49` 的尺寸是 4000x3000。”
|
||||
5. 它去 `iloc` 查找 ID `49` 的**图像数据**。
|
||||
6. `iloc` 返回:“这是它的数据:**8 个字节**。”
|
||||
7. "预览" App 崩溃,因为它无法从 8 字节的数据中渲染出一个 4000x3000 的图像。
|
||||
|
||||
---
|
||||
|
||||
### 结论:我们面临的僵局
|
||||
|
||||
我们无法通过*修改*(modify)来解决这个问题。**我们必须*转码*(transcode)。**
|
||||
|
||||
我们试图进行的“结构手术”是不可能的,因为这两种文件格式在如何定义“主图像”上存在根本性的、不兼容的差异。
|
||||
|
||||
**我们有两个选择,但都不能完全满足最初的需求:**
|
||||
|
||||
1. **选项 A:成功创建可查看的 HEIC(但丢失动态照片链接)**
|
||||
* 我们可以使用 `heic_file.py` 中的 `reconstruct_primary_image()` 方法(它在内部使用 `pillow_heif`)。
|
||||
* 这个方法很聪明,它*可以*读取三星的 `iref['dimg']` 网格并将其在内存中拼凑成一张完整的 4000x3000 图像。
|
||||
* 然后我们可以将这张 `PIL.Image` 对象保存为一个**新的、扁平的** `output.heic` 文件。
|
||||
* **结果:** 我们会得到一个完美的 HEIC 静态图像,但它将丢失我们辛苦注入的 `ContentIdentifier` (UUID),因为 `PIL.Image.save()` 只是一个像素编码器,它不会重建我们需要的复杂 `meta` 盒。
|
||||
|
||||
2. **选项 B:保持我们的代码(但它永远无法工作)**
|
||||
* 我们当前的 `builder.py` 方案是*唯一*能够注入 `ContentIdentifier` 的方法,但它操作的*源文件*(三星网格)与我们的*目标*(苹果扁平)不兼容。
|
||||
|
||||
**简而言之:** 我们的 `builder.py` 擅长元数据(Metadata)操作,而 `pillow_heif` 擅长像素(Pixel)操作。要完成这个任务,我们**同时**需要两者,但现有的库不允许我们将 `ContentIdentifier` 注入到一个正在被转码(从网格转为扁平)的文件中。
|
||||
|
||||
我建议我们暂停对 `builder.py` 的修改,因为我们当前的偏移量计算(如 V10 日志所示)已经是**正确**的。失败的原因超出了我们的修复范围,它是一个文件*结构*的根本性不兼容。
|
||||
|
||||
实际上我的需求是:将三星的动态照片(motion photo)转换为两份文件,一份是视频一份是照片,将它们与iOS一样通过ContentIdentifier链接,使其变为可以被iOS设备读取并链接的Live Photo,我无所谓转换为静态照片的部分是否存在视频,但是我希望其可以注入ContentIdentifier,将三星的文件结构转换为iOS的文件结构
|
||||
Reference in New Issue
Block a user