From 63f148951cfab54f94e8be8b350bdee31e87b6c6 Mon Sep 17 00:00:00 2001 From: Nymiro Date: Mon, 20 Oct 2025 22:32:06 +1300 Subject: [PATCH] Implement HEIC parsing with vendor-specific handlers - Added a new parser for HEIC files that supports recursive parsing of boxes. - Introduced vendor-specific handlers for Apple and Samsung HEIC features. - Created an abstract base handler for common functionality across vendors. - Implemented methods to find motion photo offsets and handle embedded video data. - Updated the structure to accommodate additional box types and improve parsing efficiency. --- .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 815 bytes .../__pycache__/__main__.cpython-314.pyc | Bin 0 -> 342 bytes .../__pycache__/base.cpython-314.pyc | Bin 0 -> 8054 bytes .../__pycache__/builder.cpython-314.pyc | Bin 0 -> 8221 bytes pyheic_struct/__pycache__/cli.cpython-314.pyc | Bin 0 -> 2158 bytes .../__pycache__/converter.cpython-314.pyc | Bin 0 -> 10572 bytes .../__pycache__/heic_file.cpython-314.pyc | Bin 0 -> 28197 bytes .../__pycache__/heic_types.cpython-314.pyc | Bin 0 -> 43534 bytes .../__pycache__/parser.cpython-314.pyc | Bin 0 -> 3813 bytes pyheic_struct/base.py | 52 +++-------- pyheic_struct/builder.py | 51 ++++------ .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 437 bytes .../__pycache__/apple_handler.cpython-314.pyc | Bin 0 -> 1190 bytes .../__pycache__/base_handler.cpython-314.pyc | Bin 0 -> 1130 bytes .../samsung_handler.cpython-314.pyc | Bin 0 -> 1407 bytes pyheic_struct/heic_file.py | 88 ++++++------------ pyheic_struct/heic_types.py | 87 +++++++---------- pyheic_struct/parser.py | 24 ++--- 18 files changed, 103 insertions(+), 199 deletions(-) create mode 100644 pyheic_struct/__pycache__/__init__.cpython-314.pyc create mode 100644 pyheic_struct/__pycache__/__main__.cpython-314.pyc create mode 100644 pyheic_struct/__pycache__/base.cpython-314.pyc create mode 100644 pyheic_struct/__pycache__/builder.cpython-314.pyc create mode 100644 pyheic_struct/__pycache__/cli.cpython-314.pyc create mode 100644 pyheic_struct/__pycache__/converter.cpython-314.pyc create mode 100644 pyheic_struct/__pycache__/heic_file.cpython-314.pyc create mode 100644 pyheic_struct/__pycache__/heic_types.cpython-314.pyc create mode 100644 pyheic_struct/__pycache__/parser.cpython-314.pyc create mode 100644 pyheic_struct/handlers/__pycache__/__init__.cpython-314.pyc create mode 100644 pyheic_struct/handlers/__pycache__/apple_handler.cpython-314.pyc create mode 100644 pyheic_struct/handlers/__pycache__/base_handler.cpython-314.pyc create mode 100644 pyheic_struct/handlers/__pycache__/samsung_handler.cpython-314.pyc diff --git a/pyheic_struct/__pycache__/__init__.cpython-314.pyc b/pyheic_struct/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2b1d606c6f1f3e371bd751ddaeacb550bcf4171d GIT binary patch literal 815 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 literal 0 HcmV?d00001 diff --git a/pyheic_struct/__pycache__/builder.cpython-314.pyc b/pyheic_struct/__pycache__/builder.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..03be449e3537d8b6ba61c4f3d3b4dd2ab805ba55 GIT binary patch literal 8221 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 Pa|2$b7ng@9Q%?Q`f(iba literal 0 HcmV?d00001 diff --git a/pyheic_struct/__pycache__/converter.cpython-314.pyc b/pyheic_struct/__pycache__/converter.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8707e489666807384c8068548f19e96301a56296 GIT binary patch literal 10572 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 literal 0 HcmV?d00001 diff --git a/pyheic_struct/__pycache__/heic_file.cpython-314.pyc b/pyheic_struct/__pycache__/heic_file.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dc307a123c833337b7ae84a79a8dd5d8ac619b05 GIT binary patch literal 28197 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 Um|^C9fAU@~ literal 0 HcmV?d00001 diff --git a/pyheic_struct/base.py b/pyheic_struct/base.py index 9dbb9d7..57c1084 100644 --- a/pyheic_struct/base.py +++ b/pyheic_struct/base.py @@ -5,17 +5,15 @@ import struct from io import BytesIO class Box: - """ - Represents a generic ISOBMFF box. - Now supports finding children and building (writing) data back. - """ + """Generic ISOBMFF box supporting hierarchical parsing and rebuilds.""" + 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'] = [] - self.is_full_box = False # 默认不是 FullBox + self.is_full_box = False # Flag toggled by FullBox subclasses def __repr__(self) -> str: return f"" @@ -48,17 +46,12 @@ class Box: return header.getvalue() def build_content(self) -> bytes: - """ - 序列化此盒的 *内容* (不包括头部)。 - 对于一个通用的容器盒 (container boxes),它会递归构建所有子盒。 - """ + """Serialise the box payload (children included, header excluded).""" if not self.children: - # 对于一个没有子盒的简单数据盒 (如 ftyp, mdat, 或未解析的盒) - # 只需返回它持有的原始数据。 + # Leaf boxes expose their stored payload verbatim. return self.raw_data - # 对于一个容器盒 (如 'meta', 'iprp', 'ipco') - # 递归构建每一个子盒 (完整的,包括头部) 并拼接它们 + # Container boxes rebuild each child (including headers) in order. content_stream = BytesIO() for child in self.children: child_data = child.build_box() @@ -67,22 +60,17 @@ class Box: def build_box(self) -> bytes: - """ - 构建完整的盒 (头部 + 内容),并返回其二进制数据。 - """ - # 1. "自底向上" 构建内容 + """Construct the full box (header + payload) and return its bytes.""" 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) -> Optional['Box']: - """在子盒中查找指定类型的第一个盒子""" + """Return the first child matching `box_type`.""" for child in self.children: if child.type == box_type: return child @@ -93,9 +81,8 @@ class Box: return None class FullBox(Box): - """ - FullBox 是一种特殊的 Box,它在内容开头包含 4 字节的 version 和 flags - """ + """Box variant that begins with a 4-byte version/flags header.""" + 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 @@ -104,38 +91,29 @@ class FullBox(Box): self._parse_full_box_header() def _parse_full_box_header(self): - """从 raw_data 中解析 version 和 flags""" + """Extract version and flags from the stored payload.""" 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 头部""" + """Serialise the 4-byte version/flags prefix.""" version_flags = (self.version << 24) | self.flags return struct.pack('>I', version_flags) def build_content(self) -> bytes: - """ - 序列化此 FullBox 的内容。 - 它首先写入 4 字节的 version/flags,然后 - 再写入子盒 (如果是容器) 或 version/flags 之后的 - 原始数据 (如果不是容器)。 - """ + """Serialise the FullBox payload, ensuring the header is emitted.""" 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 之后的部分 + # Emit the payload following the version/flags header. if len(self.raw_data) >= 4: content_stream.write(self.raw_data[4:]) else: - # 对于一个容器 FullBox (如 'meta') - # 递归构建每一个子盒 (与 Box.build_content 相同) + # Container-style FullBoxes rebuild each descendant. for child in self.children: child_data = child.build_box() content_stream.write(child_data) diff --git a/pyheic_struct/builder.py b/pyheic_struct/builder.py index 792e359..51b8b0d 100644 --- a/pyheic_struct/builder.py +++ b/pyheic_struct/builder.py @@ -1,10 +1,10 @@ -# pyheic_struct/builder.py - import struct from .heic_file import HEICFile from .heic_types import ItemLocationBox class HEICBuilder: + """Rebuild a HEIC container after metadata edits.""" + def __init__(self, heic_file: HEICFile): self.heic_file = heic_file self.mdat_box = heic_file.get_mdat_box() @@ -18,14 +18,14 @@ class HEICBuilder: if not self.iloc_box: raise ValueError("Cannot build: 'iloc' box not found.") - # 存储 'mdat' 的 *原始* 偏移量。这是至关重要的。 + # Preserve the original `mdat` offset for delta calculations. self.original_mdat_offset = self.mdat_box.offset - - # 分离元数据和数据 + + # All non-`mdat` boxes form the metadata segment. 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' 盒的偏移增量""" + """Return the delta between original and rebuilt meta offsets.""" 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) @@ -35,7 +35,7 @@ class HEICBuilder: return new_meta_offset - original_meta_offset def _rebuild_iloc_with_delta(self, mdat_offset_delta: int): - """(V14) 辅助函数:使用给定的 delta 重建 iloc""" + """Rebuild the `iloc` box using the supplied `mdat` delta.""" print(f" ... Rebuilding 'iloc' using mdat_delta: {mdat_offset_delta}") meta_offset_delta = self._calculate_meta_offset_delta() try: @@ -49,24 +49,20 @@ class HEICBuilder: ) except Exception as e: print(f"CRITICAL: Failed to rebuild 'iloc' box content: {e}") - raise # 抛出异常以停止构建 + raise def _calculate_final_meta_size(self) -> int: - """(V14) 辅助函数:递归构建所有元数据盒以获取其最终大小""" + """Compute the size of the metadata stream after rebuild.""" 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' 重建 *之前* 计算一次大小) + """Persist the rebuilt HEIC structure to `output_path`.""" + + # Pass 1: compute a preliminary layout before touching `iloc`. print("--- Builder Pass 1: Preliminary Layout ---") preliminary_meta_size = self._calculate_final_meta_size() preliminary_mdat_offset = preliminary_meta_size @@ -76,14 +72,12 @@ class HEICBuilder: print(f"Preliminary mdat offset: {preliminary_mdat_offset}") print(f"Preliminary offset delta: {preliminary_mdat_delta}") - # --- Pass 2: 初步重建 'iloc' --- - # (使用 *初步的* delta 重建 iloc,这会改变 'iloc' 的大小) + # Pass 2: rebuild `iloc` with the preliminary delta. 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 的总大小) + # Pass 3: recalculate metadata size after the first rebuild. print("\n--- Builder Pass 3: Final Layout Calculation ---") final_meta_size = self._calculate_final_meta_size() final_mdat_offset = final_meta_size @@ -93,8 +87,7 @@ class HEICBuilder: print(f"Final mdat offset (actual): {final_mdat_offset}") print(f"Final offset delta (actual): {final_mdat_delta}") - # --- Pass 4: 最终重建 'iloc' --- - # (使用 *最终的、正确*的 delta 再次重建 iloc) + # Pass 4: optionally rebuild with the corrected delta. 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) @@ -102,28 +95,24 @@ class HEICBuilder: else: print("\n--- Builder Pass 4: Skipped (Preliminary delta was correct) ---") - # --- Pass 5: 写入文件 --- + # Pass 5: write metadata followed by `mdat`. 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 - + 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: diff --git a/pyheic_struct/handlers/__pycache__/__init__.cpython-314.pyc b/pyheic_struct/handlers/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..82bce1fe4d3171f2181d3841c45b39ae1fc75cf1 GIT binary patch literal 437 zcmZ8b&r8EF6i(Xi2Xmb_uPb^dcB)61PB`mLxsQllb5GCmHaf zco)Wwg2`4Dd?YX5_q~_zz0tt$0TVZ0i|N9=@0R?8(}4M|0goUB3f#gJCNOrCGql)A z+}Kmz61zz&ZmaeXj6vGEfU1*vn;>jI>|9e$O0g{~irGEG!8GF}r$Pp`q$D7ZOTxNx=`J_?KLs!^+g=dp%QrOiUgh+`I(tc*$ zP&0YisQY)B{FCety)IXtzm3++x@38d&@-4D3iMfYCC&N~pA}4$(MgHxf^sDzToM{p zvngd5NhNBm{;_7hj1Xd+DTKz=On3K-lGHgp6uxnX=E@C|K3xdm3)uSrTd($v-g*a5 I9kFIg-*a$(N&o-= literal 0 HcmV?d00001 diff --git a/pyheic_struct/handlers/__pycache__/apple_handler.cpython-314.pyc b/pyheic_struct/handlers/__pycache__/apple_handler.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c65136a7b4cf6e0342dd0ed95f8a2c7d6ae24765 GIT binary patch literal 1190 zcmZ`&&rcIU6rSm}rIxm61jK}dQ6tcZ?LlHZpwXxaB!aTU9#oi2cc#q@9nipxrkuB8~ka%&LH%| zC`T)o3WHe?`e*~YcnK|Hji=BUx`f=!1>`#Gc-~3;bjHnILDk$Y2EB^oa)U>{kc%wx zLoU5Em^~nqU4xr&Fo?a5G%e|i=iIE$H;^83bC4$= zm+p1DAy4D)ye}q_BG!Ceb-5R`0*@>{T)IzMoavtAs#dhOGm3PC4n#z{ZJ~uCvyXyJ zPM%n$N|@wCX(72!f`~Apc$Z10Iaz+(Agv(eb44Y{7AIRGn$cj1e8M8aJ5BEU@Mtse zxgc#Wd9COfILv33fy#V?8f*)=j~ZwrbL$mCK3FHwDsBVFYxp^UJ2wO_>8fLB*x5C( z#d*o0lV}AHc7jM(GjYBpWQXavOsN+#rToCtlq%zWxgWdrr;1BekG4C36!izf>vedf zRoxRluXnfGJn*Q}vghejT03D*)tSMWrfRLb9hWF&Q6w}#LTP;QBoDQdSHn<(nt<)F zj}DNN8{IS8$FwM_rc4Ct6pyuz*i48d(8e)wHG^udD-&F^iifTEj0MP$ZV{cHmL;exo-m}- zWQeF8A}U%f#dUo3A2|N!W}&Ir9)A|fUn=Ztq4{m(yc_}32eeWx#05$t)-eDVDJ1fH zzyUSTzwEIvy&0jj6-cGSAmWjL>mf>g;ZZ6_O_5_T20#)J%(Rw4v!}TNr*kI;#p7W} zX%$ObkU{vaZBUxO-)@ExWx^f40YdgN*<9hE0yOR$E^WQmZKdkOr6yCygCfntB+R&x mOlJvpg@vS@J8}waCZ~=6%fT3bM>D^(!+7-V?R^A?ef1aJ(?RM0 literal 0 HcmV?d00001 diff --git a/pyheic_struct/handlers/__pycache__/base_handler.cpython-314.pyc b/pyheic_struct/handlers/__pycache__/base_handler.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e2103e28a46e8d7fa9d1fe9cf8b7bb6d43f4d1ad GIT binary patch literal 1130 zcmZ`&&ubGw6rR~1N!&JCA*H1jJ64GXHSyrVg9uWq2|}<-EqG{$&F*Zou-S<-vy}u7 zIf(y@H;?`;3q5&|-dck6r1)lc3s!I;Gw+)>GvD{VdAWbJ?jZP9fBhW(#t41a;Nr82 z0MGjn%+LXHaSwH|!W%j^dPdh&<_6j~@0KTmDnje%z;Ml;)wPwqftJxN;^=L4yP=PNSs zBqM(8Nl8L3$WgxUp&TqY{nMXz#{@Vh5X{gX)cxcWLIJF;qa8fN zE_T5Q*XkHRZl7q$*_9!Sd>TSI!q&JVm`X)5b)rO(kEG8N_C>o=0Tm-pNlmty?R4Hs zCS)g>j3U81FSwtMSfXUd=K<@CC;6pNN$RVA(6A^~c65QXMD6h;Tcor!o>7{uT{5Qq zcWwa`$Tb*d=p0q-#+e?z-aFOPw`q>%kVSs^Bs$JL0w3!03V<2(4LD#-$Ih;Zm?Vmm zFiHZVhKvj*iV+@$k|~(-B{mwcAYcI*aorVUJmhfVC<+*FJ8te5+yQ6HV^IiimL|ai z!eLc>h~O!;k!`0h7Jrh|`(pbmf{QK04%gi+j-5{ijql6_K(p5?W?o%on zTH!LKQvW%8>M13nL8_R9oGz>2(sc-}ARlxfwT92=%rNZQdFA58irqS|m4Tixg~kIt z0^SMwP)){BawseSxleg}78hS8%W6-HQl5#Muvs^#yaB_hi7`G#o5zU!Fqe+4JLi^z K8*>Ch&ix1NIwm9l literal 0 HcmV?d00001 diff --git a/pyheic_struct/handlers/__pycache__/samsung_handler.cpython-314.pyc b/pyheic_struct/handlers/__pycache__/samsung_handler.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..79e21b867754d5272ed47455553065ffbc0a359e GIT binary patch literal 1407 zcmZ`(&u<$=6rS;}oo(vaBzB>qpbkWhYmwR#M3h5BDix#ziKyLx4?r^7?98$&c6ZFo zxR9KDK!PJ25Z4~KaO8kgPW%h5T7PJU>3J~5Xb-!13vFVJ&!SW4IpkKJLaw!o*C5Y`uDJFx>Q?V! zm~?TrFyJW@a??v$%%z{ll&VsW)0e^Up!P1(*o6vvmbRWm0=Zb*1N0I$d&G`wfqp_) zU0c@%NYA-d*r%2?cDzK5()Y_94lfsmBIWu8HROI2L_XPked{s_xTi;wt5p(5A8?{W zPI$7<8RLv3!)uJ}i;sK0Y2JKB%)!hz?SWdyV>Cc}l~+DNh{5VJ=qe5Yk{$dmK(RUp zA*J*&v}fHlhs^nULC7PYDlJF8J~j&roK;^HsL*nuH$2GdlzM3@w5K_x*_nUi z);sE~K^y8cs7GV;5H+gpKaBcz=f>K1@8sqbe8`_&pqbe40Uq_~*I)of_{t1F<1NL- zYvt;&GN^RQRkmtye9vH<_O~n_t!n*~TTt|HeSU{~ z(hs>Ja2+`pI)(!sLjv+q82KUbMXJ3hRk`&WBI___r}Gkt(2+>#FcezQqDIy^cKMGB%!#lCwZ`aP9krkRsdesV z>)iF=R_nrN&aWq1Ki3y0?Ta^@ADZ7cCl@y-OIx?=Z~WFe^VP-|8`rP?*jky?SMJx) ziN>U{aof3cr`eKCsP+xo?mC%6Y3e10i}REwf{kKh14};~d2#7!QW`{3={QPxD!@HQ zDHA@W($F(I1#brU46nh{T1J2;R}i##1Rjw$V8c@h$oq2Hw(9cg}SxiXlkB~Xt( zg>&Aq-+X8T+aDOp=iTN(=1S`Op5n6?B~4mUa}}^V>&0PSmCLYZc2sRp_biO@ TFQ{|hUc~LscOM`a@~poAY&L0+ literal 0 HcmV?d00001 diff --git a/pyheic_struct/heic_file.py b/pyheic_struct/heic_file.py index 56386f1..b4cdf9c 100644 --- a/pyheic_struct/heic_file.py +++ b/pyheic_struct/heic_file.py @@ -1,8 +1,5 @@ -# pyheic_struct/heic_file.py - import os import math -# 1. 导入新的库 import pillow_heif from io import BytesIO from dataclasses import dataclass @@ -27,6 +24,8 @@ class Grid: output_height: int class HEICFile: + """High-level accessor for parsed HEIC/HEIF structures.""" + def __init__(self, filepath: str): self.filepath = filepath pillow_heif.register_heif_opener() @@ -38,7 +37,7 @@ class HEICFile: self._iprp_box: ItemPropertiesBox | None = None self._iref_box: ItemReferenceBox | None = None self.handler: VendorHandler | None = None - self.boxes: list[Box] = [] # 顶层盒子 + self.boxes: list[Box] = [] # Top-level boxes try: with open(self.filepath, 'rb') as f: @@ -51,7 +50,7 @@ class HEICFile: raise def _find_essential_boxes(self, boxes: list[Box]): - """在解析树中查找关键盒子的快捷方式""" + """Populate shortcut pointers for commonly used boxes.""" for box in boxes: if isinstance(box, ItemLocationBox): self._iloc_box = box if isinstance(box, ItemInfoBox): self._iinf_box = box @@ -61,10 +60,8 @@ class HEICFile: if box.type == 'ftyp': self._ftyp_box = box if box.children: self._find_essential_boxes(box.children) - # --- 辅助方法 (用于修改和构建) --- - def find_box(self, box_type: str, root_box_list: list[Box] | None = None) -> Box | None: - """递归查找第一个匹配类型的盒子""" + """Recursively locate the first box with the requested fourcc.""" if root_box_list is None: root_box_list = self.boxes @@ -78,14 +75,14 @@ class HEICFile: return None def get_mdat_box(self) -> Box | None: - """获取顶层的 'mdat' 盒""" + """Return the top-level `mdat` box, if present.""" 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""" + """Internal helper used by `remove_box_by_type`.""" for i, box in enumerate(box_list): if box.type == box_type: box_list.pop(i) @@ -96,19 +93,14 @@ class HEICFile: return False def remove_box_by_type(self, box_type: str) -> bool: - """递归查找并移除第一个匹配类型的盒子""" + """Remove the first box matching `box_type`, searching recursively.""" 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 索引。 - """ + """Fully remove an item from iinf/iloc/ipma/iref and clean orphaned properties.""" print(f"Attempting to remove Item ID {item_id_to_remove} from all references (V16)...") - - # 1. 从 iinf (Item Info) 中删除 + + # Update iinf metadata if self._iinf_box: self._iinf_box.children = [ c for c in self._iinf_box.children @@ -120,7 +112,7 @@ class HEICFile: ] print(f" - Removed from 'iinf' box.") - # 2. 从 iloc (Item Location) 中删除 + # Update iloc entries if self._iloc_box: self._iloc_box.locations = [ loc for loc in self._iloc_box.locations @@ -128,7 +120,7 @@ class HEICFile: ] print(f" - Removed from 'iloc' box.") - # 3. (新) 从 iref (Item Reference) 中删除 + # Update iref relationships if self._iref_box: for i in range(len(self._iref_box.children) - 1, -1, -1): ref_box = self._iref_box.children[i] @@ -153,7 +145,7 @@ class HEICFile: ] print(f" - Cleaned 'iref.references' of to_id {item_id_to_remove}.") - # 4. 从 ipma 和 ipco (属性) 中删除 + # Update property associations and definitions if self._iprp_box and self._iprp_box.ipma and self._iprp_box.ipco: ipma = self._iprp_box.ipma ipco = self._iprp_box.ipco @@ -162,18 +154,17 @@ class HEICFile: print(f" - Item {item_id_to_remove} not in 'ipma'. No properties to clean.") return - # 4a. 找到要删除的属性索引 (1-based) + # Identify property indices referenced exclusively by the removed item. props_to_remove = { assoc.property_index for assoc in ipma.entries[item_id_to_remove].associations } - # 4b. 从 ipma 中删除该 Item + # Remove the item entry itself. del ipma.entries[item_id_to_remove] print(f" - Removed Item {item_id_to_remove} from 'ipma'.") - # 4c. 确定哪些属性是“孤儿” - # (即,它们在 props_to_remove 中,但*不在*任何*剩余*的 item 关联中) + # Determine which properties are now unused. all_remaining_props = set() for entry in ipma.entries.values(): all_remaining_props.update( @@ -188,16 +179,12 @@ class HEICFile: print(f" - Found orphaned properties to remove from 'ipco': {orphaned_props}") - # 4d. 创建一个“重映射表” - # 我们从后往前遍历,以安全地删除 - # (索引是 1-based, 但列表是 0-based) + # Remove orphaned properties from `ipco`, iterating from the end to keep indices valid. orphaned_indices_0based = sorted([p - 1 for p in orphaned_props], reverse=True) - # 原始 1-based 索引 -> 新 1-based 索引 - remap_table = {} + 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) @@ -205,8 +192,6 @@ class HEICFile: 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...") @@ -220,7 +205,6 @@ class HEICFile: current_new_index += 1 current_old_index += 1 - # 4g. 应用重映射 for item_id, entry in ipma.entries.items(): new_associations = [] for assoc in entry.associations: @@ -229,19 +213,15 @@ class HEICFile: assoc.property_index = remap_table[old_prop_index] new_associations.append(assoc) # else: - # 该属性已被删除 (不应发生,因为我们只删除了孤儿) + # The property was removed entirely (should not occur). # 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 - """ + """Locate the primary `infe` entry and update its item name with a UUID.""" primary_id = self.get_primary_item_id() if not primary_id: print("Error: Cannot find primary item ID.") @@ -253,9 +233,6 @@ class HEICFile: 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)] @@ -269,16 +246,14 @@ class HEICFile: 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' 盒 + + # Update both the parsed box and the cached entry representation. 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 @@ -288,12 +263,8 @@ class HEICFile: 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 重建主图像,它能自动处理网格图像。 - """ + """Reconstruct the primary image using pillow-heif (handles grid tiles).""" try: print("Reconstructing primary image using pillow-heif...") image = Image.open(self.filepath) @@ -326,11 +297,10 @@ class HEICFile: if not primary_id: return None if not self._iref_box: return None - # 尝试直接匹配 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 + # Fall back to Samsung-style shifted IDs. 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.") @@ -343,17 +313,14 @@ class HEICFile: 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 @@ -393,15 +360,13 @@ class HEICFile: location = next((loc for loc in self._iloc_box.locations if loc.item_id == target_id), None) if not location: - # 尝试检查 "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: @@ -450,7 +415,6 @@ class HEICFile: target_id = primary_id if primary_id not in self._iref_box.references['thmb']: - # 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.") diff --git a/pyheic_struct/heic_types.py b/pyheic_struct/heic_types.py index 1cf0ed6..59d7c46 100644 --- a/pyheic_struct/heic_types.py +++ b/pyheic_struct/heic_types.py @@ -1,11 +1,9 @@ -# pyheic_struct/heic_types.py - import struct from .base import Box, FullBox from io import BytesIO -from typing import List, Optional # <-- 添加 List, Optional 导入 +from typing import List, Optional -# --- 帮助函数 --- +# Helper functions --------------------------------------------------------- def _read_int(data: bytes, pos: int, size: int) -> int: """Helper to read an integer of variable size.""" @@ -26,24 +24,26 @@ def _write_int(value: int, size: int) -> bytes: if size == 8: return struct.pack('>Q', value) return b'' -# --- ItemLocation --- +# ItemLocation ------------------------------------------------------------- class ItemLocation: + """Represents a single `iloc` entry with all associated extents.""" + 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 = [] + # Stored as (relative_offset, length) + self.raw_extents = [] + # Stored as (absolute_offset, length) + self.extents = [] def __repr__(self): total_length = sum(ext[1] for ext in self.extents) return f"" -# --- ItemLocationBox --- -# ('iloc') +# ItemLocationBox (`iloc`) ------------------------------------------------- class ItemLocationBox(FullBox): def __init__(self, size: int, box_type: str, offset: int, raw_data: bytes): self.locations: List[ItemLocation] = [] @@ -122,13 +122,9 @@ class ItemLocationBox(FullBox): 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 - """ + """Rebuild the `iloc` payload after `mdat` or `meta` offsets change.""" print(f"Applying mdat delta ({mdat_offset_delta}) and meta delta ({meta_offset_delta}) to 'iloc' box...") content_stream = BytesIO() @@ -191,9 +187,9 @@ class ItemLocationBox(FullBox): new_absolute_offset = _adjust_absolute_offset(original_absolute_offset) if original_absolute_offset == 0: - new_absolute_offset = 0 # 保持 0 偏移量 - - # 最后的安全检查,防止打包负数 + new_absolute_offset = 0 + + # Guard against negative offsets produced by delta adjustments. 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 @@ -222,12 +218,11 @@ class ItemLocationBox(FullBox): 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) --- +# ItemInfoEntry ------------------------------------------------------------ class ItemInfoEntry: def __init__(self, item_id, item_type, item_name): self.item_id = item_id @@ -236,10 +231,9 @@ class ItemInfoEntry: def __repr__(self): return f"" -# --- ItemInfoEntryBox --- -# ('infe') +# ItemInfoEntryBox (`infe`) ----------------------------------------------- class ItemInfoEntryBox(FullBox): - """代表一个 'infe' 盒""" + """Parse and emit `infe` (ItemInfoEntryBox) structures.""" def __init__(self, size: int, box_type: str, offset: int, raw_data: bytes): self.item_id: int = 0 self.item_protection_index: int = 0 @@ -261,7 +255,7 @@ class ItemInfoEntryBox(FullBox): pos += 2 self.item_protection_index = struct.unpack('>H', stream[pos:pos+2])[0] pos += 2 - self.item_type = "" + 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') @@ -290,26 +284,26 @@ class ItemInfoEntryBox(FullBox): candidate_type = stream[pos+2:pos+6] if candidate_protection <= 0x00FF and b'\x00' not in candidate_type: - # 标准顺序: 2 字节保护索引 + 4 字节类型 + # Standard ordering: 2-byte protection index followed by the 4CC. self.item_protection_index = candidate_protection self._has_protection_field = True pos += 2 type_bytes = candidate_type pos += 4 else: - # 三星文件: 直接写入类型 (如 'hvc1'),缺少保护索引 + # Samsung files often omit the protection index and write only the 4CC. self.item_protection_index = 0 self._has_protection_field = False type_bytes = stream[pos:pos+4] pos += 4 elif remaining >= 4: - # 某些三星文件缺少 2 字节的保护索引字段, 直接紧跟 4 字节类型 + # Some vendor variants omit the 2-byte protection field entirely. self.item_protection_index = 0 self._has_protection_field = False type_bytes = stream[pos:pos+4] pos += 4 else: - # 无法获得完整的 4 字节类型 + # Truncated data: fall back to an empty type. type_bytes = b'' self.item_type = type_bytes.decode('ascii', errors='ignore').strip('\x00') @@ -376,7 +370,7 @@ class ItemInfoEntryBox(FullBox): content.write(struct.pack('>I', self.item_id)) if self._has_protection_field: content.write(struct.pack('>H', self.item_protection_index)) - # item_type 必须是 4 字节的 4CC,若不足则以 NUL 填充 + # Item types must be a 4CC; pad with NUL bytes when necessary. content.write(self.item_type.encode('ascii', errors='ignore')[:4].ljust(4, b'\x00')) content.write(item_name_bytes_to_write) if self.content_type is not None: @@ -396,8 +390,7 @@ class ItemInfoEntryBox(FullBox): return content.getvalue() -# --- ItemInfoBox --- -# ('iinf') +# ItemInfoBox (`iinf`) ----------------------------------------------------- class ItemInfoBox(FullBox): def __init__(self, size: int, box_type: str, offset: int, raw_data: bytes): self.entries: list[ItemInfoEntry] = [] @@ -433,16 +426,12 @@ class ItemInfoBox(FullBox): else: header.write(struct.pack('>I', len(infe_children))) - # --- START FIX 2 --- - # 之前是: children_data = super().build_content() - # 我们需要调用 Box.build_content (祖父级) + # Call the grandparent implementation so nested boxes rebuild correctly. children_data = super(FullBox, self).build_content() - # --- END FIX 2 --- return header.getvalue() + children_data -# --- PrimaryItemBox --- -# ('pitm') +# PrimaryItemBox (`pitm`) -------------------------------------------------- class PrimaryItemBox(FullBox): def __init__(self, size: int, box_type: str, offset: int, raw_data: bytes): self.item_id: int = 0 @@ -469,8 +458,7 @@ class PrimaryItemBox(FullBox): content.write(struct.pack('>I', self.item_id)) return content.getvalue() -# --- ImageSpatialExtentsBox --- -# ('ispe') +# ImageSpatialExtentsBox (`ispe`) ----------------------------------------- class ImageSpatialExtentsBox(FullBox): def __init__(self, size: int, box_type: str, offset: int, raw_data: bytes): self.image_width: int = 0 @@ -496,7 +484,7 @@ class ImageSpatialExtentsBox(FullBox): content.write(struct.pack('>I', self.image_height)) return content.getvalue() -# --- ItemPropertyAssociationEntry (DataClass) --- +# ItemPropertyAssociationEntry --------------------------------------------- class ItemPropertyAssociation: def __init__(self, property_index: int, essential: bool): self.property_index = property_index @@ -517,8 +505,7 @@ class ItemPropertyAssociationEntry: return (f"") -# --- ItemPropertyAssociationBox --- -# ('ipma') +# ItemPropertyAssociationBox (`ipma`) ------------------------------------- class ItemPropertyAssociationBox(FullBox): def __init__(self, size: int, box_type: str, offset: int, raw_data: bytes): self.entries: dict[int, ItemPropertyAssociationEntry] = {} @@ -587,14 +574,12 @@ class ItemPropertyAssociationBox(FullBox): return content.getvalue() -# --- ItemPropertyContainerBox --- -# ('ipco') +# ItemPropertyContainerBox (`ipco`) --------------------------------------- class ItemPropertyContainerBox(Box): def _post_parse_initialization(self): pass -# --- ItemPropertiesBox --- -# ('iprp') +# ItemPropertiesBox (`iprp`) ---------------------------------------------- class ItemPropertiesBox(Box): def _post_parse_initialization(self): pass @@ -610,7 +595,7 @@ class ItemPropertiesBox(Box): if isinstance(child, ItemPropertyAssociationBox): return child return None -# --- ItemReferenceEntry (DataClass) --- +# ItemReferenceEntry ------------------------------------------------------- class ItemReferenceEntry: def __init__(self, from_id, to_ids): self.from_item_id = from_id @@ -618,8 +603,7 @@ class ItemReferenceEntry: def __repr__(self): return f"" -# --- ItemReferenceBox --- -# ('iref') +# ItemReferenceBox (`iref`) ------------------------------------------------ class ItemReferenceBox(FullBox): def __init__(self, size: int, box_type: str, offset: int, raw_data: bytes): self.references: dict[str, dict[int, list[int]]] = {} @@ -674,10 +658,7 @@ class ItemReferenceBox(FullBox): header = BytesIO() header.write(self.build_full_box_header()) - # --- START FIX 2 --- - # 之前是: children_data = super().build_content() - # 我们需要调用 Box.build_content (祖父级) + # Preserve nested reference payloads by delegating to Box.build_content. children_data = super(FullBox, self).build_content() - # --- END FIX 2 --- return header.getvalue() + children_data diff --git a/pyheic_struct/parser.py b/pyheic_struct/parser.py index 94da9d6..a97e04e 100644 --- a/pyheic_struct/parser.py +++ b/pyheic_struct/parser.py @@ -1,5 +1,3 @@ -# pyheic_struct/parser.py - import struct from typing import List, BinaryIO from io import BytesIO @@ -8,7 +6,7 @@ from .base import Box, FullBox from .heic_types import ( ItemLocationBox, PrimaryItemBox, ItemInfoBox, ItemPropertiesBox, ItemPropertyContainerBox, ItemPropertyAssociationBox, ImageSpatialExtentsBox, - ItemReferenceBox, ItemInfoEntryBox # <-- 导入新的 'infe' 盒 + ItemReferenceBox, ItemInfoEntryBox ) BOX_TYPE_MAP = { @@ -20,13 +18,12 @@ BOX_TYPE_MAP = { 'ipma': ItemPropertyAssociationBox, 'ispe': ImageSpatialExtentsBox, 'iref': ItemReferenceBox, - 'infe': ItemInfoEntryBox, # <-- 注册新的 'infe' 盒 + 'infe': ItemInfoEntryBox, } -# 'iref' is also a container. -# 'iinf' IS a container (它包含 'infe' 盒) +# Boxes treated as containers (their payload should be parsed recursively). CONTAINER_BOXES = {'meta', 'moov', 'trak', 'iprp', 'ipco', 'dinf', 'fiinf', 'ipro', 'iinf', 'iref'} -# 几乎所有语义盒都是 FullBox +# Boxes that should default to the `FullBox` implementation. FULL_BOXES = {'meta', 'hdlr', 'pitm', 'iinf', 'iloc', 'ipma', 'ispe', 'iref', 'infe'} def parse_boxes(stream: BinaryIO, max_size: int) -> List[Box]: @@ -75,28 +72,23 @@ def parse_boxes(stream: BinaryIO, max_size: int) -> List[Box]: parse_size = len(box.raw_data) if box.is_full_box: - # 跳过 4 字节的 version/flags + # Skip the 4-byte version/flags prefix when recursing. 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 + child_stream.read(2) parse_size -= 2 else: if parse_size >= 4: - child_stream.read(4) # 跳过 32-bit item_count + child_stream.read(4) parse_size -= 4 - # --- END FIX --- box.children = parse_boxes(child_stream, parse_size) - # 调用钩子 box._post_parse_initialization() boxes.append(box)