iOS:压缩和解压live图(.livp)——百度一刻相册+百度网盘识别支持

市面上首篇把静态图+mov短视频打包回live图的方案。

iOS:压缩和解压live图(.livp)------百度一刻相册+百度网盘识别支持

重磅原创!

背景

iPhone拍照的live图实际上是一个静态图+一个短视频,如果你把它导入电脑会得到一个文件夹(里面为xx.heic静态图+xx.mov短视频),如果你把它导入网盘或照片同步工具(如一刻相册)则会得到一个.livp格式的文件(其实是xx.heic静态图和xx.mov短视频的zip)。

截至今日市面上有大量 的 live图/.livp文件 解压或转静态图的方案,但未能找到任何关于如何把 .heic静态图+.mov短视频 压缩/打包 回软件可以识别的live图的方案。(直接压缩为zip的话网盘/第三方相册识别不了的)

经过一个下午的研究,终于找到了稳定的可以把 静态图+短视频 打包回 百度网盘和一刻相册可以直接识别的(.livp格式)live图的方案。

livp文件组成介绍

首先需要明确的是,.livp文件的得到方式是 xx.heic静态图+xx.mov短视频 这两个文件直接压缩为.zip压缩包,然后修改后缀.zip为.livp。

关于网盘/第三方相册能否识别的核心就在于压缩为.zip时候的具体压缩方式。具体的zip压缩需要满足以下几点:

  1. 不压缩(别扭吗?不别扭)。zip命令添加参数-0代表只打包不做压缩,保持原始字节。
  2. 去掉额外的文件属性(eXclude extra file attributes)。zip命令添加参数-X代表 不保存文件的Unix UID/GID等信息。
  3. 注释。还需要使用-z参数给.zip文件添加形如0002000000300022175F0003002217BE003D8A51313030304C495650的注释,用来快速获取原始文件数据在zip文件中的位置信息。

至于注释 是什么含义?注释一共有以下几部分组成:

复制代码
0002 00000030 000E26AB 0003 000E270A 004B806E 313030304C495650
0002 00000030 0010B331 0003 0010B390 004208DB 313030304C495650
part1  part2    part3  part4  part5   part6       part7
  • part1: 0002
  • part2: 00000030
  • part3: heic字节数(16进制)
  • part4: 0003
  • part5: part3+(30+静态图文件名长度)+(30+动态图文件名长度),目前来看其中静态图文件名长度必须是18
  • part6: mov字节数(16进制)
  • part7: 313030304C495650(ASCII含义为1000LIVP)

part3和part6的8位的字节数最大约 2 4 × 8 2^{4\times 8} 24×8字节也就是大约4 GiB。

part1、part2、part4、part7都为固定值,part7的ASCII含义为1000LIVP。

python 复制代码
hex_str = "313030304C495650"
bytes_obj = bytes.fromhex(hex_str)
ascii_str = bytes_obj.decode("ascii")
print(ascii_str)  # 1000LIVP

文件夹转livp方法

若能保证某文件夹只有 一个.heic后缀的静态图、一个.mov后缀的动态视频,则可以将以下代码保存为gen_livp_comment.py并调用python gen_livp_comment.py 文件夹路径名命令获取这个文件的comment:

python 复制代码
'''
Author: LetMeFly
Date: 2026-07-04 16:41:49
LastEditors: LetMeFly.xyz
LastEditTime: 2026-07-05 18:21:02
'''
import os
import glob
import random
import string
import sys


def u32_hex(x: int) -> str:
    """转8位大写十六进制(32-bit)"""
    return f"{x & 0xFFFFFFFF:08X}"


def find_single(pattern: str) -> str:
    files = glob.glob(pattern)
    if len(files) != 1:
        raise RuntimeError(
            f"Expected exactly 1 file for {pattern}, got {len(files)}: {files}"
        )
    return files[0]


def fix_heic_filename(path: str) -> str:
    """
    保证HEIC文件名(含扩展名)长度为18。
    不满足则直接重命名文件。
    """
    folder = os.path.dirname(path)
    filename = os.path.basename(path)

    stem, ext = os.path.splitext(filename)

    TARGET = 18
    stem_len = TARGET - len(ext)

    if len(stem) > stem_len:
        new_stem = stem[:stem_len]
    elif len(stem) < stem_len:
        new_stem = stem + "".join(
            random.choices(
                string.ascii_letters + string.digits,
                k=stem_len - len(stem),
            )
        )
    else:
        new_stem = stem

    new_name = new_stem + ext

    if new_name == filename:
        return path

    new_path = os.path.join(folder, new_name)

    if os.path.exists(new_path):
        raise RuntimeError(f"目标文件已存在:{new_name}")

    os.rename(path, new_path)

    print(f"Rename: {filename} -> {new_name}")

    return new_path


