From e9c4f17f6e1eb66d9bdf4a3da1e44c0af78bf6bc Mon Sep 17 00:00:00 2001 From: nymiro Date: Thu, 23 Oct 2025 18:12:08 +1300 Subject: [PATCH] =?UTF-8?q?=E6=95=B4=E7=90=86=E6=96=87=E4=BB=B6=E5=A4=B9?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .venv/bin/python | 1 + .venv/bin/python3 | 1 + .venv/bin/python3.12 | 1 + .venv/lib64 | 1 + .venv/pyvenv.cfg | 5 + README.md | 180 +------------ dist/pyheic_struct-0.1.0-py3-none-any.whl | Bin 0 -> 33050 bytes dist/pyheic_struct-0.1.0.tar.gz | Bin 0 -> 32100 bytes README.en.md => docs/README.en.md | 4 +- docs/README.md | 181 +++++++++++++ apple.HEIC => examples/apple.HEIC | Bin apple.MOV => examples/apple.MOV | Bin .../apple_samsung_compatible.heic | Bin samsung.heic => examples/samsung.heic | Bin .../samsung_apple_compatible.HEIC | Bin .../samsung_apple_compatible.MOV | Bin pyheic_struct.egg-info/PKG-INFO | 247 ++++++++---------- pyheic_struct.egg-info/SOURCES.txt | 1 + pyheic_struct.egg-info/requires.txt | 1 + pyproject.toml | 2 +- inspect_heic.py => scripts/inspect_heic.py | 4 +- main.py => scripts/main.py | 0 test.py => scripts/test.py | 11 +- 23 files changed, 323 insertions(+), 317 deletions(-) create mode 120000 .venv/bin/python create mode 120000 .venv/bin/python3 create mode 120000 .venv/bin/python3.12 create mode 120000 .venv/lib64 create mode 100644 .venv/pyvenv.cfg create mode 100644 dist/pyheic_struct-0.1.0-py3-none-any.whl create mode 100644 dist/pyheic_struct-0.1.0.tar.gz rename README.en.md => docs/README.en.md (97%) create mode 100644 docs/README.md rename apple.HEIC => examples/apple.HEIC (100%) rename apple.MOV => examples/apple.MOV (100%) rename apple_samsung_compatible.heic => examples/apple_samsung_compatible.heic (100%) rename samsung.heic => examples/samsung.heic (100%) rename samsung_apple_compatible.HEIC => examples/samsung_apple_compatible.HEIC (100%) rename samsung_apple_compatible.MOV => examples/samsung_apple_compatible.MOV (100%) rename inspect_heic.py => scripts/inspect_heic.py (96%) rename main.py => scripts/main.py (100%) rename test.py => scripts/test.py (75%) diff --git a/.venv/bin/python b/.venv/bin/python new file mode 120000 index 0000000..b8a0adb --- /dev/null +++ b/.venv/bin/python @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/.venv/bin/python3 b/.venv/bin/python3 new file mode 120000 index 0000000..ae65fda --- /dev/null +++ b/.venv/bin/python3 @@ -0,0 +1 @@ +/usr/bin/python3 \ No newline at end of file diff --git a/.venv/bin/python3.12 b/.venv/bin/python3.12 new file mode 120000 index 0000000..b8a0adb --- /dev/null +++ b/.venv/bin/python3.12 @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/.venv/lib64 b/.venv/lib64 new file mode 120000 index 0000000..7951405 --- /dev/null +++ b/.venv/lib64 @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/.venv/pyvenv.cfg b/.venv/pyvenv.cfg new file mode 100644 index 0000000..5ff1fa6 --- /dev/null +++ b/.venv/pyvenv.cfg @@ -0,0 +1,5 @@ +home = /usr/bin +include-system-site-packages = false +version = 3.12.3 +executable = /usr/bin/python3.12 +command = /usr/bin/python3 -m venv /home/nymiro/code/pyheic_struct/.venv diff --git a/README.md b/README.md index bc138b1..a6e3ccd 100644 --- a/README.md +++ b/README.md @@ -1,181 +1,5 @@ # pyheic-struct -`pyheic-struct` 诞生的初衷,是为了解决我个人在迁移照片时遇到的需求:把三星手机拍摄的动态照片(Motion Photo)批量转换成 iOS/macOS 可以直接识别、导入的 Live Photo。 -工具会自动拆分并重建 HEIC 与 MOV 配对文件,补齐 Apple 生态所必需的 `ContentIdentifier`、`PhotoIdentifier` 与 MakerNote 元数据,让 macOS「照片」或 iOS 设备将它们视为同一张 Live Photo,而不是互不关联的静态图与视频。 +Primary documentation now lives in `docs/README.md` (Chinese) and `docs/README.en.md` (English). -> Looking for an English version? Please see [README.en.md](README.en.md). *(本 README 以中文为主,当需要英文说明时可跳转到该文件。)* - -同时,转换流程也促使我编写了一套可复用的 HEIF/HEIC 结构化解析、诊断与重建工具:它能读出盒结构、修复三星特有的 shifted item ID、重算 `iloc` 偏移,并以可预测的方式重新写回文件。即便你暂时不需要完整的 Live Photo 转换,也能够将其中的模块用在其它 HEIC 调试或自定义处理流程中。 - ---- - -## 功能亮点 - -- **三星 Motion Photo → Apple Live Photo**:自动生成匹配的 HEIC 与 MOV,并写入统一的标识符;导入后即是一张可播放的 Live Photo。 -- **完整元数据补齐**:在 MOV 中注入 `ContentIdentifier`,在 HEIC 中写入 MakerNote / PhotoIdentifier 等 Apple 生态所需字段。 -- **HEIC 深层解析能力**:提供 `HEICFile`、`HEICBuilder` 等类,可读取 `ftyp`、`meta`、`iloc`、`iinf`、`iprp`、`iref` 等关键盒,适用于诊断和自定义修复。 -- **安全重建机制**:`HEICBuilder` 会自动重算偏移量与引用关系,避免手工编辑造成的结构损坏。 -- **CLI 与 Python API 并存**:既能在命令行一键转换,也可以在脚本、流水线甚至 GUI 中复用底层逻辑。 -- **真实样例随仓库提供**:包含原始三星 HEIC、转换后的 Apple 兼容文件与苹果原生样例,便于对照调试。 - ---- - -## 组件概览 - -| 组件 | 作用 | -| --- | --- | -| `scripts/samsung_live_photo.py` | 命令行转换脚本,输出苹果兼容的 HEIC + MOV | -| `scripts/batch_media_manager.py` | 图形化批处理工具:扫描目录、转换 Motion Photo、匹配 Live Photo 配对、清理由 MotionPhoto_Data 标记的冗余视频,并可归档原始素材 | -| `pyheic_struct.HEICFile` | 解析 HEIC 盒结构、重建主图、提取 Motion Photo 等 | -| `pyheic_struct.HEICBuilder` | 在修改元数据后重新写出 HEIC,确保偏移正确 | -| `pyheic_struct.convert_motion_photo` | 通用转换入口,可指定厂商与目标适配器 | -| `pyheic_struct.AppleTargetAdapter` | Apple 生态适配器,负责写入品牌、MakerNote、ContentIdentifier 等 | -| `pyheic_struct.handlers.SamsungHandler` | 三星专用处理逻辑:识别 `mpvd`、校正 shifted IDs | -| `inspect_heic.py` | 快速巡检脚本,输出盒结构、偏移与引用信息 | - ---- - -## 安装与环境 - -- **Python 3.10+** -- 必装依赖:`Pillow>=10.0.0`、`pillow-heif>=0.15.0` -- 新增依赖:`piexif>=1.1.3`(用于 MakerNote 写入) -- 可选依赖:`exiftool-wrapper>=0.5.0` -- 推荐系统安装 [ExifTool](https://exiftool.org/) - - macOS: `brew install exiftool` - - Ubuntu/Debian: `sudo apt install libimage-exiftool-perl` - - Windows: 下载官方 exe 并加入 `PATH` - -安装方式: - -```bash -# 仓库根目录安装 -pip install . - -# 或开发模式(含可选依赖) -pip install -e .[full] -``` - -如仅需解析能力,可忽略 `.[full]`;若要写入 MOV 的 `ContentIdentifier`,请启用 `full` 并确保系统可调用 `exiftool`。 - ---- - -## 快速开始:把三星动态照片变成 Live Photo - -### 1. 准备样例 - -仓库中已经附带常用测试素材: - -- `samsung.heic`:原始三星 Motion Photo -- `samsung_apple_compatible.HEIC` / `.MOV`:转换后的示例输出 -- `apple.HEIC`:来自苹果的原生 Live Photo 参考 - -### 2. 命令行一键转换 - -```bash -python3 scripts/samsung_live_photo.py samsung.heic \ - --output-dir output/live -``` - -生成的 `output/live/samsung_apple_compatible.heic` 与 `.mov` 拥有同一个 `ContentIdentifier` 和 `PhotoIdentifier`,可直接导入 macOS「照片」验证。 - -常用参数: - -| 参数 | 说明 | -| ---- | ---- | -| `source` | 必选,原始三星 HEIC | -| `--output-dir` | 输出目录,默认与源文件相同路径 | -| `--heic-name` / `--mov-name` | 输出文件名(不含路径),默认 `*_apple_compatible` | -| `--skip-mov-tag` | 无 `exiftool` 时跳过 MOV 的 `ContentIdentifier` 注入 | - -### 3. 通过 API 集成 - -```python -from pathlib import Path -from pyheic_struct import convert_samsung_motion_photo, HEICFile, HEICBuilder - -heic_path, mov_path = convert_samsung_motion_photo( - "samsung.heic", - output_still=Path("converted/apple_ready.HEIC"), - output_video=Path("converted/apple_ready.MOV"), -) - -# 读取重建后的 HEIC,执行进一步操作 -rebuilt = HEICFile(str(heic_path)) -rebuilt.set_content_identifier("MY-INTERNAL-ID") -HEICBuilder(rebuilt).write("converted/customized.HEIC") -``` - ---- - -### 4. 图形化批处理工具(GUI) - -`scripts/batch_media_manager.py` 提供完整的桌面界面,便于批量巡检目录: - -- 递归扫描 HEIC/MOV/MP4 等媒体,并以进度条与日志反馈处理情况; -- 检测 Live Photo 配对,自动移动缺失伴侣的孤立视频到安全子目录; -- 将三星 Motion Photo 转换为 Apple 兼容的 HEIC + MOV,并补写 `ContentIdentifier`; -- 清理带 `MotionPhoto_Data` 标签、与 Motion Photo 重复的冗余 MP4; -- 可选地把成功转换的原始素材按照原路径层级移动到 `归档/`(或 `Archive/`)并打包 zip; -- 内置“停止”按钮和窗口关闭钩子,可安全取消长时间任务。 - -运行示例: - -```bash -python3 scripts/batch_media_manager.py -``` - ---- - -## Live Photo 转换管线解读 - -1. **解析原始 HEIC**:读取 `ftyp`、`meta`、`iloc` 等盒;通过 handler 自动识别三星文件并查找 `mpvd` 盒。 -2. **重建主图像**:借助 `pillow-heif` 还原三星的网格化主图,写入临时平面 HEIC(`mif1` brand)。 -3. **提取并存储 MOV**:抽取 `mpvd` 中的嵌入视频,写成独立 MOV,并用 `exiftool` 写入 `ContentIdentifier`(如未安装可跳过)。 -4. **修正 item ID 与引用**:三星会将 item ID 左移 16 位;工具会同步修复 `iinf`、`ipma`、`iref`。 -5. **补齐 Apple 元数据**:设置苹果兼容的 `ftyp` 品牌,写入新的 `ContentIdentifier`、`PhotoIdentifier`,填充 MakerNote。 -6. **最终重建**:`HEICBuilder` 重新计算 `iloc` 偏移、盒大小并写回,释放临时文件。 - -了解以上步骤,有助于调试其它厂商的 Motion Photo,或在需要时定制新的 TargetAdapter。 - ---- - -## 核心 API 快速索引 - -- `pyheic_struct.convert_motion_photo(...)`:通用转换接口,可指定 `vendor_hint`(如 `"samsung"`)与 `TargetAdapter`。 -- `pyheic_struct.convert_samsung_motion_photo(...)`:三星专用封装,调用更简洁。 -- `pyheic_struct.HEICFile`: - - `get_primary_item_id()`、`reconstruct_primary_image()`、`list_items()` 等解析方法 - - `set_content_identifier()`、`set_exif_maker_note()` 等元数据操作 -- `pyheic_struct.HEICBuilder`:将修改后的 `HEICFile` 安全写回磁盘。 -- `pyheic_struct.handlers.VendorHandler`:厂商自定义扩展基类。 -- `pyheic_struct.targets.TargetAdapter` / `AppleTargetAdapter`:目标生态适配接口。 - ---- - -## 扩展与自定义 - -1. **实现新的厂商 Handler**:继承 `VendorHandler`,实现 `matches`、`extract_motion_video`、`prepare_flat_heic` 等方法,并注册到 `HANDLER_REGISTRY`。 -2. **接入其它目标生态**:继承 `TargetAdapter`,在 `apply_to_flat_heic` / `post_process_mov` 中写入所需元数据或外部脚本。 -3. **诊断 HEIC 文件**:使用 `inspect_heic.py` 或直接调 `HEICFile` API,输出盒结构、偏移与引用,便于对比不同厂商实现。 - ---- - -## 常见问题 - -- **导入照片后仍显示两个文件?** - 请确认系统安装了 `exiftool`,并在转换时未使用 `--skip-mov-tag`。同时建议使用工具生成的最新 HEIC/MOV,避免手动修改导致标识符不匹配。 - -- **能否支持其它厂商的 Motion Photo?** - 可以。通过实现新的 handler 并组合自定义 target adapter,即可重用现有的重建管线。 - -- **英文说明在哪里?** - 参见 [README.en.md](README.en.md);若你想贡献改进英文文档,也欢迎提交 PR。 - ---- - -## 开发提示 - -- 项目使用 `ruff` / `black` 风格指南,可根据需要自行配置。 -- 在修改 HEIC 结构后务必重新运行转换脚本,确保 `HEICBuilder` 输出的文件能被 `pillow-heif` 与 `exiftool` 读取。 -- 欢迎围绕 HEIC 解析、Motion Photo 适配提交 issue 或 PR,特别是其它安卓厂商的兼容性案例。 +For installation and usage details, please consult those files. diff --git a/dist/pyheic_struct-0.1.0-py3-none-any.whl b/dist/pyheic_struct-0.1.0-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..bd5f5a26eab57dd0a03a142c4d7eea8111313be6 GIT binary patch literal 33050 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 literal 0 HcmV?d00001 diff --git a/dist/pyheic_struct-0.1.0.tar.gz b/dist/pyheic_struct-0.1.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..fc0a99bb1b84175e156462c53455898de83fe3c6 GIT binary patch literal 32100 zcmcecQ*$OvxUOT{ww+8cu|07nwr$(C?c|MZ+qRvFZNB?kYyF4a)zt@2^+9)ab=6(h za}h_wz%aDGTYv!_JS|KujrE;foLr4v7+IKDnOK-y44pu(daT_sB$HV9@;}v&o9B2h zIBO)(ME3Q#<_wsOb0Md}@_bM0d1{p1VOQqcuHg$ze?!|i|1!Ay4r5*)^|(4XTqDY^ zdS{+1*IuLshH{aS|DtcAtLmg=`Ng}mNl5IZv&o$bPSp9D@ zLGmZ`=iBk|PDh}-j{pxFC(jGL+p)UBK48a&8}aN%9hfC(jIUXG@^|~|EG%+i!JJBh z^N$IaO%9H>V5h)+)xezKw}P+V>~mD8S=9nlYnWtX@kU}x-|)v?y&?Y(Z>QmR#7~{p zWASC+<7y2)u_p3fY+U@G>Q78W?lU4+aQX&DPXcr8rf9ht&EH<_iqBUnxjEAa z6|S7k+Qe-9YXrO}0(=X;ucwH@x+G^lN) ztRGUvDIlL|}w#@MYdN1=leGgvN z;N5TCnEYFw?uHLryrZe>81wjdd|$&)&;h*WcEVdHQ)69wy-$zd$ene)oerNiuRRK# z=5wyhd)Ikg^S{njuIxyANA>L!Ols-cL$Of^~ zv8vo;L8fh>!THnL`U$s%W2N(H_{kDB&+CoTt}%R@zA>-<8^5-IXQA_>{A=zK{HtGp z60>ec=hr?O`3-m!Nas$|7%#HI;Cg+PG9M~o4qUZc0ZBbSLVIa_w}1+(ePfJ{#l+Ubof-7-PiK_q08xN#cXUk_RY8H`6g{f z@NDTLd|SNxjejK~1}I_p7`8*!O)qGli@aZ(%OPVtN zO7C~(7$UZejL@yM#DIRrwp&o6Txff+mb6~J2A3Ock6xF@&l~SL(ENS|rtdexX=aD1 z#hEHG=YGa-7cx-e5M~Ee_HTdwhPRcDP~Ln7J|EA60a1hAk)UvdV^rmX+Cu;5p1GKyEnX+FdTMpST zbJ}6(!N&Yfp-Es2+P*gEclz>zTX)=VTyFG3=DKdTgHT-rzBCZKoe#P~^NE=F-kLuL zWLdm2#EP^}N8fL@_Of$kB3R}j6(Lz}eePi!C!{T03BBJhWKN6S1BvSS4oG9-)_u-b zr)0VIpk4+`9gc!SdChtCyBj^uQ%LTvFLSuU!o!6n_(pi@$j(B$xUlp)!4)=^6wpaC zdX+bT{Lf(;Bm!Ovg5OO#cI{TPIu|*8;IgvQaawj_q!i%Q+ur&8I%GHbVDULxUsLIV zd%y%N*V9F7A$+F5gxkQJ9j;pUy${;vb?!JZrj}6)Sm%%c9G^t;U_WS=4lC`x2y`Fn z`EL1f%V>8-_-eaWk8c}Ak^W)V+Ps;- zpsrC`Fla;l*DB(^14ygS7v+9JXjk1r(OI{no+D&KkAjlLD`pK&!d~t#L5FTRf>fW< zQXyFoh~Ix)Pk*+?qp16|zmT9Cd=3pyk0ICL4W0*h^ghm?Pmcb(e!t6N=cV3T$>W0f z#%CH~a*n=*zFD@r0-H(JC46LJvbWv zb@yu9H<>C<Rlptad2ebF zeKq7Q)GUm)?Th$AwvJ)3f+y>Ize_*g{z>Vulqq(oaA^NR-Fmmrh7kA-n>Wz*=L`EM zi;APHqg7h#Z{c716vKOrcFxqAIfbFIsE81b1^AIW1^XZaj0mTz6=l7l^pu66m?gSd zOgg&>XE;p21%U%T+E2KkE0GTr7%r!K5}yp1`<&lWkB8&^5dMEZm%b4<35v^;Ny3F& z3ZGN{{5l!L?!Lxe9RH`Y9Irv&gx8ffI|b`N<~N z-JVAGSDpoFN%eQ$Qgo6Fwn$eIY*s&msvdbiqqCNnOxu3w>g?tmS_|#0j=70)>KEZM zG(=tbdZ33Dy1q`<$sZ4uGo9KtnDvqq{m6Zq0zcBHWAr<7@z84Fv#{*sGEa3m98&l0)aIoQKV_AUwFv$XoLv=yl=BG7x#r-S^$n+~LStC3x zGJwxZ!`@;&l|ekOXp+9V%)3j#ZtQo^pwn0N9?xLoK4H(x%3`Q2$+{u1Ot|5DYyZ*r z?p@WYcBPWb*?LU|WJBwyf72zX!BE-D*~Y8vfXGbfJWYj(9^EX<|KadY*QQn7&c&<; z5{}8zj9bNMK-coAsr_hGfCsJ`446~Vy}#Gr`l&2#gAiv~WFi;4Zsu-cx|^&T&Mt71R-=w}qG2sO4;6AqB-Zz+H-MAw9BFU0up#ArCCI(i!M+dU#px zNt*Dhoe;-_&$=1UP18*<%UER{x8UwX7XEdlYaI!pgzmmc4D1{6#!lb!FLnN$n!Vnd=Pw_tpGX9OdSK*h z5BP%p&5fBrHq%njdxr}m@<5BXx*N&J7!B`==Wgsvyn0es^LId)^)JmToh#sECPkMi z79hNkJqu=un%5i7?B|FbCL+5FGB%*7O|c}YE=*WNi0)3*+jWVretIY?JEl*?6Kk6J z*Y_T`VOD&IXVn_rTrH~}I7rE=W8HRfz3MPw&GO;O*h=|B&XL=c_`;MWk+ZDBoIR^% z6<((Ky8X94Zv3}y`*%w$K5lsig9!n8ax#a)hu=Dh-Vff}8-%&+AyxsAO-v5z&+r-Y zc1DLSZ7Ol$x70IW)eb{9>44f$)2L0@HMVydcBIXlbT{&ImU++H{(Kah<>6*Wwq%IW z&jWw7=jmu>R-sM`Wjj~dm5#~0m$gqFp zhD6OENIZ8!_jJ{=bu88vqJI?j$1a1u?1`vTQf-JyhHBLrS0NZB8UfNvvLTDm5B}6x zkPK+}ue9byf3_k0#cD9GdeeWXm#WG_F0##D@Q7=)GmjiOfAC}H-_@dRA;d;GEw>TJ zCD56m7QaFVSNXjWTQ0+i2kFlB`Zj$Z!Dy~vA3|CUB9e$`NsMHaNORNjO|tFaJpvVs z2UH$2WNyBO?v87Hw_dI`yNx}Ztl4ts&oa?smo|nF>rO7nXLuX8f}k#2V{Vsdy4~R?O%{{D z+a|y9MM1HMZMVl!EwmI@(qTDGvR*&X)}`nBN19Bj=xuNP#lYLO?^v_sTw4Oq)j*r6;kf?8)+p)v@y9wz1MSf=R9*bWu z&ESW)^uql?x7*YEHJTsTD}qkur0)ax9LhXtBQ9!s-?_!U0fkb3+pT}PL#t-mEJWmI zYWLlIU*7+&Z?e4~O&#e%78VRX$(0JQdwg0dsYXqFV^2Dw-xVL);XVs|R~*+e9AP=+ zZIllg%6Wky*`-$RGWSQLg2&7Ky~`>l%*DgYtWRMNab{ijlX*Tmz^?7}MnpoVlUXpv z=h5?d?yIr7i~&2nG7-A9SM2;U>t4?py3F%S_ufnru<-{RC3IrAcg=9m=Dhq zHc{o@3Um%g2xA8YuRlNXyzkcM@@Khy&L=zrb^fCqhw3`TOQ-`B`@fr$I%!hu-s5lp zq}x#hVb;9CDl9dl$|{beQbxE?l{R3RI+wT)!q2Q29bs9)MQ}11{7F*3PezZm>~oDS z?pA*&Zq2!NUI8%$R-xMQh}z(zfIRefXd+{^8JJBwW)7=C6aAixe4a@b<+rj?(A?6= zQS|F93P3~0{a8~2>;ylj#$@t)uz%^DD!~j`Haxw4`yv1~1iZhcsxj{2aw_DO{(X7C)4`ZWE1QyewH{ zNvbI0=%fA|niSax0!vL!!pPWz#V!|wbF3$F%W4O?Pruu-`|}%?-sPl91UI7fj(uOH zoQsbyn~|g`_##2ZnbWF00KVn1Pbgcfo7sB?%YHEgnx;eQIIw2>`^FZGs4z*<+b0qp zZ7#Y59 z%EKj~=W~t42sW4kyZvsY*X>^8{X&|J6nDYb0?r!KmAiHPI4~C!9R=*rlZ`70({{(R zGi<#|7>iawoB0K6&WKJH5cAtoDF&kPDB3+uU(L_2L2lt4szQ^Z z#sc2sF5#Aol#93@HGHZ~Z*>D13G+I5QXFIpmZeyEB$hl^z4Y|s-*@TKUR4tx z1=#D((z>Ivp5J`Olj$oXN-g!_D+ISP&nPHJ3Fmp0NQGj#=(cyM^LdPxXv`{|bHr8x z7Iv>_>?^w((yN-x&T15@jjFSsF@?S$d z^v{?2y44{0aR&?jgt|P`=l)T*u})n)iUH>0#CHQ(IU8o~cQGnbLK;mH_vfq#Ts6XD^szBazU*j7P)`$Q9 z>c>FzSm54o*8A!GpI4!u%ZEP2yGp$(MG)^d>_{LnE}=g2t$%!+e9V8mvHBSB9M;ved5a`CpCq468HDCk`9n_4Yv?;2f3TWfZ!SQ?5+mQgnP*^ZIm7PqNI z-z4blfgEf&`HpGgYrxRU-Pwq=_u|JqXWM{#D{k7J#Z|H3pmdtQ^)DPB3V^16?M$6d zbu1x~+_$q`6!^D&0TA)2FYpH&0Q^f(_W^(5Pi|~%O!{rVv;T9Nya?nh=FOYv7vLm_ z*v-x}tHGn8ah@ejPwfe2vYzR%nTnH^`<}v@%3J&j&_;u`$8v#=aS~%BjnAfaR9dX` zxkJPjlq!X8G_MCDzW@h*1mE7FH-P3jWnCXVeXl=)JdJ+eVf{aXY^`?tKuC3fEnOg|Qma9!z?u@71(13@bi-bnE(%^MSzYLQV?s489j-3aa zFpeIeqw{nXSP@8im#0&NQP_%^dA6NwxApIE_%q^(23#?Ax$y(!pb0XJwA21+Y#3xc zsri4h7od7e%>QIJ$h*_}vWWX5rb@%KXB>HMiNTT(F{t63G4hQ^1aGzrQYfmEoEj~(17k3b{e`T;g z`29yNZhX|xXGXaN3O{$h55WD3rc>ML1mT~joScG|SJu+R5U0T?T_h;f#~u;0vMI22tp%?hKHCh2%cwx|Ci&a5Du5X!{?(A)VMuoBL@`! zka6g43s&~}+xoAgxj!~I-ex_K!zwmLJ|3gi4TWqxNcWedN8rNNoxNm6ESK{4238hJ z6p9w?xpD70Y)-2+Sk{zW9LMa@d0ZGyIF=3*@Hqbg2U5VcTc9O4z0{vZ=HG*trpz$z8e4LW=#HMt zx$jOhv<)d*U3x7VcIn&761C{BF#J;Z(J&chTGo)m#sPV}I_6m3j@ck3dc}4Iw8||#(V;Q4P8IQ5Gz;^+t$N;4^h7ORjN`!3w>i-r!1LU z*b2FDn(D(pPF40fvKR;S_^=4xUA5eP*(!GHON9KDxssertRu|KKmQ8Nhc+x9aMUA- zctL{g;Pj!y)lZajt689%v%~-kz(*T|310n6@Cgc-qtw+B>RL(nMNP6PGtcjIv z`#}iKRWokjBc#2U_$kmb;v?2AV6EcBT^?j^w<~T%D06_CNoNIq3;ccBYRW|qGwZXp zbnr%*>;jmZ=;tHo(l=i|uGF}{A*u2l+cq=lDt;g9vy)Lfyr+Tzwgy?g(@Va z;4Y=$msCa#Qv;)?89i$})cZt!0Zk&(6oTrQ>Oh*JPckGlB6zD zoPfj2GH;M6E65LZ3lzM45EC=g>y zQ*i2_L3tQAUK_}=89;rEluH4xboLa&(pzfYQiGa-ll0f&Rn!;L?{YU4yeJPe?U(H& z9d3dI&qgRUCwh)-6Nc=WM*bf{&;|ooEJ+CiNA{2nR$Ibw4HaePG~e$R95rIMc&W@} z4hyOs2vT&W5JQ0{3Un)zWQ=zy+{G$>K`o9_1o!z+M?^GoVlT9{bfE3-Xh~0&mBnOO z;V*`n2f!!H)|=oq@tO&!dcZM&(VOa3&TgZde5{q1hzQtfHHlep4QT`yShZ~&xD#+o zqjt|_eMrps)w)dg>=w<5+WTbP1o|!~K)k31;Z^2bzGG2vUq>+zaiBCtIt*JIaCPKS z>@DTNoUolZ#%DHXebzk)fhusxiuXSJ=*xqf`Hu34>=fTbtL#>6T_HV7-9hiLBfo<& zn3=O0@aNMjcfCtVn_#A)sGv94Z=_KjbQ%A#`f6I#cs(q@52lJSj~v0Uk{kBbPPq8xKMIp@bX_X!$mL% zh&!)2a>MV|w@pWbw?N4HB1$&vg-US!t=?34vTZ&)+K}JzP~$6q<2)xpH;z-5`aMA@ zY<#--+r1dBkGS(m&iIHy$&#+eoU5tgJf{TuXjYk5g_5vYZG8jo|6avP%<7+#Q^eca zw>C_-n`HJ|N!yaLD~i~kOcU=H>_*c>*ldO+U8Vcm=<+RuBR3Rpde2v_1x8ft2rCUD z1wW9LGe~0Q6hCusa(}l-pR!=gNbtA{(xdH5Mg4?*Q-@n5B`SE=AmMknHkhr?A7mD? z(`hz!h~+pHR8dx`$f;H5-9a6)Q_9e4=F*|-SOGWs*`P!vQSnjRq-;lUKa}9>jV*mS zO?84JN9!0xXs}~P{_%HFxqr*oqRTxHa2ycHHYp?iQDYDERFqi6O9ok%rR#&dOD2tB zV59lI+>fx3wj5xYoQHuFte9(o5iH39OO*I8zlQWsUO68)YLf6N|Klqs3V`@A2s#G+ z^=tB){NKbFJ5QscdEc&M@W^HVBq~0{f#u;98$q$Qg6kS>7Rt*Gae0O3UUPR-xw>VjEd)iwZ&S0Y`7-dr$Tc;f+rQ^W$=i0mC@u95 zWt0Ri=z#$sM|1Vc5H1;G9&TbGk$BF>|0ry5#>yg*PVgwf@m}Dc?~vCF?sGejII=Vo zji_o4l3Zm0-*)9WhI85d|NB46V=ed z^GnKmn-AuAuAS)Pq0Mm_Yl&DBSxMy&^GlzE@pmC$kLKsj=vQjN<1 zW2+9}2|2tTI*y=Nk=Y1`!Sav>v4U`8FmnYcl{Vv=(E8F>8kJfqp?C97^Z>b#P~`kO zEc!rz637$s369y-9&(ji)OTdQ4_MzLxS|fx8+;Aq8`fOb3}#BHhqcdGppt*j6q9s- zrjbEDlEeh%PpC%_AI?Wi6vKHw=*D+lM3PoTAlM>O-$XI091O_By)-D5?4nKm9`X|~ z@JDbS2rWoI4=m+uwW2SO{5pLLd%g^Q&tI+CVwnf#C;R}r-3{zAK0ii&W^d06cES3b zcBGlx=5-o&^{prOHf`&ciycZb9Yrb~abJuu{gg;5LLe%UntZ8oQYrn9u_a=25QY*I z?xhXQ@&L7ivLQ2C?+Nt!Pq@Tg57Py(fuoNuym82Gf_HcUNr_1!v7*SjW-g8864v;ka&YIsh^_3KJe4!&ZX6cFEe)j!pIyih44z>qn}7eg=h3FQ`+Xxq zN`E0?iRcuF4)NGe`kpUe3D+SG4sx~EL#3&L(37q9;mi}Rp_X$7tyGOFZST~P+3lNH&;PRx&GF-cyN^QtkPi~ zoP8NbB0Im4EMl*ar36W*Hvl_u?S{v4EyUFM$W!h2)S*le_?#@Sw-TMI)bSSrJBMQ> zg08*t!9kT4u8Q{ILCepzS{56tZYL=fzGX3VsoU;fkN%{G%4i&@7t5dBnb}h21`#4RafU*=ePQ*3hk~W3 zXPU~recm7M$yT_pZ_D!oZ4GIq)Q~lp4u@N-)5EIK@;QdwE}1EVVts@J*aoOe);N(ya?F z$6||sSaV2UFqG>FOEbc+=D`3ngE%|-2!|U9HN5HyD5)Io4b%SEuR%4prJ#5tYho$I z6B)az>Go2zFfdC=l$cj_G@40pP!&0hYj;ouvETv>`fxtULc3YE@}EirhS}0`+$oP4 z4c5Lg@qOrr08q&mQ;tEgwfxw^IAoY+u|2>w^9%nq-ACIn;;(PBz>Arv4z*InSp3_| zH?XWlzmoa#nM40km2(%YW&x(AHxf@qw>~7hTd1Huk1#Q!Bg~|;ek&?0wq^EhPDLyp zXktRdq7n^ENVt*LY+hk|rX0B&S0vJruO&=0uAIn#MM>~MI+Qs~57|N)$h=%>8>q$ZQl~5zcCYTs;G2ndv7Q>4U@Z ztISx_SluXd9_IiIUXi262taNz$9LVml)PW`ZOLy~&z$CK+6#;!!O1uE(=VJRYorNS zX5$I}C2y};cX)?um=~Ln-RdbN7avkGh=YOPO(?6V!ap!H-&$!@=Pys3CMz}LU=1Bw zzEfQaTg#8bFnm|mB7fsgf=uGIlBZ3AwA#$I*63TInt?>vI0pM0xzYKWRs6^LK2*6g zJW3{e+c4s-+ti!$T^wv8+rqTk^0r%9z^)=2jMDxm6L#|y3l*bQEw|CwdVm$Fp`gC? zu;|6e6>%wFhlm`B|Lr6ohBlRHsr!aeE^|Gs_eJ!tip)srnB>tIE$vbrqvOCpCF!6NU6_x%ubl;!d{Jzut{$GzdDtsR6mMK}C zr{beSimq1G0{*DhR?K>WT@JUB)1|yMwC|pK=Y`poW<&s(wk(BoM}pg45T(0~5JlUWa$N}zJeKX^hv z1H(4c)U%(XJ_b&<#Apx%7zmg@Cp>oGj1UI4Y_8)~}(;a{gnSK727I$EYWPp`o=T=dS- ztg^6DX9k(2!KJxObk!kMIXu)EU$UM0Q%yFj+E)M{e!X@uIC}f~VdR})$`UotuyxXy zTnk^g`M*T|x#>q4L`rN$q2V$B>gp4_vsO(Q*OJ|PpdMe;OUL2_0|M?>R_Vje!&th+m+r?LG_ zMC~7EdcKbpNHQ`bAG1ELtcn1_^u3_{!R`M=lnrl z)mbA@Zj}XBRwx1#1=>R|ytiMHn3gDy3gTKp-S`093^wRGiIV?)WYMrf;9;Bj)bf6z zg>a@3xVzDGObA*pD)!;pp@~lsLeayDopB{ZI9HUQDO%%qPD6 zdDc0dX54jS95t`rIp+rw{Fviwcl)`WZ|B~OrSrDgGwbp$cs>pvZan6~@3}JV>U6qU z9lYpz$&%JSxX7yz7-LrS@sb0aAgN{<2{rnTMRfw{qp(LQyE?GTJnbE<3X-3q4x`p7LVBgaQ*P-h3bWN!DYiFHK35;C^!CR^G3m7Dy zN=Aq2s7FWrfFJLFIrD!Wk@)M2c^T;XY$t9G!v6%kf5zv8cm_Tnk%NB&iDiK9W>J&B zfZJ(#oPoqW&nBsUOU3_@#9%{XkU#S*}_J zJ)RZMx3&CRf1%fk^=8Fdz{a7rKI68p?SKAiu6iFD4@+Of>whZkNn0yE$&$_ar$SmU z(C7SL&+Ldhk3WC>fb}bL5L@@x#--MlBF;7YrCxXQ-BNgNwmX)czP{h<=WOn6n&3xF z^V|rso~DOK@#9G7%}E#htKi*%i~LXh-TwCp*Ux{l0xU+)>`5IDvO>}7&kgcUlmCs+ zr`y@XstPw$4+uXN@%DdWE8z2HWp>E@Jc~$=^Xm|kV&YSOp+{sxE0?KY!U-gJL|8%n z&V@{PEaK|>m4|wcVYep?8$Etc|Kng~wkG;c->Y_qHkmWNnwppAA%l^)r%3d48hgjR z)}5#uR;%`5cS)De05$7w#QWCW&Zl5+MU`MkSf{V~?Yd`nr|timx~q%9-ctbP!QYm5 z!$vN+==nZQsdC0WnU)e^3wv=x{huARn3vdH+#Lkf94jWm z!>yaGwoYt267QV$b`QkxYGZk*U=kSUQEyT0qe=ChS3T`^-$LhaeiOM}=)fyhmnDWq zy_P8AmMHEMY{fFNFgxuMc8z}f^X-+Ju6(yXZ+t47gMrZ5%-5C$@dNu+#HbuLSsl*Z zhvLyfd_}dvjESLVG&9rmpt$7NOo|hfaLLfkY2siG=`+jL^G;}$W%tchz(+mkXsF&a z_eYt!VCv2vDxbQ?O=|eITUUXvipK|WSO0eB>zbR5R|BO+OIXqmzYQ1UvIk3y*mlm& zjBF$y57^7xaD!f?qr-sflmGn0w<2&%i1_w$6q(C`;-DE52VpdZK1V1wtj$3oC|Blu z-R@Vj*CTZ)jfKvz6F4-h+Z?XwbStVhPGK=d{x6ieprwD#|8h&|)v@jJBecH%I|$Ry zyWQ&yJUlW}okgG^QD68jqV(hJX`2v8#Wp6z?}u-MYC`xtc7nphXpJ{le@w);r~w6%8BXj7k?KT^LffKcqDLryb6v7!}hCih8FSg zHW2|dW07hP-vF}kZF6pWpA=`t?*-1EZm0k2d`8Ge6Zaa1WQSTaP~Uf%5bFvkd3)4d zvO8$0ww{xUC0nTx=(hWN}mFZk1`#Kh2Qi49_W?qjZ|m|;sPHuvT3>@Y{2iDR3tFXWa`X`#4M(ucUx zeKa4c3r%!ZCSgEXMGH^*N1CeHy_8$?tOjqrOwPuIEH>A&SiK(La~SbHeA$Hw$TOy_ zTi={hK^yRA0sB5{2oW%Ez26cCG3GnM{43bRAxo^X%??qwLrQ0qXUdz)v-cI)^ZOv6 zO27$Nh!AR5WVLN1$Ke;_&^6g_^k@M;VB;6!e#{ z$Bf=I2KAIa6=~V+s)t9YHoNHKaC`wLKcA+S{WCS%40k#WzSA3oztgE7(JL2}2W*_v*yy(_;=FiYr9Sh#pJ{6S z;Cr_CJ4D|0r9}EI+=hDm{<2WXz`~k{q;^FN4A`8<3+#!Hjhw2u(uDG)6?wctwt; zzqxY2TFDhmHe|p%#cIxy&BKX4mjA6|F6Ckc{L;3G?p&)>gikg->O#%S)X4}v+L9LK z8Xj|;xia-W)VedJpHu>KXbyi-I0zi<)zWo9sj=D9t_3u3kgp=Rjn@~BPNP6ph0fp- z1*|^(KsQu!G&PZ24CoxjR&o64-Lbh}D-07@omWQZryVHk{=2zb-H&W(z-_Rp24KMZ zKq<~jq^=V}oxq#Y`h~j1->v=f=O%l*(Gz;B)qD=#TccuU|J~p|2LpQG@}9soIE_Ie zqOfLC>gyN7??f}z-5aeQ-BD|s8EzEa5x3|Im?=7=5v@|?s`oExwvK{{HF{zj2)&K$k ze1ZeNw!Q@}U&k0@Yv7}&C#|2CnB-%VUhzgzu_gw?Tlv9{`<87Hzyk}t3#%z`h!s3N zLPPr~xtmHNX3mDdK91ea4I>iGu#Z3x?3zOsOOmT|_Icho2W3OS0PEqOnATUgDj(&b zvsB=~oA}oRV_vG7P@7UWs^A0x*sPK4HO>?uighb7i1kplQPK;WjR>X9kW~}gAUGg`DeuVq5DPSp?tik3b##Y&gTa0&ZtzED_5(Gzxz6bEwnhrHGJ#_fPd{ z_)QhXF=R-)WTQ#g2=%AdnFSROmFV1{YD`Q3=~tS^mwK-5@Wp|?MV8die-YYo1>O(S zX}ecR;|0dM;FaQnTmvWXA{$QEBu~!TcV^moeu-4`z_g^@q3RlrxhW}$AD~X#Z0hZY zw_jRJ5skXGoUS@%%+w1~qxw|ZMLK6Z;zr!?*N|@W-;@A{x5rlh%e3*1N8>M8Lmu#> zK}C9>&AYbOYRb`GX0WqLPY05(d31@x)ZyR5v@}FI3)I^ZoX`8$-QzGlqt!&~VL@n0 zbvn@(=UPz<19t>E0pit`AWm%pW3u?msYDhCGL`Sy+8U<;YdVLaF&=_sKjDOkh8{5= zZj+{h4#gA7eZ;v^O9O3GgL>8JisB-Ce-|ReB8pQqvthDX3qi6JS&-7LO;F2o*A}No z=lO)BEbR8*5?-yq?yM$YF$H$)$qp?$`=B zPFHMA8lXV$bo=+ZrWT10!a-@2;G|;B)>(l1n`Nvr)(l|%e#}1QvBjbnF!`e%I6KwV zc3?h&GMK>8Y7@y+Q3)oPkr)?-d4?yuE8d_tF#1brVlx;5i(k0lDc>N3j8L#MY`}#E zP*j)bO{t@H2?BQ&A(B~{DEnpu?^7AI|zO?>8PNxz+ZazbgHe;Ca zIMz63ON<%|xz=)+t=;jL)HZ7w`?{nUpAqpeMnPG1qq^rGYc{_$zf1cx%&W8YG)>TY zfWr-AiRpRSJ_1r9sBDjh*$&fCXk5sDCZm}b%?2z+rfONNXChQ#TG!}yivq(%C|i0dp&}0+BFPK->~c9L zwH8LQLM2J^9}M&ss?+}jfl5l`R5^|nN*D9=twB;~H*C*cixcwB4Uos>3%OV%PdD}u zoP1#Q6`EU7lJ{`u@Aq(FHZ6+3YNs^diw*4y*8kwu-#kmm%f?4H6d*z^kO{mlxD(MC z#3=H2O-M?vXv0#i@iFTzqUdQikBv76t*%+D)fcEZSx0lG=L5knE4>Q(&BeK`gG{iF zI45#W&MQP+_s9&)w|rs&)rTXTd;h+i5Q@wfs*!QCq%A`$452a|*2A2xf=0#7gJSi2 z>ggC;3+|Q{FBn(De2$pCdZ@^7BM(b$ciuQlk15X>#^SQbe`5h2%U28tp+s^e%9pnD z?0#>jl-_}MbE==DG9!1|>B6EI5Ht5D!x7%g8HS)$o|+~-e!Hp;oJ-+Vm>mj+hM$)+ zba_5m57Rkd0`x2lC;dA83~RI_h2X_W=y-nK5#ezRLW;MYjKMP6S&R%Uk)|rQV(ZNx zE3cc<^oqB+b;2A{JXPCQLz>OOGH(i-&L772;->l~487xZRwv5S>i-sLVk%Q1(00Lt9hSL#$lscfYw+C&G=vR9 z@~)E&g0>AoL;wv)WSc(AY0p0>f0kQBA>!hck>y74@KdiJ4ZoKw`y+M)&>Kb$tuK-2 z-#6z|SRu&SR0YlGW2tD6BIojtn|j%Q@y8~@25hxk#T9+Vs=_^t5@gQqj+NZ%*6Fe(92kLyi<VTQDa1tP`r# zrBhc@LxcAi#tEEt8iwMj5}*^8JPhr%7!=qkwg&gst&-&W>%N_dlZNzs9G$G^dlXuU+dXEK&Y`4>CCfIDPyBd{#?XV&pj6kB{BVg z4MPcuuKs%Qfpvkk%JFi;F6kJ(Grj0^fJXk}-4K~Ni--8Q@q!-Swd$%v*37b4eR9M? zY8;D?s!Nn}Gva-aF_~Ui^qq~#@SltU=kL)iTD!I^&X3B;({I=qE|`~APm)FxGC3%R zF&gMWfY<U(xN*u&(P z4q`c{@Ck?X?dawUqnhE5g$&aO3mM%B9&G69&r<_O zH9(4=a*0Go*~pMJyY694d8$Rs<^2(aVwhIEGU-A)#ra@(A67q1@rN0f#K%~NB$>vb zI&BpCIyMl&#q6UR8E8nDg}K-e3_X(L6!?n5Pvr@{8ROzF12wyP#I-ri+g+F{iaEr? zcc}!I+-9-i&7gQajMZ#>_juVm0uLhuivBdHIc z_aH4Dic|1VONGye2+Lna1$#Or9!JT)A*)=361C$rc{?>Pv_%4Yu_LJQSCHIkv&b{ht}YOP5$k}H6sTOve4yqZJ~l6s9| zmRh^TnM6<$xQKV?L(7^%Djkf))rgZuU*~AQ2qs^^yB{Xs5o)|(7G?4cB(kAcR~{gH zfZzSndfz`XWwIesy@0nl(QPYPO;0xFFIkNIIpnI3#W%x2I7axt+2~m|!gxT2@pO&E zbQ7nQLPAB@^uzh(BELl=7+<=qF?>%>9%Yx~6IgfV%QeTOnhdAqk|%`2{s7Los6k9= z;gE&H4ZtQ|qLJ@JAe5u-d!#hm;=P_DmD6Y*gtE zs)DKfE<9?KHlvwZ?I4~f;UlLhT|$2kiN@(T5NWFvY5xh-`zH};=#K6s)_6iyRT%(3 z(w-!Azq`w$_?4{lfhV+}1D*p42du4A3{%v;eqs_;wIIwxBFKhQZz+tx{;r~z9KIz< zUA!{~V$EqZoOugMQOyD(I;W3sN?7jV(~#U!&_&gRL|lCN@Dx;by=e8be!w=#wv4I3G-f6byBEit$)pZg7NC~t@ggffF0wfkO--it{;80uAz(G7bmo7rLTt)pqh7V`ChQ>zJg-7t z^k9=5e7h|Dv5i;8)Yp3J5b_4-O?fGMEKxmiV1dv0T!xR{H6ED?y$mo>AXOm6$Ce8L zoe>X)IRUiR7IbHtwQ{?qN!uX8K4_b}GT1(jG`9ZMS&rvd8&oF6QRQ15Bh1jHlzD1wjB|nnsz-hl)*sdR6+EExBGY2 zO6{@>{cy)M-;ek`|0Bg5o#{FUDxfS@Yp~`If&3UoVz^a|qwKY3hi@ovhZk!a=T;TN~z|OW+~Emqn)C7 z;NNUSDv}z_)4+$=Ns%|GoSK9nt0=E-vYBoreYk0}VZGr}e#jZqrRGD;a^<3lr=zJE zey3CW-vrS%;OjI1`;hSNZTLfpy7t|8Zc0}rU@#8dWeeXc^7SUaeL9l}2bl|F$389? z7#~Om$S^s{|Jk`S8BD5aU?PR2j{w1t#7V+tprI6;FwEB+OoGDix`AElQ8Yl+?9jxe zWxIH+L#CCKMGz?kG~LHt-(Vtp+&Ckn2TX^_f81zN@V9U6H4@%JTn4B+rl# zj5rq#ML3Ho`O+|^TVkm>Q6l3&1H^9&n4p=?#%_utOhoI5!;J4Id_GGb=EZozX@V?Y z(*jB}8#>|`c~;!xRk)$?z}wnmRQS8qGak<9;I_v5s*JXE&f8&EKj)S- znxIB2w63lFhy-$6Xtw6EMb|5*eCo5c8f5L@v3A(fLiDv(?>Tf>iTs(u_IdnIM)sc< z|7MH?c6pr-j2w1(t;e*vI`o-W%Yx^R;H4Ix(AqGMIaNkB+oyV)p^{fQFcf>ukGsM( ztYfbP61=I1>@Gc8AtkJfSo6tjM>9YMHK-a&t@*O61xq!viIv5gt$-3{JlYB=A-4vK zQG1bZNfK=>3`|C{@C}eyD-=t#BBJ_82cY!MGo%Tah~wv08*CD-xJZ!WWyXa^yE>Wf z!OVJao{U2Y--eX#FiXewzznmZhyc<@pkm)0JW^m`sN{)2@HcGa2xi5_@SKD#^eb5L zWJTzPaE`Ba5*^JRFP(5KfuQghH1{inBCcMVmp6mJbD7D)1Ry zsj{$i+zl&nt17P5ezPBI6$UMA62Hzwq@j(^*u$G?<1zX2U(bK-Xh9>bV}#Wb#BIfUX~)OGgO6 zjmkZgORg-2%(#n5_b`qn^#gY^9cTb;Jr65CJu|Wb)7OE`0BM5$rr1!V_{p@#V40Er ze(IH#&1By@f;@BKPU0i2RI13e#+o(|MA<)sckIp` zGs>QBuZV+A|9(*0P;3Ih>vTB)=Qq3Z82c!aQn56B`@^jdx4?jCTj_@R7|E`8y>h~@ zg!1j`uugoVXjnY?qqp_`m^l!i&F_tI--90T!B0sJIStR@u@e6M5rvvey3{isH-L;i zIwgFS#l%Ys3!lTdo*I)UT@gz6%+QdJ0uM{znGXX8jw?Q)dN`+4R{PAV~So5kWt!ZEU4&z_{y ztl5wwSBGWaeIv*l*yn=OR0WdNjj3i|Yf485{lz+dy8l|8Q}C7I%Qc07dBs%279{6B z^*Mj1^%VOn9UY$t)_)tH7#-Aq zdxG`fXs+$`*fIKNVLD=vIN;a!2HMbk46IqlaFN=5zyeKR})eeln&$p{dEXt97S%Fg3L4`Js$z zmV^JT8JkK=WlaMx9tmJF2w>a_U}GeJ{|Exu=mStgPUzRhV3zo`b?ek=<122yBLxf&~q>#j$gXxG-#7xZ!sRxVluMD#)uaGVYbjcSWg+XuU5a5_;RB1 zemX!k8GB?JkS24)Jty;iH{~)amsMgZp5axv;9_e1$(Yr^k%k}k8+7~g*W9V4j1G0g zHxj5eXT+xmUfM2Ngg&AX$P9O;Z}2I$m#&?G9hOoTa{`h0Cb-bO1uLwmsD2x2`uzu@ zE+%Xd)YXXEs0Og)kHX#=f& zf@G9yJA84?rz`9Otpb%wkZkbTO&3LFyD51Gg?g1Td zfrn!jSG~a^)LB$FGF>GqKL9Mc|7Yzz=?oV``mx-&N&0Z#p=mM^(QnzVh7AH2cDNeahy2t*=hzD8K0Y;hK@)yDLiV(MD_YIL&B)3$F`Q#G z?Sp3m7ucnKFOSyyVIsfFbVM3p8xv`WCi+|zD_#&{Oe-F+H`ZBStNIH=^8lu=S5@HC}|0p<4_^4IW zL!*o+5Se{bSu(<-nz9^fn%`!*b4*l~=abB@xuD55$4~O~3RNK^C>0Vh7(f70`A_1N zzG&gqM*mG{w}#i_=|?t8IAVVaiOU|N54q$xoU~a$INnU@6R`4as3n)y2pm4$?E~y& zB-lhKlLu!$Sm2iQ6dGS*+MLiLwE#mVjR5BJuK`RhK`nL;4Rp%=PzZRgQmUP{pu_1@ zgDFKMHhG&CV0hAPt#vZ!1tsL8NLohr6$G&+EHtCB(dgI%8*?&Nf=y7?BziYXL}tm1 z%8j{7!_~_F+G&6mU>mSL)3KG5`qeM(cia0}=n)p93`~y0Zv|c0jJqB~UxA^8SHc4Z zQp8dXSTO1S$fiwXpsOfbtDjO1VXQ}@2_h6JEX!4I)G&@mWJuGU?IMy}e?)1SLVc$y z&JjyioB(+;Y$UDx^7a}n5ng-|4xF{+znNO?h7_hjI-QQpsifxKo?J5Rc1)>f~%sT3p!8}0=(;pXivV@Jq{V=F0elM#e?}ddwN)vmH*en0^SV=kx z{aTyhDUp_d4y}*cOlq>iiLioB4oxoz%&c`qhXM9f8v-x-KwsZ`LmlR$Cf$zSNZ7J{ ziv6zbG{B}f;NYb^Xn26uh_ohvV@qwUZaFhAYylc$g&TX;O6HcnpN&G)zbmHhL1?)O zoghA7h*-d5!8K7tiXGMVU#$WHm?|Str4RZh*2=ajm8HyNC0Rx&B#{G~#0rYW_1#M% zYQBQ4AAP=*ur($w(9dI-xbV0~RxruEXuc|?A+#%mJW?xHMrdQP_=NfpA-0`TxoIqP z>5+9WxSblHqISf<<{fq~$IXP9nbS6$M2u4_&_rTrt958@IJrb`532!OK1_szBt*n_ zV44rOEglAwN(b-V=(kNpBdnjH=k7u0NGq$~tR;7e?hdo1;@58$OVnh@f~sD_9#hvmu)U2B7w4P?(6HpPvUkvBLvx3UqeEspQ8vb}fAbW3 zao_ggjd%zFLa1jjiWyPj6EPf8f>`B;jw)=A4e`op@)7=iY{(BG(W?nz3<(xtTX77D zZ92jsB*|oQH&1DX+NE3ZTC(O_Dpb};oY%A)S^<+^ECNNp&K(i=0Xj;HGNO@I827Eh z0^TQ+CWBZ$#|be$5AlT_f5q1}^`Huiz^7Beta--wU?$f}ROPj2o5Gya)Q3Ckxn)cS zTC0`ed!tm*DnegY6Y>%nsS>(l$G^;ls;&EykH;sB#H1MwdtgUw7Sd=~a0i4(%z2|F z^wiLQY$uEH-1tlPCxl)yoTd|*S7ad<8xCF_OU(>nvd1AFQJl7FgsNSh4Ugt@INB*! z3`#3Zj{W^O1VIlmpSVYQm|uf^I&n z6_HI!&j9HJv`43$L@P|VSiRdVTcp+4iUreTSWt||3X1WB#m!y;+nz|I4FD`oXk$mn zy3IiIaH&!;n|>dg@@yi4TVzI84e}$&MiXdg{j3?|jVGoOkvwR_=-E6^yRm&cSA+yF z%j?BvcX8#A_qO0u^sLkxCAY!1_pxDAaG2H??GLjxOo2%*7Ci19-?Ake2|0G3u?;95 zQq)XGRy~Z}l+@OZ;zs6!HEGrh=(yd9*`xThNV>=K6=9_v5Dlz`IxOXO4GcK}8AG9QXh%Ps_d7R7W=;rggjvKum8X7v=1PL*NOjCqbtwl|(e>Xt|a&u^kOHK$qhJjnG%uMIdB@ zDtlOWW^u+QL2F?K+PShgQilC@HE9Vf2g^?|ErH54UY1D-)B&|ED}jHj*>W`eM9xWh zoixmm&&y26&~%wKKXlHirN~#UGNBXvbc7ixs3QY&tg4>@%kRVwT@O4WS_B^Q5RDLc z+x4725*qY%-fIn<)lW5^)e_spSZ;mXJPjpHh`6C|MPmF>keJkw2%Pr_rCZmz?d4qj z_&k*}S8CX>z$|H*iq*c=uy{%Hw1(Tp4*9tCg%6481ts`!5i724FFeRq? z({W8An&?_X)D5IUSEg9M;3j*4GcbP&qb;d6>4An*Ey0ZP%IvayJ7uzI){?HDD z#VuB~1Bva&1o)(Opy;rqf9SXyOIF&|iQWGFmCMOCO}6_W3<8~ME_ zVU?6O|f|hDMHg;zIrZ zorFz)62Ko;I=z~vq9#|me=Bvjs3o@9uZgyb;B?JOwE&D19cCdjAt>A?ri_HSr2nO zKlYn)=;D*3EExTxDUc0uYe4Lz^@`K)P8vVIe-FP6))%MpoN;8vSvz{{Nvqac7Ds(r zQU&LJSkFP*Y=UHl3I}nB8-QW&`o$HnTr_nwBKtM%2*hq5R!kQ$`r^R zOvE^v<|juzvT|ujx%?s8Z%nTybD%a-YYL_wSp&5|gvgA6Q5Sg$-O>JVw24FiR_5u5?7R zf`V&5LRWilL2S$;M>dX(j8JVPHN3pcC4z4wX%VjODv3)-aE;M#?BY(vDQ7;7Pd{9U1yAurz0VzV_oR` z#WKK+h@-oDQzRVd02?@&B9GGt`^$wJ^iuWxB-uo}bac?dN;jF+ZZO92i;`$&0b6-k zLPd1balR4lK6XDDe&PuBoJ@2QS_1~ztUIJC7#(hGui5-4GPdG04|1v2OZ+5$3CU zCScJ1qu0tz5MKwf4b}EmGbRh*AkU*N$4TD zhK}k(J2sSMdk=bbtoUV1JA~V{S8WHgU5C6gQfL~p8F}k2AZ=EE3wV|H*?RQ9vMaff z7eb<4B4fQKRG(BTB-?Wy`I^A0qNXGjReOETj-T(IOJTnbp1jN~GD#kBj_s}G*0#)` z*+;0-YOd&53|LFNzD{@^V$ZEt3~KB-0X!JLztt4I{LMW!khU@4E)0vMslmvE=S`7&kf)ZTlyIPqm{Cc8 zw}_@eoz;KTpF#cSLH*}J{pUgb=ji&++%jvdw)S)H`p+X1>&8a|^`A#3M#l&BpZ`wv zpKX<#pYy61J^Vsc3Fy8mM`!&~;_@Rp#0r${%mkv9Gy3J3OxXFENd!;4bcbP_3oa88 zGwDOreLUg2C5dz*@!SjlZ(-jbc5f@}+`Jo2e%_Q4ljcGrqnA90HWwVFXo&X?a>j_o zi+ZCjURXvygg#pHwMhl}RmBN>6`+Xt4kG|_vD6{cCKslXc#GExav_Rk635UV_Z$zo zN86x9wXZ?O&5)giY$pUx(>ys&HE=E+^#_7q?u zw(Z3k7{byRVD}AGYPCc7Z>!-P#D6&nGlEv|ZwYA)|IT{i7x;j`P=DhPkNNn{IW36~ z*aS|y+(JE@S056bJ{`DbkWFlVamNk|ZgZt_1OMf`pm&LklMyqO5i=oC$q2e0qw?D1 zG~?G7QZj;FuG9>xI7bR*rEMPj1xQY*WTyOIp3$@^u0imX>hK&<(nZy-^L#xB(5tj7 zRC}AZ+*LcwoN}|q=A3G&%r8-hxseud*64}4P$nvhUS5|AOhvy4lE@y=a?535bnyQi zp7T)>FWIma#Iyd;LxoI5g6a7F3?oOGbmBWSZBrZ5!T2QlOoR!Ob!^mKitt!bv;D#7 zBhC3Jz8KF<^~`7#g}n5WZZsn2RZ(;^FEk!|;=4kxfop>gHZsUy7~*zHV5##EjG&Xo z9LwlL;Lfiq0B`G1G6Zl;Dv}kf+!QD##Y(%~ z9flSw?B~$d=S;^U#J9@b55v+w3^6gXB%OWanZD5*i3`EyZ-u3+Q7RoeNJN zJF(k%L>d(z+ORbRr%rKxF_CLRQ885bY5e7@$dQcYVne6GBN|J`HZVsvwu;=0{Ste{ zM2{kVthW?Nt6e13wG6%Ej0QCwxgxVMhlgoHy7+goXLK3Tr$8hu5}I3*gB-Xr{ulWF z){Te#e`5px-xKlwZPfjL&rFPLn8C!2dTm{~5^t!THbN=N~}+%fP{vp9J+d{}~zEFcvuf z8CyTLesKQtBz}^~6evDn9KvT|Og^F{~Z4{t)mtsTYnG|M`!@Dm0 zVIB1l^U>4vPZ0p)NE+^cx80^{Chl~Ja*_0D87uG-q(ffGtp&i{; z)bc^Z)T!kk$qcwj-Z05C;3hExu7F*wH7e+QcU4fYR&3WRW!rUrMJV_!ClQ_l2%<-Q zCrFO>jTL&S@O)sNbO|bLz!L6zGi|MZ$*DH;N4MDG_nP*r4vr>U&Vgi(S0fu41vL?m zkKpfT;8z~t4!`4LP9Xe!;#daH$ka^zp3abbfW}FN27Poi6L>{EjRhXdUh!BjOz||Y z9bmS=LqBL>PP?V4-;Bl}F2qkNX0lRcbi5C$j1fd0@3V!bY#*M(rF^DM_d%i!b`p(F zM9?UL684pe*ZMg3r#1rWGv0wDUaC7#bxV0(>MBGZ#GRx5djv9RB$qNMw7oZlMhGMa z*M!Bs-F1*i+wn-XH(|y`9*K9v;#m#v$fiNs=rg^CzM0(G$3UKYq=E1`UCl74VQ>(9 zHUjJ~(pRbA733JF(QxKdGh|pY?}fc66vh8WckUE-Zp1YPUZuQbWFkmzdOqoABA~1d z)D14e3K+vKKW`Ux+7HjU4VTKo&6M3?D(=opps^YEfL9eVA0Nn*Ouj|tjXkzK8;KkZ-66d;Xg7@srk|t696BF zrh_KHAayW`gHt(zW1gR!1f~JXWi)zah$b1M^K=j|tw#x5A4H&z#ZJ&KP{&}0YmE|v z1#_T^;fH*V_f2l!u5JirZJ}ElVD|smLL;o`2)!aMW2($Yp-nys)2TH34G5F%;^*;_ zR4&X|KnP_dokrqS;R80geXCfzwzd`ykP!x)zr^Qhl_49RbL&*+&Uecn zea2mFZ0Xj^-M7DAxP5o&+@0>nmpdo#cW&J2etN%i|KiX0&v)+p(7F5-+xf!p*~0DX zom203PF@Ww0)IRHlBrh8!aQ8Z7SA^C-o0a6;kiAVcWo`~-u#ChFKph5YbJ@tpC%yr z|0>?>=uYqX=iq>r=S6zn?ikaA0 z%TKhkR`O=&`GrpP9md4YxI*zS!Y%;j5^xDi(J+x({J-0>*=!pAr!5u>@>xB04=)E= zW+%427EBh^9`}@uhD7xNq15wdM)Fb0f0VW+Zra-}q6vu6X!8u^XowYYgTZb%$+NK% z!!~7pLa6?rYlcy?Rz4(E2fCK3e#L|yYGesNi;pxt3;qTgYn%q-JiSsTkVhbZg!wvb z9OYwL*!Tyh%{}7H>X?Oxkq+|nw#tlKDgj-CW-CUgb1uZqLo-d+c1S^u z&8-$zq7}DhsW&UD!P_iKUM@INIBOov7sCl?;RwuCPFXjkmn7S0SHmgW_FJR&k`iJv zW6wm89R82CS3I~6)=4geX#S{AsbdiklEbw_Zi6y2)fyfsrA}05FlQ-A=ydp#sqmxX zoLfAY$Ku$QcY@{V3@PHH7VNy1SLvP-h~^IXI%f6^8AR9nxAA}QfD7;TA1UTgGpIM5 z0~IHSn?n(a#fDkQa}HF2>%Ag(clDW6yEj7xxkkWZEVXaW6Ur`{LbxD?ByS8!C75R% zDua~Frmb*h0gx*Ecch4={|4tjgY%!k`On~ICFehA6R++TaF6(p(ea55f%Biy4I>){ z=Rf}_=Rb5MaHgo@JD%GTedc4iLd11?NL|25lekB^C zV!s{!R@Sv?SOYFWbNa>y3wO?fQEiD_S$go7?uE}fXKpQic%%F2>BYA`=^j7PJ$rol z^6|wFURi$Q&z^E5_TrMmn@iSZ&X#QDqWg6GA)G3Y~S?&ao_t zb5g6&+qwKz7lwE53_1%=-|d{e@bmri%Ws_N+`OYx0sbVbxO3uz&YLF|&z@h~Ir(z; z?OWXoH^?O+2@~wV$XEE}i~}778cyfteHb82BzKQ=-#NW-?}zT$YfD$oFMsw?=Zkkb zU%aw->D15n&vd?gDav#5^Zm2^jB^nG^Emat!TpcFpZ+(v|1r@2;`G0)d*HyZSDme` zeWdlj()%CdV}tu2gZR&Z{C~3cKgs`r{XdZZ7Wu#X-+wnexoi6itF!;dqwN2&v2_!= z{2xdAfBm|F{r^OMc8UX^VG#$HV`JIz#4c{~5+4kUxKH%~XOhI;b_J7H<=Bqe;o-7- z$St#W%PV`Bm5QGp$rQMl<|BBW1pc#~pQ1D~?6B9GV|$%Svt2zf+^P-JkyM4uc8Wa& zYfz?om~+c@9yPhS-I}X4hTV$em2+$tpB?rGk3Cze9G-pVnepF>f3w9}Wm95@S9Ggb zaeC+EzQi7Ja5KDn9$>(THs#l4M@JG{-Da`j@q>V2VHghY<7wLQKw>Qn_h6}Zxazmx z>NQ(n$$I5-?eM1j=tvg+FZj03-wp#MXE)`6bn9)OpidtF0Abf9b~kDm^wu2ScBIvC zatu#4f-U43!0_P)rc7ucWB~kc@B-RkK8z$bSxihDe^0Zen;$Q}^$8fwoiiUT zUHS3n`{%pw-CVeRcj4}-rE8yePJP!s^ZLT=uNUs#?Ogt9>EVgRYxfpUURykSy8G@A z%O_5wFXe+*mMq`s%{%v)%VT>7G5;y?nQO?n3wVR}lECUv-b4poU<8 z>rNT4xVvZXET6te&YA9+OBkUA)GofTmvt`QT)202@x$-CuU%cbd8%{fo4=u};y&2! zXy^MUlwhO4G2z9BXpaurMiD${`wv}ppb&Qp~O1$(&hXU0eMx8vuD z5`Q~>p?l^X94=eB@l)rE(-;bJHNa&oTj^P zz%QMX-z}YZ3uob@_tBB_;ZJ}A09gL)b#!GUHvRwYUF%O9R~G+1f5nya1!yq|kVJ~q zU1b}zMQYk8O}nbHh%w+0D?BW0x|?lQ;t}4)c6cX1z`P5^z(Rt-82>Mo?~MJa|H7Vg z&z`E#pz&m9j$G?z+>ZFm_M_7ZC*@x z3V6Nj;r+VXckb4?y&dlMmWDFxuW~o5DAN{xWOS3k!ekP=NI>A0L=uxzQj_%^P z>;Ud(_l~r=Fz^YiV)pnD))%-XySGj>hq0AcC<@FGO5m($`QP;FTVt7%rA)d%lS%{g z>1Q*1@FlP#l45-p+Tv^D`qUOK3hI$6v8sARWzwTWGA>imMRi!l

