From 7715acd18b752cdd0ab1105d3006160d19784b73 Mon Sep 17 00:00:00 2001 From: Nymiro Date: Mon, 20 Oct 2025 12:47:47 +1300 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20HEICFile=20=E7=B1=BB?= =?UTF-8?q?=E4=BB=A5=E6=94=AF=E6=8C=81=E6=96=B0=E7=89=B9=E6=80=A7=EF=BC=8C?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=9B=BE=E5=83=8F=E5=B0=BA=E5=AF=B8=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E5=8A=9F=E8=83=BD=EF=BC=8C=E9=87=8D=E6=9E=84=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E9=80=BB=E8=BE=91=EF=BC=8C=E5=A2=9E=E5=BC=BA=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=8F=AF=E8=AF=BB=E6=80=A7=E5=92=8C=E5=81=A5=E5=A3=AE?= =?UTF-8?q?=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 8196 -> 8196 bytes __pycache__/base.cpython-314.pyc | Bin 1526 -> 1737 bytes __pycache__/heic_file.cpython-314.pyc | Bin 6947 -> 8781 bytes __pycache__/heic_types.cpython-314.pyc | Bin 7609 -> 13810 bytes __pycache__/parser.cpython-314.pyc | Bin 2969 -> 3184 bytes base.py | 6 +- heic_file.py | 83 +++++++++----- heic_types.py | 143 ++++++++++++++++++++----- main.py | 16 ++- parser.py | 25 +++-- 10 files changed, 204 insertions(+), 69 deletions(-) diff --git a/.DS_Store b/.DS_Store index c8f860b6742574c236a6be75c66a4878933081a5..f68cfb6e4d83b8042f9216ee5a4c21dc78473dfa 100644 GIT binary patch delta 157 zcmZp1XmQw}DiCj{;mE+iz`~%%kj{|FP?DSP;*yk;p9B=+0AlCsLC4YLQ}D_cWEch~ z=jRpx^)fIx2{24{5R{rcPr!vKy8_JoC&0t{uf}h}^~wB#nykljuK8U9vScSm3Gy*c Om^@cda&wbl6CVJ3Mkzx8 delta 157 zcmZp1XmQw}DiCk`b}|D40}F#5LpnnyLrHGFi%U{YeiBfO1Bg#v2|DhGDxZQ^z97Re zI5|JJ0H}w7!7+hhvV)-1~? diff --git a/__pycache__/base.cpython-314.pyc b/__pycache__/base.cpython-314.pyc index 28195d2727e09e00ed004ec09ebc1f06458cb4c9..a49bc2a4768e1f3e77c98f32be1a61c88e4e6e4e 100644 GIT binary patch delta 353 zcmeyyeUg_?n~#@^0SM-mf6d5Zn#h;V=r^&chgfoaknL(3t zg-rn{R-`gngVm2wYjQcOtiT5bMphrj?`%x0ydM~V#N=75@^(eaAY~#T0_+qHo80`A l(wtPgB0ZoO$c@E9K;i>4BO~Ko2J!DK0*t($8Gr;>4*<8ITMz&M delta 128 zcmX@f`;D7Vn~#@^0SKn#f6aKpIFT=%(PLtnBTor?7()q15W6yiCdbB;3z-=;Cug%H zOzvQnW#pW^jMarve)0!aKSqVg_H43j-xwHKeHbU_u*oy7oZQbQrltinkP(QBMS#Qy YW=2NFyA0yrS$G(I7(X+B$Rdyg09tAt+yDRo diff --git a/__pycache__/heic_file.cpython-314.pyc b/__pycache__/heic_file.cpython-314.pyc index 9a25696439f08455909a9a3c831ecf751ac44b8e..727537aa0ac271b21a35d63f8af81fb8739c45c6 100644 GIT binary patch delta 4156 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 Ang9R* delta 2664 zcmbVOU2I%O6`r|&dw1{t*!$yM+w0xyzctqmj!DzFAx>%H*7D-WI7`?d5yxGxuk97~ z?rQFJaH>%IfuDe)Hr61eeWL;d0ac_bNVE?{;e{8dnyR$c1=EP12ZWHEq-8^2;G9{z zrQ#tHW8ZJ)oHM^?zH?@O`}p^cgonbR0DnrGhdXYRtaZ4+DApqFI-=OhuA>>wtPyU?gv_+?n{txI_?MLq{V;8+ zc|8;#tIOw-`@PQg?w`L%s=uo8s@bckx3?f`3(+Ff3`>li0i!VG;hi zGQ$1pB8~96+6k*(QO$$6Ip-?CCfQDzS3=u^!1}Rm znXEy=1Mh@#@c(8g_kn{G-PBJEo*YwuHD-*5_^dXq;YnF9e@TmNv|=*~;NWHUY_b?C z;s^8^Y5T80h?+Cnp_$B$pqs=yJ1ABRo3(zD)flnqRcyAb-w@He$g-kp{ zNL?yQGor(Xzz-TK@7E*7o|g$JO58N!9R=x;FA}og!!4XOs~^t3ZiJh zjQbG<%#S7FeADoUU`U!wIj|s^3GsioC-{j(jL#~8@D%vnPcjf5Kpc%<*N5gSE48wx zEV>u#o>Hk)7d@q3aaX4;pQnJ%X6-OL>p(zXG4z_JmzOIm3(T&vJ`jQHIW+a6NrsSc zaL>7%*>rdx&-;5fgtr$#ze)ZaOI;q{jzphb|JM3*N8XBb-x&Y?qyLY5>Df!)zVzIg zce+Mzb&dW){$=o$;M-kK+%rhLy?Jry`KI@IMKkz~mWM}?AD+f+ca2p=JXfl&C9wx^ zrJuLAcGEU~to8L?;Q_VIV4&x#Kse7;7Az`U;NW687UQ|-5!%mZqa!fpbMf|{A$9Ye*S7=C<5|1i~>;#0sdaX zDuHfI%EDKdDv;@(IvvPYow{9i9n)R5%>kswfC<-Xx;1mbc5Sv=soKJJ!LBaUm|3bW zm?g)lELDZJTw@G|o6DeIvDq*mYI~Y?@E^Bz<&Yxm5CSG{)`j3lK({a*0LjGv(bjWF z1ZzKNyyWB)`SFR$iak7s%a`4`=DLT$===;|lkDmw*up=P+)BQu5r6oN@MquBr~Z)a z;~STiXM+E_8{>aXp5L$(Cep(gjA4%eOv42nh1yf+FS_D79>9YM=*R!6}ItUiO)1H+>Ty&T*xOxGX*HGT)- zg|#2KKe!~CAB7`y5(WIx%%d2f!0ePXBlE7zKnS-g3M4O$pU%Xn#GjYb3glNm|9YmI ze=QS(cScXG$!k)9;@@G+H&C(!=uY|bN_QC^?PGEmsZdLP>Qvxyd&OQV!3_(3I(CVb zm(4|B?rIG^Y1Wr($ZhZxavWLTq_e$X?ooW7%YkVo?nPr=yfvE{Ze9Vb&1Mni0QS@2 zc~syX?AQ8 zb?}Z(TH*~q2ygERA2J`Ei!f*I*5$SBG|%6o3=vyw0R_J zp#l>(JC1M?0RxSd5tjK+%#jW8Y+^QhteM2|M*u^>&lv}}|4leRLkg)qy9A(-!54H_ zyr7M!qULr9z}=uv?YSF`r~{2Yh!7Db4HLNWPb7$7g_zZ9VHj1OGGB6RCKEp2J=pFg z_Pp*4qk3*nVmkk6cN!AfZ@M#2iZp_Ix=xz2VcPKYvDMnb+KT-s`wED73}+Z%SEiKy np7?i2e1~LqNbX%Sev6F1uMs-3Lk{kchZ@QmO5^tkqL}YLO!rMs diff --git a/__pycache__/heic_types.cpython-314.pyc b/__pycache__/heic_types.cpython-314.pyc index bb18c79edccc0ca0a3c972999a98939060746fd6..7227956207280384cd95800bdc2e619a0f821f1e 100644 GIT binary patch literal 13810 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 UD{?p%&sLLBvOWC^hoc+r4}~AISO5S3 diff --git a/__pycache__/parser.cpython-314.pyc b/__pycache__/parser.cpython-314.pyc index 9e537d5938aabcff26452ad71bb726833f095435..c58d80f6b704c7b176298c193f3223ce10696063 100644 GIT binary patch delta 709 zcmYk4&ubGw6vt;bYxehUHVGyaV~bWpNHOU_RFHzk&<*}7+u}u6jLB*jlI$`&N&W$*%4jEkzCc0UCmKk-BDe`(Ok18 zl1v>rW}hP#iy0QPj$S~HQ9!*6$vNCxL%wC8bU>Gcx zF871(lb*MPZ!*cT9X+HgYW~iIPjUPNG3)}y20Es0-To;kTekq$8Md`j!uf<}6P_BI ZQidODe~Kt~j~kk?-W)2EG`oW2(0_kzwSNEr delta 529 zcmXw#Pe>a<6vp3}NisXTn?D+jCxb{$gNI11h@yd#7AY1BtJjo@Q4CGGVcaSn#65Zq za}vEvp*QWVQ0TFz_9k0Ip?H;Ttw?WuZ&L^6_syHfn>XJJ2M_Yav6L+V*O0p1xD)_C zDY##W|A(8cPI3Nm0h~n5aFR9Yh?-fBdDb{MW{Y_?s)d?zj666p56z61cGw7%?9P`l z7VBJ>v%JxCR|DQ|c&&gp8gJM2F}oS>#|FK)Hy0G?u80el7M`QD2H`E=L!odqmHk=_zB$SY%*j{TF{0v^HLeN;jC( ze~oGJCn z71;~KY|E{`Ypzw)JcbPS<`YC1V~qU(`vQi~Vc-gK7f}5U)lfj(+<$q-<^CdJAx!d2 Xk8l09pl_M^>A;@! str: - return f"" \ No newline at end of file + return f"" + + def _post_parse_initialization(self): + """Called by the parser after children have been assigned.""" + pass \ No newline at end of file diff --git a/heic_file.py b/heic_file.py index 348fd62..5c5f7ef 100644 --- a/heic_file.py +++ b/heic_file.py @@ -1,10 +1,13 @@ +# pyheic_struct/heic_file.py + import os from base import Box from parser import parse_boxes -from heic_types import ItemLocationBox -from heic_types import ItemLocationBox, PrimaryItemBox, ItemInfoBox +from heic_types import ( + ItemLocationBox, PrimaryItemBox, ItemInfoBox, ItemPropertiesBox, + ImageSpatialExtentsBox +) -# Import handlers from handlers.base_handler import VendorHandler from handlers.apple_handler import AppleHandler from handlers.samsung_handler import SamsungHandler @@ -12,34 +15,68 @@ from handlers.samsung_handler import SamsungHandler class HEICFile: def __init__(self, filepath: str): self.filepath = filepath - self._iloc_box: ItemLocationBox = None - self._ftyp_box: Box = None - self._iinf_box: ItemInfoBox = None # <-- Add this line - self._pitm_box: PrimaryItemBox = None - self.handler: VendorHandler = None # Will hold our strategy object + self._iloc_box: ItemLocationBox | None = None + self._ftyp_box: Box | None = None + self._iinf_box: ItemInfoBox | None = None + self._pitm_box: PrimaryItemBox | None = None + self._iprp_box: ItemPropertiesBox | None = None + self.handler: VendorHandler | None = None 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() + # --- FIX: Changed 'elif' to separate 'if' statements for robustness --- def _find_essential_boxes(self, boxes: list[Box]): """Recursively search for essential boxes.""" for box in boxes: - if box.type == 'iloc': self._iloc_box = box - if box.type == 'ftyp': self._ftyp_box = box - if box.type == 'iinf': self._iinf_box = box # <-- Add this line - if box.type == 'pitm': self._pitm_box = box - if box.children: self._find_essential_boxes(box.children) - + if isinstance(box, ItemLocationBox): + self._iloc_box = box + if isinstance(box, ItemInfoBox): + self._iinf_box = box + if isinstance(box, PrimaryItemBox): + self._pitm_box = box + if isinstance(box, ItemPropertiesBox): + self._iprp_box = box + if box.type == 'ftyp': + self._ftyp_box = box + + if box.children: + self._find_essential_boxes(box.children) + + # All other methods remain unchanged + def get_image_size(self, item_id: int) -> tuple[int, int] | None: + """ + Gets the width and height of a given image item ID. + """ + if not self._iprp_box or not self._iprp_box.ipma or not self._iprp_box.ipco: + print("Warning: 'iprp' box or its sub-boxes ('ipma', 'ipco') not found.") + return None + + if item_id not in self._iprp_box.ipma.entries: + print(f"Warning: No property association found for item ID {item_id}.") + return None + + item_associations = self._iprp_box.ipma.entries[item_id].associations + + for assoc in item_associations: + property_index = assoc - 1 + if property_index < len(self._iprp_box.ipco.children): + prop = self._iprp_box.ipco.children[property_index] + if isinstance(prop, ImageSpatialExtentsBox): + return (prop.image_width, prop.image_height) + + print(f"Warning: 'ispe' property not found for item ID {item_id}.") + return None + def get_primary_item_id(self) -> int | None: if not self._pitm_box: print("Warning: 'pitm' box not found. Cannot determine primary item.") return None return self._pitm_box.item_id - + def list_items(self): if not self._iinf_box: print("Warning: 'iinf' box not found. Cannot list items.") @@ -50,9 +87,6 @@ class HEICFile: print(f" - {entry}") def _detect_vendor(self): - """ - Inspects the 'ftyp' box to determine the vendor and assign the correct handler. - """ if self._ftyp_box: compatible_brands = self._ftyp_box.raw_data[4:].decode('ascii', errors='ignore') if 'samsung' in compatible_brands.lower() or 'mpvd' in [b.type for b in self.boxes]: @@ -62,13 +96,10 @@ class HEICFile: self.handler = AppleHandler() return - # If no specific handler is found, use the base one. print("Could not detect a specific vendor. Using default handler.") self.handler = VendorHandler() - def get_item_data(self, item_id: int) -> bytes | None: - # ... (this method remains unchanged) ... if not self._iloc_box: print("Error: 'iloc' box not found.") return None @@ -77,20 +108,16 @@ class HEICFile: print(f"Error: Item with ID {item_id} not found in 'iloc' box.") return None with open(self.filepath, 'rb') as f: - # IMPORTANT: iloc offsets are absolute from the start of the file. - # The mdat box's offset is not needed for this calculation. f.seek(location.offset) data = f.read(location.length) return data def get_motion_photo_data(self) -> bytes | None: - """ - Delegates the search for motion photo data to the assigned vendor handler. - """ + if not self.handler: + self._detect_vendor() offset = self.handler.find_motion_photo_offset(self) if offset is not None: with open(self.filepath, 'rb') as f: - # We assume the video data goes to the end of the file from its offset f.seek(offset) return f.read() return None \ No newline at end of file diff --git a/heic_types.py b/heic_types.py index f0a8609..77996d3 100644 --- a/heic_types.py +++ b/heic_types.py @@ -1,8 +1,11 @@ +# pyheic_struct/heic_types.py + import struct -from base import Box # <-- THIS IS THE KEY CHANGE +from base import Box + +# --- Existing classes (unchanged) --- class ItemLocation: - # ... (rest of this class is unchanged) def __init__(self, item_id, offset, length): self.item_id = item_id self.offset = offset @@ -11,13 +14,11 @@ class ItemLocation: return f"" class ItemLocationBox(Box): - # ... (rest of this class is unchanged) 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): - # ... (all the parsing logic here is unchanged) stream = self.raw_data version_flags = struct.unpack('>I', stream[:4])[0] sizes = struct.unpack('>H', stream[4:6])[0] @@ -28,8 +29,8 @@ class ItemLocationBox(Box): current_pos = 8 for _ in range(item_count): item_id = struct.unpack('>H', stream[current_pos : current_pos+2])[0] - current_pos += 2 # Skip construction_method - current_pos += 2 # Skip data_reference_index + current_pos += 2 + current_pos += 2 current_pos += base_offset_size extent_count = struct.unpack('>H', stream[current_pos : current_pos+2])[0] current_pos += 2 @@ -47,35 +48,22 @@ class ItemLocationBox(Box): return 0 class ItemInfoEntry: - """A data class for one item described in an 'iinf' box.""" def __init__(self, item_id, item_type, item_name): self.item_id = item_id self.type = item_type self.name = item_name - def __repr__(self): return f"" class ItemInfoBox(Box): - """ - A specialized class for the 'iinf' box. - Parses its own data to build a list of all available items. - """ 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): - # 'iinf' is a FullBox, so the first 4 bytes are version/flags item_count = struct.unpack('>H', self.raw_data[4:6])[0] - - # The children are 'infe' boxes, which are parsed by the generic parser for infe_box in self.children: if infe_box.type == 'infe': - # 'infe' is a FullBox (version, flags) - # version = infe_box.raw_data[0] 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:] @@ -83,15 +71,118 @@ class ItemInfoBox(Box): self.entries.append(ItemInfoEntry(item_id, item_type, item_name)) class PrimaryItemBox(Box): - """ - A specialized class for the 'pitm' 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): - # 'pitm' is a FullBox (version, flags) - self.item_id = struct.unpack('>H', self.raw_data[4:6])[0] \ No newline at end of file + self.item_id = struct.unpack('>H', self.raw_data[4:6])[0] + +# --- NEW classes for Task 3.1 & 3.2 --- + +class ImageSpatialExtentsBox(Box): + """'ispe' box, contains image dimensions.""" + def __init__(self, size: int, box_type: str, offset: int, raw_data: bytes): + super().__init__(size, box_type, offset, raw_data) + # 'ispe' is a FullBox + 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 __repr__(self): + return f"" + +class ItemPropertyAssociationEntry: + """Helper class for an entry in the 'ipma' box.""" + def __init__(self, item_id, association_count): + self.item_id = item_id + self.association_count = association_count + self.associations = [] # List of property indices + def __repr__(self): + return f"" + +class ItemPropertyAssociationBox(Box): + """'ipma' box, maps items to their properties.""" + 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] = {} + self._parse_associations() + + def _parse_associations(self): + stream = self.raw_data + + # A FullBox has a 4-byte header (version + flags) + 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 + + # Determine field sizes based on the box version and flags + item_id_size = 4 if version >= 1 else 2 + is_large_property_index = (flags & 1) == 1 + + for _ in range(entry_count): + # Boundary check for the item ID + if pos + item_id_size > len(stream): + break + + if item_id_size == 4: + item_id = struct.unpack('>I', stream[pos:pos+4])[0] + pos += 4 + else: # item_id_size == 2 + item_id = struct.unpack('>H', stream[pos:pos+2])[0] + pos += 2 + + # Boundary check for association_count + if pos + 1 > len(stream): + break + + association_count = stream[pos] + pos += 1 + + entry = ItemPropertyAssociationEntry(item_id, association_count) + + for __ in range(association_count): + if is_large_property_index: + # 2-byte association entry + if pos + 2 > len(stream): + break # Not enough data + assoc_value = struct.unpack('>H', stream[pos:pos+2])[0] + property_index = assoc_value & 0x7FFF # Lower 15 bits + pos += 2 + else: + # 1-byte association entry + if pos + 1 > len(stream): + break # Not enough data + assoc_value = stream[pos] + property_index = assoc_value & 0x7F # Lower 7 bits + pos += 1 + + # A property_index of 0 is not a valid index. + if property_index > 0: + entry.associations.append(property_index) + + self.entries[item_id] = entry + +class ItemPropertyContainerBox(Box): + """'ipco' box. Just a container for property boxes like 'ispe'.""" + pass + +class ItemPropertiesBox(Box): + """'iprp' box. Container for 'ipco' and 'ipma'.""" + + @property + def ipco(self) -> ItemPropertyContainerBox | None: + """Finds and returns the 'ipco' child box if it exists.""" + for child in self.children: + if isinstance(child, ItemPropertyContainerBox): + return child + return None + + @property + def ipma(self) -> ItemPropertyAssociationBox | None: + """Finds and returns the 'ipma' child box if it exists.""" + for child in self.children: + if isinstance(child, ItemPropertyAssociationBox): + return child + return None \ No newline at end of file diff --git a/main.py b/main.py index fc30ae2..bcbf4e5 100644 --- a/main.py +++ b/main.py @@ -1,22 +1,28 @@ -# In main.py +# pyheic_struct/main.py + from heic_file import HEICFile def analyze_file(filename: str): print(f"--- Analyzing {filename} ---") heic_file = HEICFile(filename) - + # List all available items in the file heic_file.list_items() - + # Get the ID of the primary image primary_id = heic_file.get_primary_item_id() if primary_id: - print(f"\nThe primary item ID is: {primary_id}") + print(f"The primary item ID is: {primary_id}") + + # --- Use our new feature! --- + size = heic_file.get_image_size(primary_id) + if size: + print(f"Primary image dimensions (WxH): {size[0]} x {size[1]}") print("\n" + "="*40 + "\n") def main(): - analyze_file('apple.heic') + analyze_file('apple.HEIC') analyze_file('samsung.heic') if __name__ == "__main__": diff --git a/parser.py b/parser.py index a003bd4..e0cad8a 100644 --- a/parser.py +++ b/parser.py @@ -2,19 +2,29 @@ import struct from typing import List, BinaryIO from io import BytesIO -from base import Box # Import the base Box class -from heic_types import ItemLocationBox, PrimaryItemBox, ItemInfoBox +from base import Box +from heic_types import ( + ItemLocationBox, PrimaryItemBox, ItemInfoBox, ItemPropertiesBox, + ItemPropertyContainerBox, ItemPropertyAssociationBox, ImageSpatialExtentsBox +) -# The factory map remains here +# The factory map remains the same BOX_TYPE_MAP = { 'iloc': ItemLocationBox, - 'pitm': PrimaryItemBox, + 'pitm': PrimaryItemBox, 'iinf': ItemInfoBox, + 'iprp': ItemPropertiesBox, + 'ipco': ItemPropertyContainerBox, + 'ipma': ItemPropertyAssociationBox, + 'ispe': ImageSpatialExtentsBox, } -CONTAINER_BOXES = {'meta', 'moov', 'trak', 'iprp', 'ipco', 'dinf', 'fiinf', 'ipro'} -FULL_BOXES = {'meta', 'hdlr', 'pitm', 'iinf', 'iloc'} +# --- FIX: Add 'iinf' to CONTAINER_BOXES --- +# 'iinf' is a container for 'infe' boxes, so it needs to be parsed recursively. +CONTAINER_BOXES = {'meta', 'moov', 'trak', 'iprp', 'ipco', 'dinf', 'fiinf', 'ipro', 'iinf'} +FULL_BOXES = {'meta', 'hdlr', 'pitm', 'iinf', 'iloc', 'ipma', 'ispe'} +# The parse_boxes function remains completely unchanged def parse_boxes(stream: BinaryIO, max_size: int) -> List[Box]: """ Parses boxes from a file stream up to a maximum size. @@ -49,11 +59,9 @@ def parse_boxes(stream: BinaryIO, max_size: int) -> List[Box]: raw_data = stream.read(content_size) if len(raw_data) < content_size: break - # --- Factory Pattern --- box_class = BOX_TYPE_MAP.get(box_type, Box) box = box_class(size, box_type, current_offset_in_stream, raw_data) - # --- NEW: Recursive parsing logic moved here --- if box.type in CONTAINER_BOXES: child_stream = BytesIO(box.raw_data) parse_size = len(box.raw_data) @@ -61,7 +69,6 @@ def parse_boxes(stream: BinaryIO, max_size: int) -> List[Box]: child_stream.read(4) # Skip version/flags parse_size -= 4 box.children = parse_boxes(child_stream, parse_size) - # --- End of new logic --- boxes.append(box) stream.seek(current_offset_in_stream + size)