From b4d35521cf7109ac66b7b5c2a0c6e76cb1f844cc Mon Sep 17 00:00:00 2001 From: Nymiro Date: Mon, 20 Oct 2025 12:10:59 +1300 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=20AppleHandler=20=E5=92=8C?= =?UTF-8?q?=20SamsungHandler=20=E7=B1=BB=EF=BC=8C=E5=A4=84=E7=90=86?= =?UTF-8?q?=E7=89=B9=E5=AE=9A=E4=BA=8E=E4=BE=9B=E5=BA=94=E5=95=86=E7=9A=84?= =?UTF-8?q?=20HEIC=20=E7=89=B9=E6=80=A7=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=9F=A5=E6=89=BE=E5=B5=8C=E5=85=A5=E8=A7=86=E9=A2=91=E7=9A=84?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= 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 -> 1526 bytes __pycache__/heic_file.cpython-314.pyc | Bin 0 -> 5657 bytes __pycache__/heic_types.cpython-314.pyc | Bin 3925 -> 3925 bytes .../__pycache__/apple_handler.cpython-314.pyc | Bin 0 -> 1171 bytes .../__pycache__/base_handler.cpython-314.pyc | Bin 0 -> 1111 bytes .../samsung_handler.cpython-314.pyc | Bin 0 -> 1417 bytes handlers/apple_handler.py | 14 ++++ handlers/base_handler.py | 17 ++++ handlers/samsung_handler.py | 17 ++++ heic_file.py | 76 ++++++++++++++++++ heic_types.py | 3 +- main.py | 55 +++++-------- 13 files changed, 144 insertions(+), 38 deletions(-) create mode 100644 __pycache__/heic_file.cpython-314.pyc create mode 100644 handlers/__pycache__/apple_handler.cpython-314.pyc create mode 100644 handlers/__pycache__/base_handler.cpython-314.pyc create mode 100644 handlers/__pycache__/samsung_handler.cpython-314.pyc create mode 100644 handlers/apple_handler.py create mode 100644 handlers/base_handler.py create mode 100644 handlers/samsung_handler.py create mode 100644 heic_file.py diff --git a/.DS_Store b/.DS_Store index 7a2552b9f89bc84bdf14c1098323116654d05af4..2edecdfa0e9034416f99a12270e362e9d818d0bc 100644 GIT binary patch delta 782 zcmZp1XmOa}UDU^hRb;$$8Hskn{43=9k`40;Ud44Diix%nT_QssLkif8B8FmoI)J({)aRrld4xf<4VxO|z{>p(^b{np zfM!LZTArJa2sKoNsOnHck0AkQCj$@ABdNe3NCtX84M^uqP7o4fVpQ5(A+(pVUWlO> zXhJTKECqTtotVI4Vgi~94=f;C0>g}4hJ0XM*`3AivROan9j3GlEoFf^23oXjt%31V@9 xShAC&1o;>ZCeIa=oZKXMeez5pz0Jpk*D+4!6q~pCGLH%KW_F2hEE_Y}nF0LtGcEuC diff --git a/__pycache__/base.cpython-314.pyc b/__pycache__/base.cpython-314.pyc index 0b4cf316016d651ed0eb6194212365f2424c4541..28195d2727e09e00ed004ec09ebc1f06458cb4c9 100644 GIT binary patch delta 19 Zcmeyy{f(PTn~#@^0SKn#Z{&Kz3IH=R1q=WH delta 19 Zcmeyy{f(PTn~#@^0SHcJZ{&Kz3IH@+1w8-& diff --git a/__pycache__/heic_file.cpython-314.pyc b/__pycache__/heic_file.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..64efcce60b333268663d91686144cb194c487705 GIT binary patch literal 5657 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 pqB|CjN?austRBk$%j z%#w_Z{F|S#XoDyrHc=)$TxWpUm4@$$;SLj03S3O A%>V!Z diff --git a/handlers/__pycache__/apple_handler.cpython-314.pyc b/handlers/__pycache__/apple_handler.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b8b90c7379aa5a4fc773bce3f5101294c637d597 GIT binary patch literal 1171 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 literal 0 HcmV?d00001 diff --git a/handlers/apple_handler.py b/handlers/apple_handler.py new file mode 100644 index 0000000..81e00b9 --- /dev/null +++ b/handlers/apple_handler.py @@ -0,0 +1,14 @@ +from .base_handler import VendorHandler + +class AppleHandler(VendorHandler): + """ + Handles Apple-specific HEIC features. + Apple's motion photos (Live Photos) are stored in a separate MOV file, + so we won't find an embedded video here. + """ + # We override the method to make it explicit that Apple files are handled, + # even though the result is the same as the base class. + def find_motion_photo_offset(self, heic_file) -> int | None: + # We could add logic here later to find the ContentIdentifier + print("Apple HEIC detected. Motion photo video is in a separate .mov file, not embedded.") + return None \ No newline at end of file diff --git a/handlers/base_handler.py b/handlers/base_handler.py new file mode 100644 index 0000000..9a60ab2 --- /dev/null +++ b/handlers/base_handler.py @@ -0,0 +1,17 @@ +from __future__ import annotations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from heic_file import HEICFile + +class VendorHandler: + """ + Abstract base class for vendor-specific logic. + """ + def find_motion_photo_offset(self, heic_file: HEICFile) -> int | None: + """ + Tries to find the byte offset of the embedded motion photo video. + Returns the offset if found, otherwise None. + """ + # Default behavior: no motion photo found. + return None \ No newline at end of file diff --git a/handlers/samsung_handler.py b/handlers/samsung_handler.py new file mode 100644 index 0000000..8583868 --- /dev/null +++ b/handlers/samsung_handler.py @@ -0,0 +1,17 @@ +from .base_handler import VendorHandler + +class SamsungHandler(VendorHandler): + """Handles Samsung-specific HEIC features, like the embedded mpvd box.""" + + def find_motion_photo_offset(self, heic_file) -> int | None: + """ + 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 + + print("Samsung HEIC detected, but no 'mpvd' box found.") + return None \ No newline at end of file diff --git a/heic_file.py b/heic_file.py new file mode 100644 index 0000000..b89e5bc --- /dev/null +++ b/heic_file.py @@ -0,0 +1,76 @@ +import os +from base import Box +from parser import parse_boxes +from heic_types import ItemLocationBox + +# Import handlers +from handlers.base_handler import VendorHandler +from handlers.apple_handler import AppleHandler +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.handler: VendorHandler = None # Will hold our strategy object + + 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() + + 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.children: self._find_essential_boxes(box.children) + + 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]: + self.handler = SamsungHandler() + return + if 'apple' in compatible_brands.lower() or 'MiHB' in compatible_brands: + 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 + location = next((loc for loc in self._iloc_box.locations if loc.item_id == item_id), None) + if not location: + 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. + """ + 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 739b7f1..cdfd8be 100644 --- a/heic_types.py +++ b/heic_types.py @@ -1,4 +1,3 @@ -# In heic_types.py import struct from base import Box # <-- THIS IS THE KEY CHANGE @@ -29,7 +28,7 @@ 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 += 4 # Skip item_id and construction_method + current_pos += 2 # Skip construction_method current_pos += 2 # Skip data_reference_index current_pos += base_offset_size extent_count = struct.unpack('>H', stream[current_pos : current_pos+2])[0] diff --git a/main.py b/main.py index b5dcc9f..8c73282 100644 --- a/main.py +++ b/main.py @@ -1,44 +1,27 @@ -from base import Box -from parser import parse_boxes -from heic_types import ItemLocationBox -from typing import List -import os - -def print_box_tree(boxes: List[Box], indent: int = 0): - for box in boxes: - print(" " * indent + str(box)) - - if isinstance(box, ItemLocationBox): - for loc in box.locations[:5]: - print(" " * (indent + 1) + str(loc)) - if len(box.locations) > 5: - print(" " * (indent + 1) + f"... and {len(box.locations) - 5} more locations") - - if box.children: - print_box_tree(box.children, indent + 1) +from heic_file import HEICFile def main(): - # Test with the Apple HEIC file - print("--- Analyzing Apple HEIC file ---") - try: - with open('apple.heic', 'rb') as f: - file_size = os.fstat(f.fileno()).st_size - top_level_boxes = parse_boxes(f, file_size) - print_box_tree(top_level_boxes) - except FileNotFoundError: - print("Error: apple.heic not found.") + print("--- Processing Samsung HEIC file ---") + samsung_heic = HEICFile('samsung.heic') + samsung_video_data = samsung_heic.get_motion_photo_data() + if samsung_video_data: + output_filename = 'samsung_motion_photo.mp4' + with open(output_filename, 'wb') as f: + f.write(samsung_video_data) + print(f"Successfully extracted motion photo to '{output_filename}' ({len(samsung_video_data)} bytes)") + print("\n" + "="*40 + "\n") - # Test with the Samsung HEIC file - print("--- Analyzing Samsung HEIC file ---") - try: - with open('samsung.heic', 'rb') as f: - file_size = os.fstat(f.fileno()).st_size - top_level_boxes = parse_boxes(f, file_size) - print_box_tree(top_level_boxes) - except FileNotFoundError: - print("Error: samsung.heic not found.") + print("--- Processing Apple HEIC file ---") + apple_heic = HEICFile('apple.heic') + apple_video_data = apple_heic.get_motion_photo_data() + + if apple_video_data: + # This block should not be reached for Apple files + print("Extracted embedded video from Apple file (this is unexpected).") + else: + print("No embedded motion photo found in Apple file, as expected.") if __name__ == "__main__":