def generate_comment(folder: str) -> str:
    heic_path = find_single(os.path.join(folder, "*.heic"))
    mov_path = find_single(os.path.join(folder, "*.mov"))

    # 保证HEIC文件名长度为18
    heic_path = fix_heic_filename(heic_path)

    heic_size = os.path.getsize(heic_path)
    mov_size = os.path.getsize(mov_path)

    heic_name = os.path.basename(heic_path)
    mov_name = os.path.basename(mov_path)

    part3 = heic_size
    part5 = part3 + (30 + len(heic_name)) + (30 + len(mov_name))
    part6 = mov_size

    if part3 < 0 or part3 > 0xFFFFFFFF:
        raise ValueError("HEIC size out of 32-bit range")
    if part5 > 0xFFFFFFFF:
        raise ValueError("part5 overflow 32-bit")

    comment = (
        "0002"
        "00000030"
        + u32_hex(part3)
        + "0003"
        + u32_hex(part5)
        + u32_hex(part6)
        + "313030304C495650"
    )

    return comment

# python livp.py ~/Downloads/Photos/IMG_5190
if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: python livp.py <folder>")
        sys.exit(1)

    folder = sys.argv[1]
    print(generate_comment(folder))

这个脚本可以得到一串长为56的16进制数字组成的字符串(如0002000000300022175F0003002217BE003D8A51313030304C495650),之后再使用如下zip命令将这个文件夹下的静态图(IMG_4770.HEIC.heic)和动态视频(IMG_4770.HEIC.mov)压缩为live图文件:

bash 复制代码
zip -0 -X -z LetMeFly.xyz.Handsome.livp IMG_4770.HEIC.heic IMG_4770.HEIC.mov <<EOF
0002000000300022175F0003002217BE003D8A51313030304C495650
EOF

这样你将会得到一个LetMeFly.xyz.Handsome.livp文件,把这个文件拖入百度网盘或者一刻相册,你就能得到一个可以直接预览和播放的live图了。

杂念

为何没有两图压为live图的方案?因为实际上.livp格式的live图本来就不是Apple公司官方定义的,只是大多互联网厂商为了适配苹果live图推出的不约而同的格式。

把相册中live图下载后解压缩比较容易,但是由于没有官方的压缩方案,所以压缩回去就比较困难了。

Mac/iOS系统上的live图方案(以及传输到Windows上)

iOS拍摄的live图如果想通过iCloud之外的方式同步到其他电脑/设备,主要有两个地方可以设置:

  1. 数据线传输时:iPhone -> 设置(settings) -> 应用(Apps) -> 照片(photos) -> 最下面的"传输到Mac或PC"(Transfer to Mac or PC) -> 保留原片(Keep Originals)
  2. AirDrop时:分享 -> 选项(Options) -> 打开"所有照片数据"(All Photos Data)

这样分享一张照片会在电脑上得到一个文件夹。

将一个文件夹下所有的live图文件夹们打包成.livp文件的脚本

将手机上一堆照片传输到电脑上后,会得到一个个如IMG_5526的文件夹。

将这些文件夹所在的文件夹路径记下来(如果在下载文件夹下则路径为~/Downloads),把以下脚本保存为lets_export_photos.py,并运行命令python lets_export_photos.py 照片文件夹所在路径命令,即可在照片文件夹所在路径/exported文件夹下找到一个个导出的照片文件,有的是静态图有的是live图,直接拖拽到百度网盘或一刻相册即可直接上传。

python 复制代码
'''
Author: LetMeFly
Date: 2026-07-04 16:41:49
LastEditors: LetMeFly.xyz
LastEditTime: 2026-07-05 18:09:50
'''
import os
import shutil
import subprocess
import sys
import random
import string


IMAGE_EXTS = {
    ".heic",
    ".jpg",
    ".jpeg",
    ".png",
    ".gif",
    ".bmp",
    ".tif",
    ".tiff",
    ".webp",
}


def u32_hex(x: int) -> str:
    return f"{x & 0xFFFFFFFF:08X}"


def fix_len18(filename: str) -> str:
    stem, ext = os.path.splitext(filename)

    target = 18
    stem_len = target - len(ext)

    if len(stem) > stem_len:
        stem = stem[:stem_len]
    elif len(stem) < stem_len:
        stem += "".join(
            random.choices(
                string.ascii_letters + string.digits,
                k=stem_len - len(stem),
            )
        )

    return stem + ext


def generate_comment(heic_path: str, mov_path: str) -> str:
    heic_size = os.path.getsize(heic_path)
    mov_size = os.path.getsize(mov_path)

    # zip 中记录的是文件名,不含路径
    heic_name = os.path.basename(heic_path)
    mov_name = os.path.basename(mov_path)

    # Local File Header(30B) + 文件名长度
    offset = (30 + len(heic_name.encode("utf-8"))) + (
        30 + len(mov_name.encode("utf-8"))
    )

    part3 = heic_size
    part5 = part3 + offset
    part6 = mov_size

    if part5 > 0xFFFFFFFF:
        raise ValueError("HEIC file too large")  # 可能不到32-bit range就不行了
    if part6 > 0xFFFFFFFF:
        raise ValueError("MOV size out of 32-bit range")

    return (
        "0002"
        "00000030"
        + u32_hex(part3)
        + "0003"
        + u32_hex(part5)
        + u32_hex(part6)
        + "313030304C495650"
    )


