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.
This commit is contained in:
Nymiro
2025-10-20 22:19:40 +13:00
parent b3f0fd4ce3
commit 1a40cc236d
28 changed files with 381 additions and 241 deletions
+41
View File
@@ -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
```
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -2
View File
@@ -1,9 +1,10 @@
# pyheic_struct/inspect_heic.py # pyheic_struct/inspect_heic.py
import sys import sys
from heic_file import HEICFile
import pprint # 用于漂亮地打印字典 import pprint # 用于漂亮地打印字典
from pyheic_struct import HEICFile
def inspect_file(filename: str): def inspect_file(filename: str):
print(f"--- 正在侦察: {filename} ---") print(f"--- 正在侦察: {filename} ---")
@@ -107,4 +108,4 @@ if __name__ == "__main__":
print("用法: python inspect_heic.py <文件名>") print("用法: python inspect_heic.py <文件名>")
print("例如: python inspect_heic.py samsung.heic") print("例如: python inspect_heic.py samsung.heic")
else: else:
inspect_file(sys.argv[1]) inspect_file(sys.argv[1])
+1 -224
View File
@@ -1,227 +1,4 @@
# pyheic_struct/main.py from pyheic_struct.cli import main
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)
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+23
View File
@@ -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__",
]
+6
View File
@@ -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()
View File
+3 -3
View File
@@ -1,8 +1,8 @@
# pyheic_struct/builder.py # pyheic_struct/builder.py
import struct import struct
from heic_file import HEICFile from .heic_file import HEICFile
from heic_types import ItemLocationBox from .heic_types import ItemLocationBox
class HEICBuilder: class HEICBuilder:
def __init__(self, heic_file: HEICFile): def __init__(self, heic_file: HEICFile):
@@ -136,4 +136,4 @@ class HEICBuilder:
f.write(mdat_data) f.write(mdat_data)
print(f"\nSuccessfully rebuilt file at: {output_path}") print(f"\nSuccessfully rebuilt file at: {output_path}")
+43
View File
@@ -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,
)
+212
View File
@@ -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
``<source>_apple_compatible.HEIC``.
output_video:
Optional output path for the extracted MOV. Defaults to
``<source>_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
+7
View File
@@ -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"]
@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from heic_file import HEICFile from ..heic_file import HEICFile
class VendorHandler: class VendorHandler:
""" """
@@ -14,4 +14,4 @@ class VendorHandler:
Returns the offset if found, otherwise None. Returns the offset if found, otherwise None.
""" """
# Default behavior: no motion photo found. # Default behavior: no motion photo found.
return None return None
+6 -6
View File
@@ -8,16 +8,16 @@ from io import BytesIO
from dataclasses import dataclass from dataclasses import dataclass
from PIL import Image from PIL import Image
from base import Box from .base import Box
from parser import parse_boxes from .parser import parse_boxes
from heic_types import ( from .heic_types import (
ItemLocationBox, PrimaryItemBox, ItemInfoBox, ItemPropertiesBox, ItemLocationBox, PrimaryItemBox, ItemInfoBox, ItemPropertiesBox,
ImageSpatialExtentsBox, ItemReferenceBox, ItemInfoEntryBox, ImageSpatialExtentsBox, ItemReferenceBox, ItemInfoEntryBox,
_read_int _read_int
) )
from handlers.base_handler import VendorHandler from .handlers.base_handler import VendorHandler
from handlers.apple_handler import AppleHandler from .handlers.apple_handler import AppleHandler
from handlers.samsung_handler import SamsungHandler from .handlers.samsung_handler import SamsungHandler
@dataclass @dataclass
class Grid: class Grid:
@@ -1,7 +1,7 @@
# pyheic_struct/heic_types.py # pyheic_struct/heic_types.py
import struct import struct
from base import Box, FullBox from .base import Box, FullBox
from io import BytesIO from io import BytesIO
from typing import List, Optional # <-- 添加 List, Optional 导入 from typing import List, Optional # <-- 添加 List, Optional 导入
+3 -3
View File
@@ -4,8 +4,8 @@ import struct
from typing import List, BinaryIO from typing import List, BinaryIO
from io import BytesIO from io import BytesIO
from base import Box, FullBox from .base import Box, FullBox
from heic_types import ( from .heic_types import (
ItemLocationBox, PrimaryItemBox, ItemInfoBox, ItemPropertiesBox, ItemLocationBox, PrimaryItemBox, ItemInfoBox, ItemPropertiesBox,
ItemPropertyContainerBox, ItemPropertyAssociationBox, ImageSpatialExtentsBox, ItemPropertyContainerBox, ItemPropertyAssociationBox, ImageSpatialExtentsBox,
ItemReferenceBox, ItemInfoEntryBox # <-- 导入新的 'infe' 盒 ItemReferenceBox, ItemInfoEntryBox # <-- 导入新的 'infe' 盒
@@ -102,4 +102,4 @@ def parse_boxes(stream: BinaryIO, max_size: int) -> List[Box]:
boxes.append(box) boxes.append(box)
stream.seek(current_offset_in_stream + size) stream.seek(current_offset_in_stream + size)
return boxes return boxes
+30
View File
@@ -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*"]
Binary file not shown.
Binary file not shown.