From 1a40cc236daf8252ed1f67bb9c8fe63f4485b4e6 Mon Sep 17 00:00:00 2001 From: Nymiro Date: Mon, 20 Oct 2025 22:19:40 +1300 Subject: [PATCH] feat: Implement HEIC/HEIF parsing and rebuilding utilities - Add `heic_types.py` to define structures for HEIC/HEIF boxes including ItemLocationBox, ItemInfoEntryBox, and others. - Introduce parsing logic in `parser.py` to handle various box types and their contents, including nested structures. - Implement rebuilding functionality for ItemLocationBox to adjust offsets based on new metadata. - Enhance ItemInfoBox to correctly parse and build content for ItemInfoEntry. - Update `pyproject.toml` to include project metadata and dependencies. --- README.md | 41 ++++ __pycache__/base.cpython-314.pyc | Bin 8342 -> 0 bytes __pycache__/builder.cpython-314.pyc | Bin 8206 -> 0 bytes __pycache__/heic_file.cpython-314.pyc | Bin 28196 -> 0 bytes __pycache__/heic_types.cpython-314.pyc | Bin 43427 -> 0 bytes __pycache__/parser.cpython-314.pyc | Bin 3807 -> 0 bytes handlers/.DS_Store | Bin 6148 -> 0 bytes .../__pycache__/apple_handler.cpython-314.pyc | Bin 1176 -> 0 bytes .../__pycache__/base_handler.cpython-314.pyc | Bin 1116 -> 0 bytes .../samsung_handler.cpython-314.pyc | Bin 1393 -> 0 bytes inspect_heic.py | 5 +- main.py | 225 +----------------- pyheic_struct/__init__.py | 23 ++ pyheic_struct/__main__.py | 6 + base.py => pyheic_struct/base.py | 0 builder.py => pyheic_struct/builder.py | 6 +- pyheic_struct/cli.py | 43 ++++ pyheic_struct/converter.py | 212 +++++++++++++++++ pyheic_struct/handlers/__init__.py | 7 + .../handlers}/apple_handler.py | 0 .../handlers}/base_handler.py | 4 +- .../handlers}/samsung_handler.py | 0 heic_file.py => pyheic_struct/heic_file.py | 12 +- heic_types.py => pyheic_struct/heic_types.py | 2 +- parser.py => pyheic_struct/parser.py | 6 +- pyproject.toml | 30 +++ samsung_apple_compatible.HEIC | Bin 4596991 -> 4596991 bytes samsung_apple_compatible.MOV | Bin 2908726 -> 2908726 bytes 28 files changed, 381 insertions(+), 241 deletions(-) create mode 100644 README.md delete mode 100644 __pycache__/base.cpython-314.pyc delete mode 100644 __pycache__/builder.cpython-314.pyc delete mode 100644 __pycache__/heic_file.cpython-314.pyc delete mode 100644 __pycache__/heic_types.cpython-314.pyc delete mode 100644 __pycache__/parser.cpython-314.pyc delete mode 100644 handlers/.DS_Store delete mode 100644 handlers/__pycache__/apple_handler.cpython-314.pyc delete mode 100644 handlers/__pycache__/base_handler.cpython-314.pyc delete mode 100644 handlers/__pycache__/samsung_handler.cpython-314.pyc create mode 100644 pyheic_struct/__init__.py create mode 100644 pyheic_struct/__main__.py rename base.py => pyheic_struct/base.py (100%) rename builder.py => pyheic_struct/builder.py (97%) create mode 100644 pyheic_struct/cli.py create mode 100644 pyheic_struct/converter.py create mode 100644 pyheic_struct/handlers/__init__.py rename {handlers => pyheic_struct/handlers}/apple_handler.py (100%) rename {handlers => pyheic_struct/handlers}/base_handler.py (88%) rename {handlers => pyheic_struct/handlers}/samsung_handler.py (100%) rename heic_file.py => pyheic_struct/heic_file.py (98%) rename heic_types.py => pyheic_struct/heic_types.py (99%) rename parser.py => pyheic_struct/parser.py (97%) create mode 100644 pyproject.toml 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 050e88f625ac4fb3b238cab1ef6ed168dc57e55d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8342 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 L_x&eB#kcw&pQhsd diff --git a/__pycache__/builder.cpython-314.pyc b/__pycache__/builder.cpython-314.pyc deleted file mode 100644 index feb6d2948184210a8db4faa12e7fa08a6034980c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8206 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 z zn(N~&Tl%gv_bplvvA&~==D^J0Z}nzkVA3?Vb8Z9MF|aVbFv&U}W6KW44Q|i?%;o{8 zGvnu{Va*=5wOy}Vb$q%3vfn8x}0;>sW0a+>Tn* z08sxJK>`0)joWf}YBj&s7VNa-k;w`pLmW<<vOxr2O!p@H%4`-1dz=C*0T7ro=kOii=JEX`WKZtFb zf}3_SZ)4_ng6_}TQ9mqjcUaf2;w=!5h46G49X=X@%^@TfcqWi15e}X*Y54^gSwLCH zHH3T~H_7AX(-Oj!B2o-q9>^%b0~SU!0UC@z0+hCg#FZjxNrQ)VkXnUrZxHDGVc`3SBylEx}HiWJe*a$i5jO)6x+)yl^duGe& zCr>^3mrvi(z>ULj*D$$a?@lhfToqpa$Qz9{)od@<{Jgwi`~dbA8|2LxOitF=5k6yYfE(MNh~r!zA=4ljc5?3a5u8c7xU#v=vYf z-pQxQy-lp|nLEP>9`q$yjp(y2@W@6^W^H5(D6EaJU8ELkn|fx0=v2!$0SJ7+<+~S& zVcaPC^f38_OR5J!TLTN1w5wXM?y^4Crbq>DEq(GnN^ep6c2C;9sCU;14f*rZ7E@3s zmpa3MNF%GIXqJzL_vx2hN#rxt|Ea|hZY+uTk>y$zfaXlQmc^Bm);C3H=$~#tkG*F% zOgE&TkDyA3b(xx;2#@-y7r?3GgP`#DW|S#0nP! zl<^rx(#_s!IB|{QsUXJMMNzXM1E^!VjnDr;BY{*+r+H|Zpqgp;RS^_tCF}4Aocre4 zij+CkoQ6Tu4OzL`!YE84D6ovg-U*dEK@nAWS6thDmvI*1ZCm2HtwbD^TaWLK z>-Jz|LGc;WxvH~OiSl)^@^$fobtKD#=T*4lR?2Uwb;fL+m!4d*^~MW&uh%rq zF<`X>XF-$t8rjLs&L#Vl^hnWUjn;v`;w(^rtr6VE0L+!%x#!!V%HyJOWbkY<5(ke-T8e>=7t#^Hrmj6 z>G-85*lkXB)Xh2{XNx>>?V;=CwnTYTth{M%&tmz8nWCEo#E9OsV`0xiFS~D)Eg6gJ z9!?r-SX~V{!PvJzPQnTwpb)|!VQX{)q668)0L_YU5Bg>d^`N%}y*BiS@)@-fb_-LF z5lE_-M)d6H?M1I2y~oh=p@$n`41?Ye(R&lUx6zBC_xI@i3cc6SyNMnOTLc@KLKt|RJ8QG>@>q>sWLhxFNmz{}7h!?qejb?DWA$7wh5zxmY5 z)WDd-e=0Ho-Yt~^ekCf0Qsn=JD)>EB@*Y+89#!)mRk}=>-lMASDAfw(9SXb^^!_QI pQmlQCYJHDtzplu~k7gJIg?U9qDGP4l>mA*;T!m$YLZ6iW{{U$MU!nj2 diff --git a/__pycache__/heic_types.cpython-314.pyc b/__pycache__/heic_types.cpython-314.pyc deleted file mode 100644 index c928ed1617e5d8018d5745fd4834a529782e6085..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43427 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 J^G=+Z{~s-OR_Oo$ diff --git a/__pycache__/parser.cpython-314.pyc b/__pycache__/parser.cpython-314.pyc deleted file mode 100644 index d1fbb581ffb88e2daf045102d1d655a8f6005888..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3807 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 diff --git a/handlers/__pycache__/samsung_handler.cpython-314.pyc b/handlers/__pycache__/samsung_handler.cpython-314.pyc deleted file mode 100644 index 569224af6d3dc693903b1d0aa19f8d156f933df5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1393 zcmZ`(&2QX96rWjpH*V_elI#{L3hF@AY*$U!5=4|kL@E_ff<)9hVh@F6qJfVlR+g(C-~a^hcDL=mb50*O;^MO)e)`^NS{DD9Kx&GY-5`Mozj*IbxK zKwnP(j+P9B{?s2cs#n7FEC^$?hh4mdHnGB|(Fybda%;~Z*Vx5t5T{AiTyq)q>bEg` z^l-i~U|GP$rk4c?6Mo@Sv0AlHUje~`+`C9&7c%S_%6J-a8jHrka zmhQ74U;#;o*8;N7Kkobb@Oo*|7t?Fn2eyvKXn^)=uYQ710NUr!6&wL1JNR9IVs#E& zDsnI!&$z8GnKuN3qS-d!VZ@p0)$&HjMe3=1fl@#5qzqzTQ7X0fi{t!F=N-v}bh3jq z7TkHA`=gX)N;*Cdm@_1pR3a-KS>iw|MSpmZ+mw1)#+9cSrTMA+?Iz6Mn=b zfd&-a@9M61b=%2DQS3*==b7^2OcvH_alkkUJmvN0b5pw>E>|#;BatB=WdZRN;bAD5 zB8Se*sTjI|DdsB{tdjkaA{qZ*Xn(tB3qr-?RPRks5_mSp5$*~xOEyy)ScWl&o{+ITdAKPcH zhd;H?e`fuDy#1@aIO$xtVg1nhzBRe9Ia%7eX}|SH`_xw(Uu;~z@^gD-Vz1n7pkvKR zbK|CU@m8xXT9EA5(r^TR7>K*!!Mjvlk_FSefKmE$Mreuv_cpWnL4@pwks)2iP40WBeQH Q-Zd9-=kwir2(}{XAxc4G%m4rY 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 e5853e471ae204a36d2064363c85bbec9b34f2f5..644d163958bdfcaa1597b4d641a3d958c87a3679 100644 GIT binary patch delta 332 zcmWl|M^?g6006)T8hh_u6zoP3J|VH2@V%G|w=TVjXYd$a!wrt}-VDD#>q9?&S~0Zq zSQg#16JwQzD)iDKhJIQOvMk0tlvya>lG=Mx`(Jm&Q4_};cfv`hoOZ@p=bU%JMVDN5 z#Z}i_cf(D$+;+!Z_uRMPfrmD2dE~Jto_glF7halr<+W{Z?ASHfGqZ1Q;jMSx`{1Kb QKKo*6<-qur)Lws^f3OljnE(I) delta 332 zcmWl|Nm9Z<06@_S(#!}bf+#8wa6np!NjgNC>~#8KEZDO2CZ2&~a1A&3eO2#t_|BZT zKMp-#tI(LaOd~&(fk|Q+>%d5*t=3lA!0Ko#oSks~XL!ae1#`@^z#>a5v%)IRS!10I zUhtAlUa`eCui4=ZZ`tJ?@A<$U`y9~akRv|wiO+oDn6I2r@{Lo@=<%IC1BO(LIA_cc WesaMjS6p+$FKQ+^nJ_*5L?DovR2PJz@unQG5W&vu?#K)6f@O`U*ucw-c*}G!B zqdtiE+W6+AJuzo(FODPP8tWK)57i@7{}DzRW1I;lnPQq5W|?E21r}LinH5%9W1S5) i*7QA%mUP&iO;)x(f2JPzMjH9I?DI7 zKFLeqc+%D9P=?CY(pN?);~Uqg;2w)dEdCQrGQ~7A%reJ33oNq4GApdI#yT5pvc)z# j?6Su`2OM(5F(;hT;*4`HXmiOG*W7T+9rvGD9B$ViXVp&+