def find_files(folder):
    files = [
        f
        for f in os.listdir(folder)
        if os.path.isfile(os.path.join(folder, f))
    ]

    heics = []
    movs = []
    images = []

    for f in files:
        ext = os.path.splitext(f)[1].lower()
        path = os.path.join(folder, f)

        if ext == ".heic":
            heics.append(path)
            images.append(path)
        elif ext == ".mov":
            movs.append(path)
        elif ext in IMAGE_EXTS:
            images.append(path)

    return heics, movs, images


def export_live(heic_path, mov_path, output_path):
    heic_dir = os.path.dirname(heic_path)
    heic_name = os.path.basename(heic_path)
    new_heic_name = fix_len18(heic_name)

    # 如果文件名需要修改,则临时复制一份
    temp_heic = heic_path
    need_delete = False

    if new_heic_name != heic_name:
        temp_heic = os.path.join(heic_dir, new_heic_name)
        shutil.copy2(heic_path, temp_heic)
        need_delete = True

    try:
        comment = generate_comment(temp_heic, mov_path)

        cmd = [
            "zip",
            "-0",
            "-X",
            "-z",
            output_path,
            os.path.basename(temp_heic),
            os.path.basename(mov_path),
        ]

        subprocess.run(
            cmd,
            cwd=heic_dir,
            input=comment + "\n",
            text=True,
            check=True,
        )
    finally:
        if need_delete:
            os.remove(temp_heic)


def process_parent(parent_folder):
    exported = os.path.join(parent_folder, "exported")
    os.makedirs(exported, exist_ok=True)

    errors = []
    success = 0

    for name in sorted(os.listdir(parent_folder)):
        folder = os.path.join(parent_folder, name)

        if not os.path.isdir(folder):
            continue

        if name == "exported":
            continue

        try:
            heics, movs, images = find_files(folder)

            # Live Photo
            if len(heics) == 1 and len(movs) == 1:
                output = os.path.join(exported, name + ".livp")
                export_live(heics[0], movs[0], output)
                success += 1
                continue

            # 静态图片
            if len(images) == 1 and len(movs) == 0:
                src = images[0]
                # TODO: dst命名重复情况处理 (如果不同子目录里存在同名图片) 虽然是小概率
                dst = os.path.join(exported, os.path.basename(src))
                shutil.copy2(src, dst)
                success += 1
                continue

            errors.append(
                f"{name}: "
                f"{len(heics)} heic, "
                f"{len(movs)} mov, "
                f"{len(images)} image(s)"
            )

        except Exception as e:
            errors.append(f"{name}: {e}")

    print(f"导出成功 {success} 个")

    if errors:
        print()
        print("=" * 60)
        print("以下目录处理失败:")
        for err in errors:
            print(" -", err)
        print("=" * 60)


if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage:")
        print("python livp.py <parent_folder>")
        sys.exit(1)

    process_parent(sys.argv[1])

特别说明:

你需要保证你系统上有python环境、zip命令。

上述脚本的执行过程中,例如目录结构为:

复制代码
Photos/
├── IMG_0001/
│   ├── a.heic
│   └── a.mov
├── IMG_0002/
│   └── b.heic
├── IMG_0003/
│   ├── c.heic
│   ├── d.heic
│   └── c.mov
└── ...

则执行python lets_export_photos.py ~/Downloads/Photos将会生成:

复制代码
Photos/
├── exported/
│   ├── IMG_0001.livp
│   ├── IMG_0002.heic
│   └── ...
├── IMG_0001/
├── IMG_0002/
└── ...

The End

后续将创建一个 专门把两图压缩为百度系产品可直接识别的live图的 Github开源脚本仓库,敬请期待。

同步发文于CSDN和我的个人博客,原创不易,转载经作者同意后请附上原文链接哦~

千篇源码题解已开源

相关推荐
唐诺6 小时前
Objective-C 之 KVC 详解
ios·objective-c·kvc
00后程序员张7 小时前
iOS 打包方式汇总 从 Xcode Archive 到轻量级工具链的 IPA 构建
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
CHB20 小时前
uni-app x 蒸汽模式 iOS版 性能测试基准报告 Benchmark
ios·uni-app·swiftui
美狐美颜SDK开放平台20 小时前
Android/iOS直播APP平台开发中的视频美颜SDK优化技巧
android·ios·音视频·美颜sdk·第三方美颜sdk
唐诺1 天前
Objective-C 之 Foundation框架 详解
ios·objective-c·foundation
专一的黑猫1 天前
写一个iPhone应用程序,要求可以输入名字,点击按钮后,会显示一段文字向输入的名字打招呼,例如输入“宝玉”,显示“你好,宝玉!”
ios·iphone
_瑞2 天前
深入理解灵动岛
前端·ios·app
黑科技iOS上架2 天前
分享一个开源的iOS应用AppIcon生成器
经验分享·ios
wabil3 天前
【LVGL】滑动切换页面的界面优化实践
开发语言·ios·swift