WApWU9RIRuv|gGJSWXP(#4N@!eT1{>B)TXihsm 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 Looking for an English version? Please see [README.en.md](README.en.md). *(本 README 以中文为主,当需要英文说明时可跳转到该文件。)* + +同时,转换流程也促使我编写了一套可复用的 HEIF/HEIC 结构化解析、诊断与重建工具:它能读出盒结构、修复三星特有的 shifted item ID、重算 `iloc` 偏移,并以可预测的方式重新写回文件。即便你暂时不需要完整的 Live Photo 转换,也能够将其中的模块用在其它 HEIC 调试或自定义处理流程中。 + +--- + +## 功能亮点 + +- **三星 Motion Photo → Apple Live Photo**:自动生成匹配的 HEIC 与 MOV,并写入统一的标识符;导入后即是一张可播放的 Live Photo。 +- **完整元数据补齐**:在 MOV 中注入 `ContentIdentifier`,在 HEIC 中写入 MakerNote / PhotoIdentifier 等 Apple 生态所需字段。 +- **HEIC 深层解析能力**:提供 `HEICFile`、`HEICBuilder` 等类,可读取 `ftyp`、`meta`、`iloc`、`iinf`、`iprp`、`iref` 等关键盒,适用于诊断和自定义修复。 +- **安全重建机制**:`HEICBuilder` 会自动重算偏移量与引用关系,避免手工编辑造成的结构损坏。 +- **CLI 与 Python API 并存**:既能在命令行一键转换,也可以在脚本、流水线甚至 GUI 中复用底层逻辑。 +- **真实样例随仓库提供**:包含原始三星 HEIC、转换后的 Apple 兼容文件与苹果原生样例,便于对照调试。 + +--- + +## 组件概览 + +| 组件 | 作用 | +| --- | --- | +| `scripts/samsung_to_live_photo.py` | 命令行转换脚本,输出苹果兼容的 HEIC + MOV | +| `scripts/batch_media_manager.py` | 图形化批处理工具:扫描目录、转换 Motion Photo、匹配 Live Photo 配对、清理由 MotionPhoto_Data 标记的冗余视频,并可归档原始素材 | +| `pyheic_struct.HEICFile` | 解析 HEIC 盒结构、重建主图、提取 Motion Photo 等 | +| `pyheic_struct.HEICBuilder` | 在修改元数据后重新写出 HEIC,确保偏移正确 | +| `pyheic_struct.convert_motion_photo` | 通用转换入口,可指定厂商与目标适配器 | +| `pyheic_struct.AppleTargetAdapter` | Apple 生态适配器,负责写入品牌、MakerNote、ContentIdentifier 等 | +| `pyheic_struct.handlers.SamsungHandler` | 三星专用处理逻辑:识别 `mpvd`、校正 shifted IDs | +| `inspect_heic.py` | 快速巡检脚本,输出盒结构、偏移与引用信息 | + +--- + +## 安装与环境 + +- **Python 3.10+** +- 必装依赖:`Pillow>=10.0.0`、`pillow-heif>=0.15.0` +- 新增依赖:`piexif>=1.1.3`(用于 MakerNote 写入) +- 可选依赖:`exiftool-wrapper>=0.5.0` +- 推荐系统安装 [ExifTool](https://exiftool.org/) + - macOS: `brew install exiftool` + - Ubuntu/Debian: `sudo apt install libimage-exiftool-perl` + - Windows: 下载官方 exe 并加入 `PATH` + +安装方式: + +```bash +# 仓库根目录安装 +pip install . + +# 或开发模式(含可选依赖) +pip install -e .[full] +``` + +如仅需解析能力,可忽略 `.[full]`;若要写入 MOV 的 `ContentIdentifier`,请启用 `full` 并确保系统可调用 `exiftool`。 + +--- + +## 快速开始:把三星动态照片变成 Live Photo + +### 1. 准备样例 + +仓库中已经附带常用测试素材(位于 `examples/` 目录): + +- `examples/samsung.heic`:原始三星 Motion Photo +- `examples/samsung_apple_compatible.HEIC` / `.MOV`:转换后的示例输出 +- `examples/apple.HEIC`:来自苹果的原生 Live Photo 参考 + +### 2. 命令行一键转换 + +```bash +python3 scripts/samsung_to_live_photo.py examples/samsung.heic \ + --output-dir output/live +``` + +生成的 `output/live/samsung_apple_compatible.heic` 与 `.mov` 拥有同一个 `ContentIdentifier` 和 `PhotoIdentifier`,可直接导入 macOS「照片」验证。 + +常用参数: + +| 参数 | 说明 | +| ---- | ---- | +| `source` | 必选,原始三星 HEIC | +| `--output-dir` | 输出目录,默认与源文件相同路径 | +| `--heic-name` / `--mov-name` | 输出文件名(不含路径),默认 `*_apple_compatible` | +| `--skip-mov-tag` | 无 `exiftool` 时跳过 MOV 的 `ContentIdentifier` 注入 | + +### 3. 通过 API 集成 + +```python +from pathlib import Path +from pyheic_struct import convert_samsung_motion_photo, HEICFile, HEICBuilder + +heic_path, mov_path = convert_samsung_motion_photo( + "examples/samsung.heic", + output_still=Path("converted/apple_ready.HEIC"), + output_video=Path("converted/apple_ready.MOV"), +) + +# 读取重建后的 HEIC,执行进一步操作 +rebuilt = HEICFile(str(heic_path)) +rebuilt.set_content_identifier("MY-INTERNAL-ID") +HEICBuilder(rebuilt).write("converted/customized.HEIC") +``` + +--- + +### 4. 图形化批处理工具(GUI) + +`scripts/batch_media_manager.py` 提供完整的桌面界面,便于批量巡检目录: + +- 递归扫描 HEIC/MOV/MP4 等媒体,并以进度条与日志反馈处理情况; +- 检测 Live Photo 配对,自动移动缺失伴侣的孤立视频到安全子目录; +- 将三星 Motion Photo 转换为 Apple 兼容的 HEIC + MOV,并补写 `ContentIdentifier`; +- 清理带 `MotionPhoto_Data` 标签、与 Motion Photo 重复的冗余 MP4; +- 可选地把成功转换的原始素材按照原路径层级移动到 `归档/`(或 `Archive/`)并打包 zip; +- 内置“停止”按钮和窗口关闭钩子,可安全取消长时间任务。 + +运行示例: + +```bash +python3 scripts/batch_media_manager.py +``` + +--- + +## Live Photo 转换管线解读 + +1. **解析原始 HEIC**:读取 `ftyp`、`meta`、`iloc` 等盒;通过 handler 自动识别三星文件并查找 `mpvd` 盒。 +2. **重建主图像**:借助 `pillow-heif` 还原三星的网格化主图,写入临时平面 HEIC(`mif1` brand)。 +3. **提取并存储 MOV**:抽取 `mpvd` 中的嵌入视频,写成独立 MOV,并用 `exiftool` 写入 `ContentIdentifier`(如未安装可跳过)。 +4. **修正 item ID 与引用**:三星会将 item ID 左移 16 位;工具会同步修复 `iinf`、`ipma`、`iref`。 +5. **补齐 Apple 元数据**:设置苹果兼容的 `ftyp` 品牌,写入新的 `ContentIdentifier`、`PhotoIdentifier`,填充 MakerNote。 +6. **最终重建**:`HEICBuilder` 重新计算 `iloc` 偏移、盒大小并写回,释放临时文件。 + +了解以上步骤,有助于调试其它厂商的 Motion Photo,或在需要时定制新的 TargetAdapter。 + +--- + +## 核心 API 快速索引 + +- `pyheic_struct.convert_motion_photo(...)`:通用转换接口,可指定 `vendor_hint`(如 `"samsung"`)与 `TargetAdapter`。 +- `pyheic_struct.convert_samsung_motion_photo(...)`:三星专用封装,调用更简洁。 +- `pyheic_struct.HEICFile`: + - `get_primary_item_id()`、`reconstruct_primary_image()`、`list_items()` 等解析方法 + - `set_content_identifier()`、`set_exif_maker_note()` 等元数据操作 +- `pyheic_struct.HEICBuilder`:将修改后的 `HEICFile` 安全写回磁盘。 +- `pyheic_struct.handlers.VendorHandler`:厂商自定义扩展基类。 +- `pyheic_struct.targets.TargetAdapter` / `AppleTargetAdapter`:目标生态适配接口。 + +--- + +## 扩展与自定义 + +1. **实现新的厂商 Handler**:继承 `VendorHandler`,实现 `matches`、`extract_motion_video`、`prepare_flat_heic` 等方法,并注册到 `HANDLER_REGISTRY`。 +2. **接入其它目标生态**:继承 `TargetAdapter`,在 `apply_to_flat_heic` / `post_process_mov` 中写入所需元数据或外部脚本。 +3. **诊断 HEIC 文件**:使用 `inspect_heic.py` 或直接调 `HEICFile` API,输出盒结构、偏移与引用,便于对比不同厂商实现。 + +--- + +## 常见问题 + +- **导入照片后仍显示两个文件?** + 请确认系统安装了 `exiftool`,并在转换时未使用 `--skip-mov-tag`。同时建议使用工具生成的最新 HEIC/MOV,避免手动修改导致标识符不匹配。 + +- **能否支持其它厂商的 Motion Photo?** + 可以。通过实现新的 handler 并组合自定义 target adapter,即可重用现有的重建管线。 + +- **英文说明在哪里?** + 参见 [README.en.md](README.en.md);若你想贡献改进英文文档,也欢迎提交 PR。 + +--- + +## 开发提示 + +- 项目使用 `ruff` / `black` 风格指南,可根据需要自行配置。 +- 在修改 HEIC 结构后务必重新运行转换脚本,确保 `HEICBuilder` 输出的文件能被 `pillow-heif` 与 `exiftool` 读取。 +- 欢迎围绕 HEIC 解析、Motion Photo 适配提交 issue 或 PR,特别是其它安卓厂商的兼容性案例。 diff --git a/apple.HEIC b/examples/apple.HEIC similarity index 100% rename from apple.HEIC rename to examples/apple.HEIC diff --git a/apple.MOV b/examples/apple.MOV similarity index 100% rename from apple.MOV rename to examples/apple.MOV diff --git a/apple_samsung_compatible.heic b/examples/apple_samsung_compatible.heic similarity index 100% rename from apple_samsung_compatible.heic rename to examples/apple_samsung_compatible.heic diff --git a/samsung.heic b/examples/samsung.heic similarity index 100% rename from samsung.heic rename to examples/samsung.heic diff --git a/samsung_apple_compatible.HEIC b/examples/samsung_apple_compatible.HEIC similarity index 100% rename from samsung_apple_compatible.HEIC rename to examples/samsung_apple_compatible.HEIC diff --git a/samsung_apple_compatible.MOV b/examples/samsung_apple_compatible.MOV similarity index 100% rename from samsung_apple_compatible.MOV rename to examples/samsung_apple_compatible.MOV diff --git a/pyheic_struct.egg-info/PKG-INFO b/pyheic_struct.egg-info/PKG-INFO index d3f4828..8eb02c3 100644 --- a/pyheic_struct.egg-info/PKG-INFO +++ b/pyheic_struct.egg-info/PKG-INFO @@ -8,203 +8,188 @@ Requires-Python: >=3.10 Description-Content-Type: text/markdown Requires-Dist: Pillow>=10.0.0 Requires-Dist: pillow-heif>=0.15.0 +Requires-Dist: piexif>=1.1.3 Provides-Extra: full Requires-Dist: exiftool-wrapper>=0.5.0; extra == "full" # pyheic-struct -`pyheic-struct` 是一个专注于 HEIF/HEIC 结构化解析、诊断与重建的 Python 工具包。项目在三星动态照片(Motion Photo)与苹果生态兼容场景下积累了大量经验,既提供一键式的命令行转换,也暴露底层 API 便于集成到自定义流程中。 +`pyheic-struct` 诞生的初衷,是为了解决我个人在迁移照片时遇到的需求:把三星手机拍摄的动态照片(Motion Photo)批量转换成 iOS/macOS 可以直接识别、导入的 Live Photo。 +工具会自动拆分并重建 HEIC 与 MOV 配对文件,补齐 Apple 生态所必需的 `ContentIdentifier`、`PhotoIdentifier` 与 MakerNote 元数据,让 macOS「照片」或 iOS 设备将它们视为同一张 Live Photo,而不是互不关联的静态图与视频。 -## 为什么选择 pyheic-struct? +> Looking for an English version? Please see [README.en.md](README.en.md). *(本 README 以中文为主,当需要英文说明时可跳转到该文件。)* -- **跨厂商兼容痛点**:三星 HEIC 使用网格化主图和自定义 `mpvd` 盒,苹果设备无法直接识别。工具对这些差异做了完整抽象。 -- **低层结构可视化**:快速读取 `ftyp`、`meta`、`iloc`、`iinf`、`iprp`、`iref` 等关键盒,辅助调试偏移量与引用关系。 -- **安全重建**:`HEICBuilder` 会自动重新计算偏移和引用,避免手工修改导致的文件损坏。 -- **CLI + API 双入口**:既可以在命令行完成日常迁移,也能以库的方式嵌入流水线或 GUI 工具。 -- **附带样例资源**:仓库内置多个真实 HEIC/MOV 用例,方便对照分析与学习。 +同时,转换流程也促使我编写了一套可复用的 HEIF/HEIC 结构化解析、诊断与重建工具:它能读出盒结构、修复三星特有的 shifted item ID、重算 `iloc` 偏移,并以可预测的方式重新写回文件。即便你暂时不需要完整的 Live Photo 转换,也能够将其中的模块用在其它 HEIC 调试或自定义处理流程中。 -## 功能总览 +--- -- **结构解析**:`HEICFile` 提供盒树访问、主图 ID 查询、属性映射 (`ipma`) 等能力。 -- **网格图像重组**:借助 `pillow-heif` 还原三星的网格化主图,生成平面图像。 -- **Motion Photo 拆分**:自动提取嵌入的 MOV 数据并重新生成苹果兼容格式。 -- **元数据校正**:修复三星专用的 “shifted ID” 现象,补齐苹果生态所需的 `ContentIdentifier`。 -- **命令行转换**:`pyheic-struct` CLI 一键完成转换、打标签与临时文件清理。 +## 功能亮点 -## 基础概念速览 +- **三星 Motion Photo → Apple Live Photo**:自动生成匹配的 HEIC 与 MOV,并写入统一的标识符;导入后即是一张可播放的 Live Photo。 +- **完整元数据补齐**:在 MOV 中注入 `ContentIdentifier`,在 HEIC 中写入 MakerNote / PhotoIdentifier 等 Apple 生态所需字段。 +- **HEIC 深层解析能力**:提供 `HEICFile`、`HEICBuilder` 等类,可读取 `ftyp`、`meta`、`iloc`、`iinf`、`iprp`、`iref` 等关键盒,适用于诊断和自定义修复。 +- **安全重建机制**:`HEICBuilder` 会自动重算偏移量与引用关系,避免手工编辑造成的结构损坏。 +- **CLI 与 Python API 并存**:既能在命令行一键转换,也可以在脚本、流水线甚至 GUI 中复用底层逻辑。 +- **真实样例随仓库提供**:包含原始三星 HEIC、转换后的 Apple 兼容文件与苹果原生样例,便于对照调试。 -- **HEIF/HEIC**:高效图像容器格式,内部使用多层 Box 结构存储图像、元数据和引用。 -- **Motion Photo**:静态图像与短视频打包的格式,三星与苹果在容器层实现差异较大。 -- **ContentIdentifier**:苹果生态用于绑定 HEIC 主图和 MOV 视频的元数据键值。 +--- -## 环境要求 +## 组件概览 -- Python 3.10+ -- 必装依赖:`Pillow>=10.0.0`, `pillow-heif>=0.15.0` -- 可选依赖:`exiftool-wrapper>=0.5.0`(当需要自动写入 MOV 的 `ContentIdentifier` 时调用外部 `exiftool`) -- 可执行的 `exiftool`(推荐,但非必须) +| 组件 | 作用 | +| --- | --- | +| `scripts/samsung_to_live_photo.py` | 命令行转换脚本,输出苹果兼容的 HEIC + MOV | +| `scripts/batch_media_manager.py` | 图形化批处理工具:扫描目录、转换 Motion Photo、匹配 Live Photo 配对、清理由 MotionPhoto_Data 标记的冗余视频,并可归档原始素材 | +| `pyheic_struct.HEICFile` | 解析 HEIC 盒结构、重建主图、提取 Motion Photo 等 | +| `pyheic_struct.HEICBuilder` | 在修改元数据后重新写出 HEIC,确保偏移正确 | +| `pyheic_struct.convert_motion_photo` | 通用转换入口,可指定厂商与目标适配器 | +| `pyheic_struct.AppleTargetAdapter` | Apple 生态适配器,负责写入品牌、MakerNote、ContentIdentifier 等 | +| `pyheic_struct.handlers.SamsungHandler` | 三星专用处理逻辑:识别 `mpvd`、校正 shifted IDs | +| `inspect_heic.py` | 快速巡检脚本,输出盒结构、偏移与引用信息 | + +--- + +## 安装与环境 + +- **Python 3.10+** +- 必装依赖:`Pillow>=10.0.0`、`pillow-heif>=0.15.0` +- 新增依赖:`piexif>=1.1.3`(用于 MakerNote 写入) +- 可选依赖:`exiftool-wrapper>=0.5.0` +- 推荐系统安装 [ExifTool](https://exiftool.org/) - macOS: `brew install exiftool` - Ubuntu/Debian: `sudo apt install libimage-exiftool-perl` - - Windows: 安装 [ExifTool for Windows](https://exiftool.org/) 并将可执行文件加入 `PATH` + - Windows: 下载官方 exe 并加入 `PATH` -## 安装 - -仓库当前以源码分发为主,可按需选择以下方式: +安装方式: ```bash -# 方式一:直接在仓库根目录安装 +# 仓库根目录安装 pip install . -# 方式二:开发模式安装(便于调试) +# 或开发模式(含可选依赖) pip install -e .[full] ``` -若只需要解析功能,可省略 `.[full]`;如果计划写入 `ContentIdentifier`,请启用 `full` 额外安装 `exiftool-wrapper`。 +如仅需解析能力,可忽略 `.[full]`;若要写入 MOV 的 `ContentIdentifier`,请启用 `full` 并确保系统可调用 `exiftool`。 -## 快速上手 +--- -### 样例数据 +## 快速开始:把三星动态照片变成 Live Photo -仓库附带若干示例: +### 1. 准备样例 -- `samsung.heic`:原始三星 Motion Photo -- `samsung_apple_compatible.HEIC` / `.MOV`:转换后的示例输出 -- `apple.HEIC`:苹果原生样例 +仓库中已经附带常用测试素材(位于 `examples/` 目录): -可以使用它们验证流程或对比结构差异。 +- `examples/samsung.heic`:原始三星 Motion Photo +- `examples/samsung_apple_compatible.HEIC` / `.MOV`:转换后的示例输出 +- `examples/apple.HEIC`:来自苹果的原生 Live Photo 参考 -### 命令行转换 +### 2. 命令行一键转换 ```bash -pyheic-struct samsung.heic \ - --output-heic samsung_fixed.HEIC \ - --output-mov samsung_fixed.MOV +python3 scripts/samsung_to_live_photo.py examples/samsung.heic \ + --output-dir output/live ``` -参数说明: +生成的 `output/live/samsung_apple_compatible.heic` 与 `.mov` 拥有同一个 `ContentIdentifier` 和 `PhotoIdentifier`,可直接导入 macOS「照片」验证。 + +常用参数: | 参数 | 说明 | | ---- | ---- | -| `source` | 必选,原始三星 HEIC 路径 | -| `--output-heic` | 可选,输出 HEIC 路径,默认 `_apple_compatible.HEIC` | -| `--output-mov` | 可选,输出 MOV 路径,默认 `_apple_compatible.MOV` | -| `--skip-mov-tag` | 跳过为 MOV 写入 `ContentIdentifier`(无 `exiftool` 时可用) | +| `source` | 必选,原始三星 HEIC | +| `--output-dir` | 输出目录,默认与源文件相同路径 | +| `--heic-name` / `--mov-name` | 输出文件名(不含路径),默认 `*_apple_compatible` | +| `--skip-mov-tag` | 无 `exiftool` 时跳过 MOV 的 `ContentIdentifier` 注入 | -命令执行时会打印重建流程的关键日志,包括网格合成、ID 校正、临时文件清理等,出现异常时可直接复制日志用于排查。 - -### 作为库调用 +### 3. 通过 API 集成 ```python from pathlib import Path +from pyheic_struct import convert_samsung_motion_photo, HEICFile, HEICBuilder -from pyheic_struct import HEICFile, HEICBuilder, convert_samsung_motion_photo - -# 1. 解析结构 -heic = HEICFile("samsung.heic") -print("主图 Item ID:", heic.get_primary_item_id()) -print("所有条目:", list(heic.iter_items())) - -# 2. 自定义转换(可覆盖默认输出路径) heic_path, mov_path = convert_samsung_motion_photo( - "samsung.heic", - output_still=Path("converted/heic/apple_ready.HEIC"), - output_video=Path("converted/video/apple_ready.MOV"), + "examples/samsung.heic", + output_still=Path("converted/apple_ready.HEIC"), + output_video=Path("converted/apple_ready.MOV"), ) -# 3. 进一步加工:加载新的 HEIC 并注入其他元数据 +# 读取重建后的 HEIC,执行进一步操作 rebuilt = HEICFile(str(heic_path)) -rebuilt.set_content_identifier("INTERNAL-CONTENT-ID") -HEICBuilder(rebuilt).write("converted/heic/with_custom_id.HEIC") +rebuilt.set_content_identifier("MY-INTERNAL-ID") +HEICBuilder(rebuilt).write("converted/customized.HEIC") ``` -### 独立转换脚本 +--- -仓库提供 `scripts/samsung_live_photo.py`,用于在终端中直接生成苹果 Live Photo: +### 4. 图形化批处理工具(GUI) + +`scripts/batch_media_manager.py` 提供完整的桌面界面,便于批量巡检目录: + +- 递归扫描 HEIC/MOV/MP4 等媒体,并以进度条与日志反馈处理情况; +- 检测 Live Photo 配对,自动移动缺失伴侣的孤立视频到安全子目录; +- 将三星 Motion Photo 转换为 Apple 兼容的 HEIC + MOV,并补写 `ContentIdentifier`; +- 清理带 `MotionPhoto_Data` 标签、与 Motion Photo 重复的冗余 MP4; +- 可选地把成功转换的原始素材按照原路径层级移动到 `归档/`(或 `Archive/`)并打包 zip; +- 内置“停止”按钮和窗口关闭钩子,可安全取消长时间任务。 + +运行示例: ```bash -python3 scripts/samsung_live_photo.py samsung.heic --output-dir output/live +python3 scripts/batch_media_manager.py ``` -脚本默认会为 HEIC 与 MOV 写入同一个 `ContentIdentifier`(依赖系统 `exiftool`)。若暂时无法安装,可附加 `--skip-mov-tag` 跳过 MOV 注入。 +--- -### 结构巡检脚本 +## Live Photo 转换管线解读 -`inspect_heic.py` 为常用的调试脚本,可快速查看盒结构、偏移和 Vendor 信息: +1. **解析原始 HEIC**:读取 `ftyp`、`meta`、`iloc` 等盒;通过 handler 自动识别三星文件并查找 `mpvd` 盒。 +2. **重建主图像**:借助 `pillow-heif` 还原三星的网格化主图,写入临时平面 HEIC(`mif1` brand)。 +3. **提取并存储 MOV**:抽取 `mpvd` 中的嵌入视频,写成独立 MOV,并用 `exiftool` 写入 `ContentIdentifier`(如未安装可跳过)。 +4. **修正 item ID 与引用**:三星会将 item ID 左移 16 位;工具会同步修复 `iinf`、`ipma`、`iref`。 +5. **补齐 Apple 元数据**:设置苹果兼容的 `ftyp` 品牌,写入新的 `ContentIdentifier`、`PhotoIdentifier`,填充 MakerNote。 +6. **最终重建**:`HEICBuilder` 重新计算 `iloc` 偏移、盒大小并写回,释放临时文件。 -```bash -python inspect_heic.py samsung_apple_compatible.HEIC -``` +了解以上步骤,有助于调试其它厂商的 Motion Photo,或在需要时定制新的 TargetAdapter。 -输出包含 `ftyp`、`iloc`、`ipma`、`mpvd` 等关键节点,便于手动验证转换是否成功。 +--- -## Motion Photo 转换流程解读 +## 核心 API 快速索引 -1. **载入原始 HEIC**:定位 `ftyp`、`meta`、`iloc` 等盒;若未自动识别厂商,会尝试查找三星专有的 `mpvd` 盒。 -2. **主图重构**:从三星的网格化结构还原单张平面图像,并以临时 HEIC(`mif1` 品牌)写入。 -3. **视频提取**:若存在 Motion Photo 数据,则写出 MOV;在具备 `exiftool` 时写入 `ContentIdentifier`。 -4. **ID 校正**:处理三星在 `iinf`、`ipma`、`iref` 等盒中的 “shifted ID” 现象,确保苹果解析器能正确关联。 -5. **元数据注入**:更新 `ftyp` 兼容字符串、写入新的 `ContentIdentifier`,必要时可自定义。 -6. **最终重建**:`HEICBuilder` 重新计算每个盒的偏移和长度,生成干净的苹果兼容 HEIC;同时清理临时文件。 +- `pyheic_struct.convert_motion_photo(...)`:通用转换接口,可指定 `vendor_hint`(如 `"samsung"`)与 `TargetAdapter`。 +- `pyheic_struct.convert_samsung_motion_photo(...)`:三星专用封装,调用更简洁。 +- `pyheic_struct.HEICFile`: + - `get_primary_item_id()`、`reconstruct_primary_image()`、`list_items()` 等解析方法 + - `set_content_identifier()`、`set_exif_maker_note()` 等元数据操作 +- `pyheic_struct.HEICBuilder`:将修改后的 `HEICFile` 安全写回磁盘。 +- `pyheic_struct.handlers.VendorHandler`:厂商自定义扩展基类。 +- `pyheic_struct.targets.TargetAdapter` / `AppleTargetAdapter`:目标生态适配接口。 -理解该流程有助于调优转换策略、扩展到其他厂商或容器结构。 +--- -## 主要 API 速查 +## 扩展与自定义 -- `pyheic_struct.HEICFile(path)`:读取 HEIC,提供盒访问、`get_motion_photo_data()`、`reconstruct_primary_image()` 等方法。 -- `pyheic_struct.HEICBuilder(heic_file)`:将修改后的 `HEICFile` 写回磁盘,自动处理偏移修正。 -- `pyheic_struct.convert_samsung_motion_photo(...)`:高阶入口,封装完整的三星 Motion Photo 转苹果流程。 -- `pyheic_struct.handlers.*`:不同厂商的定制 Handler,可扩展以支持更多特性。 +1. **实现新的厂商 Handler**:继承 `VendorHandler`,实现 `matches`、`extract_motion_video`、`prepare_flat_heic` 等方法,并注册到 `HANDLER_REGISTRY`。 +2. **接入其它目标生态**:继承 `TargetAdapter`,在 `apply_to_flat_heic` / `post_process_mov` 中写入所需元数据或外部脚本。 +3. **诊断 HEIC 文件**:使用 `inspect_heic.py` 或直接调 `HEICFile` API,输出盒结构、偏移与引用,便于对比不同厂商实现。 -- `pyheic_struct.convert_motion_photo(...)`:全新的通用转换入口,可指定 `vendor_hint` 与目标 `TargetAdapter`。 -- `pyheic_struct.AppleTargetAdapter` / `pyheic_struct.TargetAdapter`:目标生态适配器接口,默认实现负责苹果兼容。 -- `pyheic_struct.handlers.VendorHandler`:厂商扩展基类,提供 `matches`、`extract_motion_video`、`prepare_flat_heic` 等钩子。 - -更多内部实现细节可直接阅读 `pyheic_struct/` 包目录下的源代码。 - -## 扩展 Motion Photo 适配 - -1. **实现厂商 Handler**:在 `pyheic_struct/handlers/` 中继承 `VendorHandler`,实现 `matches`(自动检测)、`extract_motion_video`(解析嵌入视频)与 `prepare_flat_heic`(修复偏移、元数据)等方法。 -2. **注册 Handler**:将新类加入 `HANDLER_REGISTRY`,即可参与自动检测,也可以通过 `vendor_hint="your_vendor"` 强制指定。 -3. **自定义目标适配器(可选)**:若需要输出其他生态格式,继承 `TargetAdapter`,覆盖 `apply_to_flat_heic` 和 `post_process_mov`。 -4. **运行全量转换**:调用通用 API `convert_motion_photo(..., vendor_hint="your_vendor", target_adapter=YourAdapter())` 验证流程。 - -通过这一解耦结构,可以在不修改核心转换逻辑的情况下迭代更多厂商或目标平台的适配。 - -## 开发与调试 - -```bash -# 建议使用虚拟环境 -python -m venv .venv -source .venv/bin/activate # Windows 用 .venv\Scripts\activate - -# 安装依赖 -pip install -e .[full] - -# 快速验证 -python -m pyheic_struct samsung.heic -python inspect_heic.py samsung_apple_compatible.HEIC -``` - -其它实用脚本: - -- `test.py`:对生成的 HEIC 做首段十六进制检查。 -- `main.py`:演示性入口,可作为定制脚本模板。 - -欢迎通过 Issues/PR 提交解析改进或新增厂商 Handler。 +--- ## 常见问题 -- **提示找不到 `exiftool`**:说明环境未安装或未加入 `PATH`,可临时使用 `--skip-mov-tag` 跳过 MOV 元数据写入。 -- **转换后只有 HEIC 没有 MOV**:原始文件可能不包含 Motion Photo 数据,可借助 `inspect_heic.py` 查看是否存在 `mpvd` 盒。 -- **苹果仍无法识别**:请确认手动修改流程中是否重新执行了 `HEICBuilder.write()`,以及 `ContentIdentifier` 是否匹配 HEIC/MOV。 -- **解析非三星文件**:目前对其他厂商测试有限,解析流程通常可用,但转换逻辑需要自行验证。 +- **导入照片后仍显示两个文件?** + 请确认系统安装了 `exiftool`,并在转换时未使用 `--skip-mov-tag`。同时建议使用工具生成的最新 HEIC/MOV,避免手动修改导致标识符不匹配。 -## 局限与后续计划 +- **能否支持其它厂商的 Motion Photo?** + 可以。通过实现新的 handler 并组合自定义 target adapter,即可重用现有的重建管线。 -- 尚未覆盖所有 HEIF 扩展盒类型,复杂属性可能需要额外支持。 -- 未提供官方测试套件,建议在生产环境前自行编写回归测试。 -- 未来计划加入对华为、小米等厂商 Motion Photo 的实验性适配。 +- **英文说明在哪里?** + 参见 [README.en.md](README.en.md);若你想贡献改进英文文档,也欢迎提交 PR。 -## 许可证 +--- -MIT License — 可自由用于商业及非商业用途。贡献代码即视为接受该许可条款。 +## 开发提示 + +- 项目使用 `ruff` / `black` 风格指南,可根据需要自行配置。 +- 在修改 HEIC 结构后务必重新运行转换脚本,确保 `HEICBuilder` 输出的文件能被 `pillow-heif` 与 `exiftool` 读取。 +- 欢迎围绕 HEIC 解析、Motion Photo 适配提交 issue 或 PR,特别是其它安卓厂商的兼容性案例。 diff --git a/pyheic_struct.egg-info/SOURCES.txt b/pyheic_struct.egg-info/SOURCES.txt index de13a13..4487dab 100644 --- a/pyheic_struct.egg-info/SOURCES.txt +++ b/pyheic_struct.egg-info/SOURCES.txt @@ -1,5 +1,6 @@ README.md pyproject.toml +docs/README.md pyheic_struct/__init__.py pyheic_struct/__main__.py pyheic_struct/base.py diff --git a/pyheic_struct.egg-info/requires.txt b/pyheic_struct.egg-info/requires.txt index bd0cc9c..4e6e9f2 100644 --- a/pyheic_struct.egg-info/requires.txt +++ b/pyheic_struct.egg-info/requires.txt @@ -1,5 +1,6 @@ Pillow>=10.0.0 pillow-heif>=0.15.0 +piexif>=1.1.3 [full] exiftool-wrapper>=0.5.0 diff --git a/pyproject.toml b/pyproject.toml index fea9c6c..96cab04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = [ { name = "Nymiro Yi", email = "dmwf994@dmwf994.com" } ] license = { text = "MIT" } -readme = "README.md" +readme = "docs/README.md" requires-python = ">=3.10" dependencies = [ "Pillow>=10.0.0", diff --git a/inspect_heic.py b/scripts/inspect_heic.py similarity index 96% rename from inspect_heic.py rename to scripts/inspect_heic.py index 6383ec5..060e89a 100644 --- a/inspect_heic.py +++ b/scripts/inspect_heic.py @@ -105,7 +105,7 @@ def inspect_file(filename: str): if __name__ == "__main__": if len(sys.argv) < 2: - print("用法: python inspect_heic.py <文件名>") - print("例如: python inspect_heic.py samsung.heic") + print("用法: python scripts/inspect_heic.py <文件名>") + print("例如: python scripts/inspect_heic.py examples/samsung.heic") else: inspect_file(sys.argv[1]) diff --git a/main.py b/scripts/main.py similarity index 100% rename from main.py rename to scripts/main.py diff --git a/test.py b/scripts/test.py similarity index 75% rename from test.py rename to scripts/test.py index d473d22..21c7abf 100644 --- a/test.py +++ b/scripts/test.py @@ -1,4 +1,9 @@ import os +from pathlib import Path + + +ROOT_DIR = Path(__file__).resolve().parents[1] +DEFAULT_EXAMPLE = ROOT_DIR / "examples" / "samsung_apple_compatible.HEIC" # --- Hex Dump Utility --- def hexdump(data, length=16): @@ -22,11 +27,11 @@ def hexdump(data, length=16): # --- End Utility --- -filename = 'samsung_apple_compatible.HEIC' +filename = os.environ.get("HEIC_SAMPLE", str(DEFAULT_EXAMPLE)) if not os.path.exists(filename): print(f"Error: File not found: {filename}") - print("Please make sure you are running this in the same directory as the generated file.") + print("Set HEIC_SAMPLE environment variable or place the sample under examples/.") else: try: with open(filename, 'rb') as f: @@ -38,4 +43,4 @@ else: print("="*60) except Exception as e: - print(f"An error occurred while reading the file: {e}") \ No newline at end of file + print(f"An error occurred while reading the file: {e}")