diff --git a/README.md b/README.md new file mode 100644 index 0000000..79013ae --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# pyheic-struct + +Utilities for inspecting, modifying, and rebuilding HEIC/HEIF files with a focus on +cross-vendor quirks between Samsung and Apple motion photos. + +## Features + +- Lightweight parser that exposes core ISOBMFF boxes (`ftyp`, `meta`, `iloc`, `iinf`, etc.) +- Helpers for reconstructing Samsung grid images into flat HEIC pictures +- Rebuilder that writes updated metadata and payload offsets safely +- High-level `convert_samsung_motion_photo` function to create Apple-compatible + HEIC/MOV pairs (with optional MOV ContentIdentifier injection via `exiftool`) +- CLI tool: `pyheic-struct samsung.heic` + +## Installation + +```bash +pip install . +``` + +## Usage + +```python +from pyheic_struct import convert_samsung_motion_photo + +convert_samsung_motion_photo("samsung.heic") +``` + +Command line: + +```bash +pyheic-struct samsung.heic --output-heic samsung_fixed.HEIC +``` + +## Development + +The package targets Python 3.10+. Run the CLI in editable mode: + +```bash +python -m pyheic_struct path/to/samsung.heic +``` diff --git a/__pycache__/base.cpython-314.pyc b/__pycache__/base.cpython-314.pyc deleted file mode 100644 index 050e88f..0000000 Binary files a/__pycache__/base.cpython-314.pyc and /dev/null differ diff --git a/__pycache__/builder.cpython-314.pyc b/__pycache__/builder.cpython-314.pyc deleted file mode 100644 index feb6d29..0000000 Binary files a/__pycache__/builder.cpython-314.pyc and /dev/null differ diff --git a/__pycache__/heic_file.cpython-314.pyc b/__pycache__/heic_file.cpython-314.pyc deleted file mode 100644 index 7e01dd8..0000000 Binary files a/__pycache__/heic_file.cpython-314.pyc and /dev/null differ diff --git a/__pycache__/heic_types.cpython-314.pyc b/__pycache__/heic_types.cpython-314.pyc deleted file mode 100644 index c928ed1..0000000 Binary files a/__pycache__/heic_types.cpython-314.pyc and /dev/null differ diff --git a/__pycache__/parser.cpython-314.pyc b/__pycache__/parser.cpython-314.pyc deleted file mode 100644 index d1fbb58..0000000 Binary files a/__pycache__/parser.cpython-314.pyc and /dev/null differ diff --git a/handlers/.DS_Store b/handlers/.DS_Store deleted file mode 100644 index b6363ba..0000000 Binary files a/handlers/.DS_Store and /dev/null differ diff --git a/handlers/__pycache__/apple_handler.cpython-314.pyc b/handlers/__pycache__/apple_handler.cpython-314.pyc deleted file mode 100644 index 53915f1..0000000 Binary files a/handlers/__pycache__/apple_handler.cpython-314.pyc and /dev/null differ diff --git a/handlers/__pycache__/base_handler.cpython-314.pyc b/handlers/__pycache__/base_handler.cpython-314.pyc deleted file mode 100644 index ebcc62d..0000000 Binary files a/handlers/__pycache__/base_handler.cpython-314.pyc and /dev/null differ diff --git a/handlers/__pycache__/samsung_handler.cpython-314.pyc b/handlers/__pycache__/samsung_handler.cpython-314.pyc deleted file mode 100644 index 569224a..0000000 Binary files a/handlers/__pycache__/samsung_handler.cpython-314.pyc and /dev/null differ diff --git a/inspect_heic.py b/inspect_heic.py index 65250f3..6383ec5 100644 --- a/inspect_heic.py +++ b/inspect_heic.py @@ -1,9 +1,10 @@ # pyheic_struct/inspect_heic.py import sys -from heic_file import HEICFile import pprint # 用于漂亮地打印字典 +from pyheic_struct import HEICFile + def inspect_file(filename: str): print(f"--- 正在侦察: {filename} ---") @@ -107,4 +108,4 @@ if __name__ == "__main__": print("用法: python inspect_heic.py <文件名>") print("例如: python inspect_heic.py samsung.heic") else: - inspect_file(sys.argv[1]) \ No newline at end of file + inspect_file(sys.argv[1]) diff --git a/main.py b/main.py index b660a17..86d1936 100644 --- a/main.py +++ b/main.py @@ -1,227 +1,4 @@ -# pyheic_struct/main.py -import os -import uuid -import subprocess -import pillow_heif # <--- (1) 导入 pillow_heif 用于转码 -from heic_file import HEICFile -from heic_types import ItemInfoEntryBox # <--- 添加这一行 -from builder import HEICBuilder -from handlers.base_handler import VendorHandler -from handlers.samsung_handler import SamsungHandler - -def convert_samsung_to_apple(samsung_file_path: str): - """ - (V17) 执行一个完整的两步转码: - 1. [转码] 使用 pillow_heif 将三星的 "网格" 图像转码为一个 "扁平" 图像。 - 2. [注入] 使用我们的 HEICBuilder 将 UUID 和 *正确* 的 Apple ftyp 注入到这个新的扁平文件中。 - """ - if not os.path.exists(samsung_file_path): - print(f"Error: File not found: {samsung_file_path}") - return - - base_filename = os.path.splitext(samsung_file_path)[0] - output_heic_path = f"{base_filename}_apple_compatible.HEIC" - output_mov_path = f"{base_filename}_apple_compatible.MOV" - temp_flat_heic_path = f"{base_filename}_temp_flat.HEIC" # <--- 临时文件 - - print(f"--- Converting {samsung_file_path} to Apple format (V17 Fix) ---") - - # --- 步骤 1: 从原始三星文件中提取所有需要的数据 --- - - print(f"Loading original file: {samsung_file_path}") - original_heic_file = HEICFile(samsung_file_path) - - # 1a. 提取视频数据 - video_data = original_heic_file.get_motion_photo_data() - # (如果找不到,手动检查 mpvd) - if not video_data: - if not original_heic_file.handler or isinstance(original_heic_file.handler, VendorHandler): - print("Vendor not auto-detected. Manually checking for Samsung 'mpvd' box...") - mpvd_box = original_heic_file.find_box('mpvd') - if mpvd_box: - original_heic_file.handler = SamsungHandler() - video_data = original_heic_file.get_motion_photo_data() # 再次尝试 - - # 1b. 提取图像像素 (转码网格) - print("Reconstructing grid image from Samsung file...") - pil_image = original_heic_file.reconstruct_primary_image() - if not pil_image: - print("CRITICAL: Failed to reconstruct primary image using pillow_heif.") - return - - # 1c. 生成 UUID - new_content_id = str(uuid.uuid4()).upper() - print(f"Generated ContentIdentifier: {new_content_id}") - - # --- 步骤 2: 处理 .MOV 文件 (这部分已经可以工作) --- - if video_data: - print(f"Saving extracted video data to {output_mov_path}...") - with open(output_mov_path, 'wb') as f_mov: - f_mov.write(video_data) - - try: - print("Attempting to inject ContentIdentifier into .MOV file (requires exiftool)...") - subprocess.run([ - 'exiftool', - f'-QuickTime:ContentIdentifier={new_content_id}', - '-overwrite_original', - output_mov_path - ], check=True, capture_output=True, text=True) - print("Successfully injected ContentIdentifier into .MOV.") - except Exception as e: - print(f"Warning: Could not inject ContentIdentifier into .MOV. ") - print(f" (This is normal if 'exiftool' is not installed.) Error: {e}") - else: - print("Info: No motion photo data found. Only a still HEIC will be created.") - - # --- 步骤 3: [转码] 创建一个临时的、扁平的 HEIC 文件 --- - try: - print(f"Saving temporary flat HEIC file to {temp_flat_heic_path}...") - pillow_heif.register_heif_opener() - - # 将内存中的 PIL 图像保存为一个新的、扁平的 HEIC 文件 - pil_image.save( - temp_flat_heic_path, - format="HEIF", - quality=95, - save_as_brand="mif1" # 存为一个基础的 'mif1' 品牌 - ) - print("Successfully created temporary flat HEIC.") - - except Exception as e: - print(f"CRITICAL: Failed to save temporary HEIC file: {e}") - if os.path.exists(temp_flat_heic_path): - os.remove(temp_flat_heic_path) - return - - # --- 步骤 4: [注入] 加载扁平文件,并执行我们的元数据手术 --- - try: - print(f"Loading temporary flat HEIC for metadata injection...") - flat_heic_file = HEICFile(temp_flat_heic_path) - - # 4a. (重要!) 修改 'ftyp' 盒以 *完全匹配* 苹果 Live Photo - if flat_heic_file._ftyp_box: - # --- START V15 FIX --- - # 使用从 apple.HEIC 侦察日志中提取的 *正确* 字符串 - # 它包含了我们缺失的 'MiHB' 品牌 - print("Modifying 'ftyp' box to be Apple compatible (heic, MiHB, MiHE...)...") - apple_ftyp_raw_data = b'heic\x00\x00\x00\x00mif1MiHBMiHEMiPrmiafheictmap' - flat_heic_file._ftyp_box.raw_data = apple_ftyp_raw_data - flat_heic_file._ftyp_box.size = len(apple_ftyp_raw_data) + 8 # (40 + 8 = 48) - # --- END V15 FIX --- - else: - print("Error: Temporary HEIC file has no 'ftyp' box. Aborting.") - os.remove(temp_flat_heic_path) - return - - # 4b. (V12 / V17 FIX) 纠正 pillow_heif 产生的 'shifted' ID - print("Checking for shifted IDs in temporary file (V17 Full Fix)...") - if flat_heic_file._iinf_box and flat_heic_file._iloc_box and flat_heic_file._iprp_box: - - # 1. 找出 iloc 中的 "正确" ID (e.g., {1, 2, 3, 4}) - # (我们假设 iloc 是正确的,因为 pillow-heif 似乎只搞错了其他盒) - correct_ids = {loc.item_id for loc in flat_heic_file._iloc_box.locations} - - # 2. 修复 'iinf' (infe boxes) - iinf_children_to_fix = [ - c for c in flat_heic_file._iinf_box.children - if isinstance(c, ItemInfoEntryBox) and (c.item_id >> 16) in correct_ids - ] - - # 我们需要建立一个映射表, e.g., {65536: 1} - shifted_id_map = {} - - if iinf_children_to_fix: - print(f" Found {len(iinf_children_to_fix)} shifted 'infe' boxes. Mapping IDs for reference...") - for infe_box in iinf_children_to_fix: - unshifted_id = infe_box.item_id >> 16 - # 检查 unshifted_id 是否真的在 correct_ids 中,以防万一 - if unshifted_id in correct_ids: - shifted_id_map[infe_box.item_id] = unshifted_id # 存映射 - print(f" - Mapping 'infe' ID {infe_box.item_id} -> {unshifted_id}") - else: - print(" 'infe' boxes seem correct. No shift detected.") - - # --- START V17 FIX --- - - # 3. 修复 'ipma' (属性) - if flat_heic_file._iprp_box.ipma: - ipma_entries = flat_heic_file._iprp_box.ipma.entries - # 找出所有 "shifted" 的字典键 - keys_to_fix = [k for k in ipma_entries if k in shifted_id_map] - - if keys_to_fix: - print(f" Found {len(keys_to_fix)} shifted 'ipma' entries. Fixing them...") - for shifted_key in keys_to_fix: - correct_key = shifted_id_map[shifted_key] - print(f" - Fixing 'ipma' key {shifted_key} -> {correct_key}") - - # 替换字典的键 (e.g., {65536: ...} -> {1: ...}) - entry_data = ipma_entries.pop(shifted_key) - entry_data.item_id = correct_key # 确保条目内部的 ID 也被更新 - ipma_entries[correct_key] = entry_data - else: - print(f" 'ipma' entries seem correct. (Keys: {list(ipma_entries.keys())})") - - # 4. 修复 'iref' (引用) - if flat_heic_file._iref_box: - iref_refs = flat_heic_file._iref_box.references - refs_fixed = 0 - for ref_type in iref_refs: # e.g., 'thmb', 'dimg' - # 找出所有 "shifted" 的字典键 - keys_to_fix = [k for k in iref_refs[ref_type] if k in shifted_id_map] - if keys_to_fix: - refs_fixed += len(keys_to_fix) - for shifted_key in keys_to_fix: - correct_key = shifted_id_map[shifted_key] - print(f" - Fixing 'iref' key [{ref_type}] {shifted_key} -> {correct_key}") - # 替换字典的键 - iref_refs[ref_type][correct_key] = iref_refs[ref_type].pop(shifted_key) - - if refs_fixed > 0: - print(f" Fixed {refs_fixed} 'iref' entries.") - else: - print(" 'iref' entries seem correct.") - - # --- END V17 FIX --- - - # 4c. 注入 ContentIdentifier (UUID) - if flat_heic_file.set_content_identifier(new_content_id): - print(f"Successfully set ContentIdentifier in flat HEIC.") - else: - print("Error: Failed to set ContentIdentifier in flat HEIC.") - os.remove(temp_flat_heic_path) - return - - # 4d. (V13 FIX) 移除与 Apple 结构冲突的项 - # (保持注释状态,因为这可能是导致损坏的原因,或者 V17 已使其不再必要) - # items_to_remove = [2, 3, 4] - # print(f"Applying V13 Fix: Removing conflicting items {items_to_remove}...") - # for item_id in items_to_remove: - # flat_heic_file.remove_item_by_id(item_id) - - # 4e. [构建] 使用我们的 HEICBuilder 重建这个*扁平*文件 - print("Rebuilding flat HEIC with new metadata...") - builder = HEICBuilder(flat_heic_file) - builder.write(output_heic_path) - - except Exception as e: - print(f"CRITICAL: HEICBuilder failed during final rebuild: {e}") - finally: - # 5. 清理 - if os.path.exists(temp_flat_heic_path): - os.remove(temp_flat_heic_path) - print(f"Cleaned up temporary file: {temp_flat_heic_path}") - - print(f"--- Conversion complete ---") - print(f"New HEIC: {output_heic_path}") - if video_data: - print(f"New MOV: {output_mov_path}") # <-- 修复了一个小的打印错误,使其指向 .MOV - print("\n" + "="*40 + "\n") - -def main(): - samsung_file = 'samsung.heic' - convert_samsung_to_apple(samsung_file) +from pyheic_struct.cli import main if __name__ == "__main__": main() diff --git a/pyheic_struct/__init__.py b/pyheic_struct/__init__.py new file mode 100644 index 0000000..7313080 --- /dev/null +++ b/pyheic_struct/__init__.py @@ -0,0 +1,23 @@ +"""Utilities for parsing and rebuilding HEIC/HEIF structures.""" + +from importlib import metadata + +from .builder import HEICBuilder +from .converter import convert_samsung_motion_photo +from .heic_file import HEICFile +from .heic_types import ItemInfoEntryBox, ItemInfoBox, ItemLocationBox + +try: # pragma: no cover - best effort metadata + __version__ = metadata.version("pyheic-struct") +except metadata.PackageNotFoundError: # pragma: no cover - local checkout + __version__ = "0.0.0" + +__all__ = [ + "convert_samsung_motion_photo", + "HEICBuilder", + "HEICFile", + "ItemInfoBox", + "ItemInfoEntryBox", + "ItemLocationBox", + "__version__", +] diff --git a/pyheic_struct/__main__.py b/pyheic_struct/__main__.py new file mode 100644 index 0000000..98a1800 --- /dev/null +++ b/pyheic_struct/__main__.py @@ -0,0 +1,6 @@ +"""Allow ``python -m pyheic_struct`` to behave like the CLI.""" + +from .cli import main + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/base.py b/pyheic_struct/base.py similarity index 100% rename from base.py rename to pyheic_struct/base.py diff --git a/builder.py b/pyheic_struct/builder.py similarity index 97% rename from builder.py rename to pyheic_struct/builder.py index ab77f9a..792e359 100644 --- a/builder.py +++ b/pyheic_struct/builder.py @@ -1,8 +1,8 @@ # pyheic_struct/builder.py import struct -from heic_file import HEICFile -from heic_types import ItemLocationBox +from .heic_file import HEICFile +from .heic_types import ItemLocationBox class HEICBuilder: def __init__(self, heic_file: HEICFile): @@ -136,4 +136,4 @@ class HEICBuilder: f.write(mdat_data) - print(f"\nSuccessfully rebuilt file at: {output_path}") \ No newline at end of file + print(f"\nSuccessfully rebuilt file at: {output_path}") diff --git a/pyheic_struct/cli.py b/pyheic_struct/cli.py new file mode 100644 index 0000000..35de9f7 --- /dev/null +++ b/pyheic_struct/cli.py @@ -0,0 +1,43 @@ +"""Command line entry points for the pyheic_struct toolkit.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +from .converter import convert_samsung_motion_photo + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Convert Samsung HEIC motion photos into Apple-compatible HEIC+MOV pairs.", + ) + parser.add_argument("source", type=Path, help="Path to the Samsung HEIC file.") + parser.add_argument( + "--output-heic", + type=Path, + default=None, + help="Optional path for the converted HEIC file.", + ) + parser.add_argument( + "--output-mov", + type=Path, + default=None, + help="Optional path for the extracted MOV file.", + ) + parser.add_argument( + "--skip-mov-tag", + action="store_true", + help="Skip injecting the ContentIdentifier into the MOV file (no exiftool call).", + ) + return parser + + +def main(argv: list[str] | None = None) -> None: + args = build_parser().parse_args(argv) + convert_samsung_motion_photo( + args.source, + output_still=args.output_heic, + output_video=args.output_mov, + inject_content_id_into_mov=not args.skip_mov_tag, + ) diff --git a/pyheic_struct/converter.py b/pyheic_struct/converter.py new file mode 100644 index 0000000..6744096 --- /dev/null +++ b/pyheic_struct/converter.py @@ -0,0 +1,212 @@ +"""High-level conversion helpers for Samsung motion photos.""" + +from __future__ import annotations + +import os +import subprocess +import uuid +from pathlib import Path +from typing import Optional + +import pillow_heif + +from .builder import HEICBuilder +from .handlers.base_handler import VendorHandler +from .handlers.samsung_handler import SamsungHandler +from .heic_file import HEICFile +from .heic_types import ItemInfoEntryBox + + +def convert_samsung_motion_photo( + source: str | os.PathLike, + *, + output_still: Optional[str | os.PathLike] = None, + output_video: Optional[str | os.PathLike] = None, + inject_content_id_into_mov: bool = True, +) -> tuple[Path, Optional[Path]]: + """ + Convert a Samsung motion photo into an Apple-compatible HEIC + MOV pair. + + Parameters + ---------- + source: + Path to the Samsung `.heic` file to convert. + output_still: + Optional output path for the converted HEIC. Defaults to + ``_apple_compatible.HEIC``. + output_video: + Optional output path for the extracted MOV. Defaults to + ``_apple_compatible.MOV``. + inject_content_id_into_mov: + When True (default), ``exiftool`` is invoked to set the + ``QuickTime:ContentIdentifier`` of the generated MOV file. If + ``exiftool`` is not available the MOV is still created without the tag. + + Returns + ------- + (Path, Optional[Path]) + Tuple containing the path to the new HEIC file and the MOV path (if one + was produced). + """ + + source_path = Path(source) + if not source_path.is_file(): + raise FileNotFoundError(f"Samsung HEIC not found: {source_path}") + + base = source_path.with_suffix("") + heic_path = Path(output_still) if output_still else Path(f"{base}_apple_compatible.HEIC") + mov_path = Path(output_video) if output_video else Path(f"{base}_apple_compatible.MOV") + temp_flat_path = Path(f"{base}_temp_flat.HEIC") + + print(f"--- Converting {source_path.name} to Apple format (V17 Fix) ---") + + original_heic_file = HEICFile(str(source_path)) + + # Extract video data if present + video_data = original_heic_file.get_motion_photo_data() + if not video_data: + if not original_heic_file.handler or isinstance(original_heic_file.handler, VendorHandler): + print("Vendor not auto-detected. Manually checking for Samsung 'mpvd' box...") + mpvd_box = original_heic_file.find_box('mpvd') + if mpvd_box: + original_heic_file.handler = SamsungHandler() + video_data = original_heic_file.get_motion_photo_data() + + # Rebuild the primary image + print("Reconstructing grid image from Samsung file...") + pil_image = original_heic_file.reconstruct_primary_image() + if not pil_image: + raise RuntimeError("Failed to reconstruct primary image using pillow-heif.") + + new_content_id = str(uuid.uuid4()).upper() + print(f"Generated ContentIdentifier: {new_content_id}") + + # Save MOV if video data exists + if video_data: + print(f"Saving extracted video data to {mov_path}...") + mov_path.write_bytes(video_data) + + if inject_content_id_into_mov: + try: + print("Attempting to inject ContentIdentifier into .MOV file (requires exiftool)...") + subprocess.run( + [ + "exiftool", + f"-QuickTime:ContentIdentifier={new_content_id}", + "-overwrite_original", + str(mov_path), + ], + check=True, + capture_output=True, + text=True, + ) + print("Successfully injected ContentIdentifier into .MOV.") + except Exception as exc: # pragma: no cover - diagnostic path + print("Warning: Could not inject ContentIdentifier into .MOV.") + print(" (This is normal if 'exiftool' is not installed.)") + print(f" Error: {exc}") + else: + mov_path = None + print("Info: No motion photo data found. Only a still HEIC will be created.") + + try: + print(f"Saving temporary flat HEIC file to {temp_flat_path}...") + pillow_heif.register_heif_opener() + pil_image.save( + temp_flat_path, + format="HEIF", + quality=95, + save_as_brand="mif1", + ) + print("Successfully created temporary flat HEIC.") + except Exception as exc: + if temp_flat_path.exists(): + temp_flat_path.unlink() + raise RuntimeError(f"Failed to save temporary HEIC file: {exc}") from exc + + try: + print("Loading temporary flat HEIC for metadata injection...") + flat_heic_file = HEICFile(str(temp_flat_path)) + + if flat_heic_file._ftyp_box: + print("Modifying 'ftyp' box to be Apple compatible (heic, MiHB, MiHE...)...") + apple_ftyp_raw_data = b"heic\x00\x00\x00\x00mif1MiHBMiHEMiPrmiafheictmap" + flat_heic_file._ftyp_box.raw_data = apple_ftyp_raw_data + flat_heic_file._ftyp_box.size = len(apple_ftyp_raw_data) + 8 + else: + raise RuntimeError("Temporary HEIC file has no 'ftyp' box.") + + print("Checking for shifted IDs in temporary file (V17 Full Fix)...") + if flat_heic_file._iinf_box and flat_heic_file._iloc_box and flat_heic_file._iprp_box: + correct_ids = {loc.item_id for loc in flat_heic_file._iloc_box.locations} + iinf_children_to_fix = [ + child + for child in flat_heic_file._iinf_box.children + if isinstance(child, ItemInfoEntryBox) and (child.item_id >> 16) in correct_ids + ] + + shifted_id_map: dict[int, int] = {} + + if iinf_children_to_fix: + print(f" Found {len(iinf_children_to_fix)} shifted 'infe' boxes. Mapping IDs for reference...") + for infe_box in iinf_children_to_fix: + unshifted_id = infe_box.item_id >> 16 + if unshifted_id in correct_ids: + shifted_id_map[infe_box.item_id] = unshifted_id + print(f" - Mapping 'infe' ID {infe_box.item_id} -> {unshifted_id}") + else: + print(" 'infe' boxes seem correct. No shift detected.") + + if flat_heic_file._iprp_box.ipma: + ipma_entries = flat_heic_file._iprp_box.ipma.entries + keys_to_fix = [key for key in ipma_entries if key in shifted_id_map] + + if keys_to_fix: + print(f" Found {len(keys_to_fix)} shifted 'ipma' entries. Fixing them...") + for shifted_key in keys_to_fix: + correct_key = shifted_id_map[shifted_key] + print(f" - Fixing 'ipma' key {shifted_key} -> {correct_key}") + entry_data = ipma_entries.pop(shifted_key) + entry_data.item_id = correct_key + ipma_entries[correct_key] = entry_data + else: + print(f" 'ipma' entries seem correct. (Keys: {list(ipma_entries.keys())})") + + if flat_heic_file._iref_box: + iref_refs = flat_heic_file._iref_box.references + refs_fixed = 0 + for ref_type in iref_refs: + keys_to_fix = [key for key in iref_refs[ref_type] if key in shifted_id_map] + if keys_to_fix: + refs_fixed += len(keys_to_fix) + for shifted_key in keys_to_fix: + correct_key = shifted_id_map[shifted_key] + print(f" - Fixing 'iref' key [{ref_type}] {shifted_key} -> {correct_key}") + iref_refs[ref_type][correct_key] = iref_refs[ref_type].pop(shifted_key) + + if refs_fixed > 0: + print(f" Fixed {refs_fixed} 'iref' entries.") + else: + print(" 'iref' entries seem correct.") + + if flat_heic_file.set_content_identifier(new_content_id): + print("Successfully set ContentIdentifier in flat HEIC.") + else: + raise RuntimeError("Failed to set ContentIdentifier in flat HEIC.") + + print("Rebuilding flat HEIC with new metadata...") + builder = HEICBuilder(flat_heic_file) + builder.write(str(heic_path)) + + finally: + if temp_flat_path.exists(): + temp_flat_path.unlink() + print(f"Cleaned up temporary file: {temp_flat_path}") + + print("--- Conversion complete ---") + print(f"New HEIC: {heic_path}") + if mov_path: + print(f"New MOV: {mov_path}") + print("\n" + "=" * 40 + "\n") + + return heic_path, mov_path diff --git a/pyheic_struct/handlers/__init__.py b/pyheic_struct/handlers/__init__.py new file mode 100644 index 0000000..6d32cc7 --- /dev/null +++ b/pyheic_struct/handlers/__init__.py @@ -0,0 +1,7 @@ +"""Vendor-specific handlers used during HEIC parsing.""" + +from .apple_handler import AppleHandler +from .base_handler import VendorHandler +from .samsung_handler import SamsungHandler + +__all__ = ["AppleHandler", "SamsungHandler", "VendorHandler"] diff --git a/handlers/apple_handler.py b/pyheic_struct/handlers/apple_handler.py similarity index 100% rename from handlers/apple_handler.py rename to pyheic_struct/handlers/apple_handler.py diff --git a/handlers/base_handler.py b/pyheic_struct/handlers/base_handler.py similarity index 88% rename from handlers/base_handler.py rename to pyheic_struct/handlers/base_handler.py index 9a60ab2..0be2c7d 100644 --- a/handlers/base_handler.py +++ b/pyheic_struct/handlers/base_handler.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: - from heic_file import HEICFile + from ..heic_file import HEICFile class VendorHandler: """ @@ -14,4 +14,4 @@ class VendorHandler: Returns the offset if found, otherwise None. """ # Default behavior: no motion photo found. - return None \ No newline at end of file + return None diff --git a/handlers/samsung_handler.py b/pyheic_struct/handlers/samsung_handler.py similarity index 100% rename from handlers/samsung_handler.py rename to pyheic_struct/handlers/samsung_handler.py diff --git a/heic_file.py b/pyheic_struct/heic_file.py similarity index 98% rename from heic_file.py rename to pyheic_struct/heic_file.py index 2997ed0..56386f1 100644 --- a/heic_file.py +++ b/pyheic_struct/heic_file.py @@ -8,16 +8,16 @@ from io import BytesIO from dataclasses import dataclass from PIL import Image -from base import Box -from parser import parse_boxes -from heic_types import ( +from .base import Box +from .parser import parse_boxes +from .heic_types import ( ItemLocationBox, PrimaryItemBox, ItemInfoBox, ItemPropertiesBox, ImageSpatialExtentsBox, ItemReferenceBox, ItemInfoEntryBox, _read_int ) -from handlers.base_handler import VendorHandler -from handlers.apple_handler import AppleHandler -from handlers.samsung_handler import SamsungHandler +from .handlers.base_handler import VendorHandler +from .handlers.apple_handler import AppleHandler +from .handlers.samsung_handler import SamsungHandler @dataclass class Grid: diff --git a/heic_types.py b/pyheic_struct/heic_types.py similarity index 99% rename from heic_types.py rename to pyheic_struct/heic_types.py index 20e802e..1cf0ed6 100644 --- a/heic_types.py +++ b/pyheic_struct/heic_types.py @@ -1,7 +1,7 @@ # pyheic_struct/heic_types.py import struct -from base import Box, FullBox +from .base import Box, FullBox from io import BytesIO from typing import List, Optional # <-- 添加 List, Optional 导入 diff --git a/parser.py b/pyheic_struct/parser.py similarity index 97% rename from parser.py rename to pyheic_struct/parser.py index 2975ad9..94da9d6 100644 --- a/parser.py +++ b/pyheic_struct/parser.py @@ -4,8 +4,8 @@ import struct from typing import List, BinaryIO from io import BytesIO -from base import Box, FullBox -from heic_types import ( +from .base import Box, FullBox +from .heic_types import ( ItemLocationBox, PrimaryItemBox, ItemInfoBox, ItemPropertiesBox, ItemPropertyContainerBox, ItemPropertyAssociationBox, ImageSpatialExtentsBox, ItemReferenceBox, ItemInfoEntryBox # <-- 导入新的 'infe' 盒 @@ -102,4 +102,4 @@ def parse_boxes(stream: BinaryIO, max_size: int) -> List[Box]: boxes.append(box) stream.seek(current_offset_in_stream + size) - return boxes \ No newline at end of file + return boxes diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5f3c810 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,30 @@ +[project] +name = "pyheic-struct" +version = "0.1.0" +description = "Low-level utilities for parsing and rebuilding HEIC/HEIF files, with Samsung-to-Apple motion photo conversion helpers." +authors = [ + { name = "Your Name", email = "you@example.com" } +] +license = { text = "MIT" } +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "Pillow>=10.0.0", + "pillow-heif>=0.15.0", +] + +[project.optional-dependencies] +full = [ + "exiftool-wrapper>=0.5.0", +] + +[project.scripts] +pyheic-struct = "pyheic_struct.cli:main" + +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = ["."] +include = ["pyheic_struct*"] diff --git a/samsung_apple_compatible.HEIC b/samsung_apple_compatible.HEIC index e5853e4..644d163 100644 Binary files a/samsung_apple_compatible.HEIC and b/samsung_apple_compatible.HEIC differ diff --git a/samsung_apple_compatible.MOV b/samsung_apple_compatible.MOV index 5bcdd72..f4eaa1a 100644 Binary files a/samsung_apple_compatible.MOV and b/samsung_apple_compatible.MOV differ