diff --git a/.DS_Store b/.DS_Store index 7a2552b..2edecdf 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/__pycache__/base.cpython-314.pyc b/__pycache__/base.cpython-314.pyc index 0b4cf31..28195d2 100644 Binary files a/__pycache__/base.cpython-314.pyc and b/__pycache__/base.cpython-314.pyc differ diff --git a/__pycache__/heic_file.cpython-314.pyc b/__pycache__/heic_file.cpython-314.pyc new file mode 100644 index 0000000..64efcce Binary files /dev/null and b/__pycache__/heic_file.cpython-314.pyc differ diff --git a/__pycache__/heic_types.cpython-314.pyc b/__pycache__/heic_types.cpython-314.pyc index 0aae52b..1186953 100644 Binary files a/__pycache__/heic_types.cpython-314.pyc and b/__pycache__/heic_types.cpython-314.pyc differ diff --git a/handlers/__pycache__/apple_handler.cpython-314.pyc b/handlers/__pycache__/apple_handler.cpython-314.pyc new file mode 100644 index 0000000..b8b90c7 Binary files /dev/null and b/handlers/__pycache__/apple_handler.cpython-314.pyc differ diff --git a/handlers/__pycache__/base_handler.cpython-314.pyc b/handlers/__pycache__/base_handler.cpython-314.pyc new file mode 100644 index 0000000..95a9e55 Binary files /dev/null and b/handlers/__pycache__/base_handler.cpython-314.pyc differ diff --git a/handlers/__pycache__/samsung_handler.cpython-314.pyc b/handlers/__pycache__/samsung_handler.cpython-314.pyc new file mode 100644 index 0000000..fb9a4b7 Binary files /dev/null and b/handlers/__pycache__/samsung_handler.cpython-314.pyc differ 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__":