实现 AppleHandler 和 SamsungHandler 类,处理特定于供应商的 HEIC 特性,添加查找嵌入视频的逻辑

This commit is contained in:
Nymiro
2025-10-20 12:10:59 +13:00
parent 8ee1d7984a
commit b4d35521cf
13 changed files with 144 additions and 38 deletions
Vendored
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+14
View File
@@ -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
+17
View File
@@ -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
+17
View File
@@ -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
+76
View File
@@ -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
+1 -2
View File
@@ -1,4 +1,3 @@
# In heic_types.py
import struct import struct
from base import Box # <-- THIS IS THE KEY CHANGE from base import Box # <-- THIS IS THE KEY CHANGE
@@ -29,7 +28,7 @@ class ItemLocationBox(Box):
current_pos = 8 current_pos = 8
for _ in range(item_count): for _ in range(item_count):
item_id = struct.unpack('>H', stream[current_pos : current_pos+2])[0] 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 += 2 # Skip data_reference_index
current_pos += base_offset_size current_pos += base_offset_size
extent_count = struct.unpack('>H', stream[current_pos : current_pos+2])[0] extent_count = struct.unpack('>H', stream[current_pos : current_pos+2])[0]
+19 -36
View File
@@ -1,44 +1,27 @@
from base import Box from heic_file import HEICFile
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)
def main(): def main():
# Test with the Apple HEIC file print("--- Processing Samsung HEIC file ---")
print("--- Analyzing Apple HEIC file ---") samsung_heic = HEICFile('samsung.heic')
try: samsung_video_data = samsung_heic.get_motion_photo_data()
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.")
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") print("\n" + "="*40 + "\n")
# Test with the Samsung HEIC file print("--- Processing Apple HEIC file ---")
print("--- Analyzing Samsung HEIC file ---") apple_heic = HEICFile('apple.heic')
try: apple_video_data = apple_heic.get_motion_photo_data()
with open('samsung.heic', 'rb') as f:
file_size = os.fstat(f.fileno()).st_size if apple_video_data:
top_level_boxes = parse_boxes(f, file_size) # This block should not be reached for Apple files
print_box_tree(top_level_boxes) print("Extracted embedded video from Apple file (this is unexpected).")
except FileNotFoundError: else:
print("Error: samsung.heic not found.") print("No embedded motion photo found in Apple file, as expected.")
if __name__ == "__main__": if __name__ == "__main__":