尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python实现HEIC批量转JPG:解决苹果照片Windows兼容问题

Python实现HEIC批量转JPG:解决苹果照片Windows兼容问题 简介这是一款专为Windows用户设计的HEIC批量转JPG工具面向苹果设备用户、数码摄影爱好者及办公场景中频繁处理iPhone截图或照片的普通用户切实解决HEIC格式在非苹果系统下无法预览、编辑、分享的兼容性痛点。资源包共4个文件2个可执行程序exe用于不同运行环境适配1个说明文档txt提供基础指引1个Python源码py便于技术用户二次开发或理解转换逻辑总大小65.57MB结构精简、开箱即用。已有123人下载学习适用于需高频批量处理照片的日常场景。用户可直接运行主程序完成一键转换享受智能跳过同名文件、实时进度反馈、自定义输出路径等实用功能同时获得防篡改保护机制与专业图标设计保障使用安全与体验一致性配套B站主页跳转与赞赏入口便于获取作者持续更新与技术支持。 苹果用户都有过这种经历手机里存的照片一堆HEIC格式拿到Windows电脑上双击却打不开要么提示格式不支持要么一片空白发给朋友也会出现对方看不了的情况。这个从iOS 11开始默认启用的HEIC格式省空间是真的省空间但兼容性也确实让人抓狂。我前阵子正好帮同事处理一批从iPhone导出的几百张照片全卡在这个格式问题上。网上找了一圈工具要么收费要么上传云端有隐私顾虑要么转换完画质肉眼可见地变差。索性自己动手写了一个HEIC批量转JPG工具思路不算复杂但把几个关键点都处理到位了一键批量转换、自动跳过已存在文件、对源文件零修改。今天把完整思路和代码整理出来分享给同样被这个问题困扰的朋友。1. 为什么苹果的HEIC格式总让人头疼1.1 HEIC到底是个什么东西HEIC的全称是High Efficiency Image File Format底层基于HEVCH.265视频编码标准。它和常见的JPG、PNG这类格式最大的区别在于编码方式JPG用的是离散余弦变换HEIC用的是帧内预测加变换编码的组合原理上更接近视频压缩。这带来的直接好处就是同样的画面质量HEIC文件体积大约只有JPG的一半。一张1200万像素的照片JPG大概3到5MBHEIC通常只有1.5到2.5MB。对于动辄几十GB照片库的用户来说这个空间节省非常可观这也是苹果从iOS 11开始默认采用HEIC的根本原因。但代价也很明显HEVC相关专利池的技术壁垒高生态支持远不如JPG成熟。Windows系统直到较新版本才原生支持部分HEIC解码且需要额外安装来自微软商店的扩展组件——这就是网上经常搜到的“win10 heic扩展”这个东西的由来。Android设备、部分网页端、老式相册软件基本都是打开一片黑或者直接提示无法识别。1.2 兼容性问题到底出现在哪些真实场景先说最常见的iPhone和Windows电脑之间传照片。用数据线连电脑导出来的全是HEICWindows照片查看器打不开。你可能会说装了扩展插件就行但实际操作中你会发现扩展只解决了缩略图预览和基础查看很多第三方软件调用解码接口时依然会失败。第二个高频场景是社交分享。微信、钉钉这类国产软件后来做了兼容处理会自动转码但不少海外IM和论坛系统对HEIC支持很差发出去了对方下载下来也打不开。还有一些在线的证件照上传、简历附件、电商后台图片上传审核系统直接拒绝HEIC格式。第三个是工作流问题。设计师、自媒体运营经常要把手机照片拖进PS、剪映或者公众号编辑器。这些专业工具对HEIC支持参差不齐Photoshop新版能打开但处理速度明显慢一些轻量级工具干脆不认。在这个场景下先把照片批量转成JPG放进工作目录是最省事的做法。1.3 现有解决方案的痛点要么贵、要么慢、要么不安全市面上的方案我能归类成三派第一派是在线转换网站比如各种“HEIC to JPG online”。优点是零安装缺点是上传下载耗时、有隐私风险。照片是很私密的东西为了一次格式转换把原图传到第三方服务器我个人非常不建议尤其是证件照或者涉及业务资料的照片。第二派是App Store付费应用或者Windows上的商业软件。功能做得全但要花钱而且不少软件界面广告满天飞绑定安装一堆附带软件。我帮同事搜集几个工具时有的安装包下载下来连数字签名都没有杀毒软件直接报毒。第三派是系统自带或插件方案。Windows装HEIC扩展属于治标不治本只能解决查看问题macOS上用预览App手动导出只适用于少量图片几百张图手动操作能点到手酸。自己写工具的意义就在这免费、本地运行、批量处理、可控性强。代码也不复杂核心逻辑就三步——读取HEIC文件、解码成位图、编码为JPG写出加上批处理和文件检查整个脚本也就百来行。2. 技术方案选型为什么我最终选择了这条路2.1 主流转换方案横向对比在动手之前我把常用的几种技术路线都过了一遍方案底层依赖优点缺点Python pillow-heiflibheif跨平台、集成方便、生态成熟需要Python环境ImageMagick命令libheif一条命令完成、适合服务器脚本批量处理需要配合shell语法FFmpeglibheif视频工具链统一、适合批量脚本面向图像处理的参数不够直观原生Swift/Objective-CImageIO苹果系集成度最高、解码效率最好只适合macOS/iOS平台在线服务API服务端无需本地资源有隐私风险、上传下载费时FFmpeg那条路线有人把HEIC伪装成JPG扩展名来处理网上搜“ffmpeg ts伪装jpg”能找到类似思路但那是针对视频流的场景对静态图片转换来说绕了远路不推荐。2.2 libheif为什么是核心所有离线方案中libheif是绕不开的那个底层库。它是一个开源HEIF/HEIC编解码库实现了ISO/IEC 23008-12标准负责把HEIC文件解成YUV数据或者RGB位图。Python生态里的pillow-heif、ImageMagick、FFmpeg本质上都是在调用libheif的解码能力。我这里选择Python pillow-heif的组合理由很实际一是pillow-heif可以直接嵌进Pillow/PIL的Image.open流程里处理方式和打开普通JPG完全一致代码写起来非常自然。注册扩展之后Image.open(photo.heic)就能直接读不需要手动管理底层解码器。二是Pillow的JPG编码器足够成熟从PIL转成JPG输出支持exif信息的保留、质量参数调节、渐进式JPG输出等多种选项灵活性完全够用。三是Python脚本后续维护成本低同事要加个需求也能看懂改得动。我之前用C#写过类似工具功能没问题但改起来麻烦现在这个方案明显更适合长期使用和维护。2.3 方案取舍背后的几个关键考量写工具的时候我给自己定了三条硬性要求第一绝对不修改源文件。转换过程必须只读源HEIC任何情况下都不对原文件进行移动、重命名或写入操作。这样即使转换过程中出现问题源文件都还是完好的。第二转换结果必须可追溯。JPG文件的元数据尽量保留包括拍摄时间、GPS信息、相机型号等。很多人没注意到这一点转出来的图片EXIF信息全丢对摄影师和做归档管理的人来说基本等于报废。第三处理流程要允许中断和续跑。几百张图片批量转换中途可能因为意外终止或者手动停止。重启工具后已经转换过的文件应该被自动跳过而不是从头再来。这就引出了后面要讲的“智能跳过已存在”功能。这三点是工具设计的目标骨架后续所有代码和逻辑都围绕它们展开。3. 工具设计与核心功能实现3.1 整体架构与目录规划工具的运行模式很简单输入一个源目录输出一个目标目录遍历源目录下所有HEIC文件逐个转换后写入目标目录。目录结构设计如下heic_converter/ ├── convert.py # 主脚本 ├── requirements.txt # 依赖清单 ├── input/ # 放待转换的HEIC文件可选 └── output/ # 转换后的JPG输出目录自动创建我把源目录和输出目录设计成动态指定而不是写死在代码里兼容两种使用方式直接命令行传参或者把要处理的文件放进input目录后直接运行。后者的好处是双击运行就能处理对不熟悉命令行的同事比较友好。3.2 批量转换主流程实现核心流程用伪代码描述是扫描源目录找出所有扩展名为.heic或.heif的文件对每个文件检查输出目录是否存在同名.jpg如果已存在根据“跳过已存在”策略决定是否跳过如果有必要调用pillow-heif解码、Pillow编码输出JPG记录处理日志统计成功数和失败数在真正动手写代码之前有一个细节需要处理文件名处理。iPhone导出的HEIC文件命名通常是IMG_0001.HEIC这种格式但也有通过微信、AirDrop传输后变成IMG_0001(1).HEIC或者中文名的情况。代码里面需要把文件名的每个字符都当作普通字符处理不做特殊转义避免因为空格和括号导致路径错误。3.3 智能跳过已存在文件的设计细节“智能跳过已存在”这个功能听起来简单细想其实有几个决策点需要处理。第一层逻辑是目标目录下已经有同名JPG文件时默认跳过一次不重复转换。这个判断只认文件名不关心文件大小因为同一张照片重复转换虽然理论上JPG输出质量参数一致时结果相同但没必要浪费时间重新编码。第二层逻辑是处理异常情况。如果某个HEIC文件解码失败理想状态应该是跳过它记录到失败日志然后继续处理后续文件。不能让一个坏文件卡住整个批次。第三层逻辑是我后来加的大小写处理。Windows和macOS对文件名大小写的敏感度不同HEIC、.heic、.Heic其实都是同一个扩展名。我在匹配时统一转成小写再判断import os def is_heic_file(filename: str) - bool: return filename.lower().endswith((.heic, .heif))这里建议不要用os.path.splitext拿扩展名再判断因为遇到.tar.heic这种带复合后缀的文件时splitext拿到的还是.heic没问题但用endswith更直白、更好理解。3.4 文件完整性保护先写临时文件再原子替换这是整个工具里最值得细说的一部分也是“保护文件完整性”这个标题的关键。直接转换、直接写目标文件的写法有一个隐患如果转换过程中程序崩溃、断电、或者磁盘写入错误目标目录里会躺着一个不完整的半成品JPG文件。等你下次运行工具时它检查到“同名JPG已存在”就跳过了结果留下一个损坏的文件到时候排查起来很难受。解决方案是“临时文件 原子替换”手法先输出到目标目录下带.tmp后缀的临时文件等编码完成后用os.replace进行原子替换把临时文件改名为最终文件名。import os import tempfile def safe_write_jpg(image, target_path: str, quality: int 92): dir_name os.path.dirname(target_path) fd, tmp_path tempfile.mkstemp(suffix.tmp, dirdir_name) try: with os.fdopen(fd, wb) as f: image.save(f, formatJPEG, qualityquality) os.replace(tmp_path, target_path) except Exception: if os.path.exists(tmp_path): os.remove(tmp_path) raiseos.replace是原子操作旧文件被覆盖之前新文件已经完整落盘这个过程对系统来说是瞬间完成的。如果转换失败临时文件会被清理掉目标目录不会产生任何残留。这个手法在数据库和文件系统工具中很常见但很多个人写的小工具都没有考虑这一点我认为这是值得坚持的专业做法。3.5 EXIF信息保留很多人忽略的关键一步JPG之所以在照片管理领域无法被完全替代除了兼容性还在于EXIF信息的标准化程度最高。拍摄时间、光圈、快门、ISO、GPS坐标、镜头信息都在EXIF里。HEIC文件本身也存EXIF但编码方式与JPG的EXIF段不完全一致。Pillow处理的时候需要把EXIF从一个格式翻译到另一个格式。pillow-heif读取HEIC后通过image.getexif()可以拿到EXIF数据保存JPG时传回去exif img.info.get(exif) if exif: jpg_image.save(target_path, JPEG, qualityquality, exifexif)注意一个容易踩的坑img.info.get(exif)返回的是原始字节而img.getexif()返回的是解析后的对象。保存时传给Pillow的应该是原始字节不要搞混。我实测过经过这个流程转换后的JPG在Windows资源管理器里能看到拍摄日期在Lightroom里能显示镜头和GPS信息基本是无损过渡。4. 实操从零搭建你的HEIC转JPG工具4.1 环境准备与依赖安装首先需要Python 3.8以上版本推荐3.10或3.11兼容性更好。然后安装依赖pip install pillow pillow-heifpillow-heif会连带安装libheif的Python绑定。之前在Windows上使用还需要额外安装系统的HEIF解码库但新版本pillow-heif已经内置了必要的二进制省去了手动配置路径的麻烦。如果遇到安装失败可以试试指定版本pip install pillow-heif0.16.0安装完成后建议先跑一个最小验证脚本确认HEIC解码可用再写主程序from PIL import Image import pillow_heif pillow_heif.register_heif_opener() img Image.open(test.heic) print(img.size, img.mode) img.thumbnail((800, 800)) img.save(test.jpg, JPEG, quality85)这段代码能跑通说明环境没问题可以继续往下写完整工具。4.2 完整代码实现下面是整个转换工具的核心代码。我把代码拆分成了几个函数每个函数职责单一方便理解和扩展。import os import sys import argparse import logging from pathlib import Path from PIL import Image import pillow_heif pillow_heif.register_heif_opener() logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, datefmt%Y-%m-%d %H:%M:%S, ) logger logging.getLogger(heic_converter) HEIC_EXTS (.heic, .heif) JPG_QUALITY 92 SKIP_EXISTING True def list_heic_files(src_dir: Path) - list: files [] for p in src_dir.rglob(*): if p.is_file() and p.suffix.lower() in HEIC_EXTS: files.append(p) return sorted(files) def convert_one(src_path: Path, dst_path: Path, quality: int JPG_QUALITY): dst_path.parent.mkdir(parentsTrue, exist_okTrue) tmp_path dst_path.with_suffix(dst_path.suffix .tmp) with Image.open(src_path) as img: exif img.info.get(exif) img.load() if exif: img.save(tmp_path, JPEG, qualityquality, exifexif) else: img.save(tmp_path, JPEG, qualityquality) os.replace(tmp_path, dst_path) def batch_convert(src_dir: Path, dst_dir: Path, skip_existing: bool True): heic_files list_heic_files(src_dir) total len(heic_files) logger.info(找到 %d 个HEIC文件, total) if total 0: return 0, 0 success 0 failed 0 for i, src_path in enumerate(heic_files, 1): relative src_path.relative_to(src_dir) dst_path dst_dir / relative.with_suffix(.jpg) if skip_existing and dst_path.exists(): logger.info([%d/%d] 跳过已存在: %s, i, total, relative) continue try: logger.info([%d/%d] 转换中: %s, i, total, relative) convert_one(src_path, dst_path) success 1 except Exception as e: failed 1 logger.error([%d/%d] 失败: %s | 原因: %s, i, total, relative, e) logger.info(转换完成。成功 %d失败 %d共 %d 个文件。, success, failed, total) return success, failed def main(): parser argparse.ArgumentParser(descriptionHEIC批量转JPG工具) parser.add_argument(src, nargs?, typestr, defaultinput, help源HEIC目录默认: input) parser.add_argument(dst, nargs?, typestr, defaultoutput, help输出JPG目录默认: output) parser.add_argument(--force, actionstore_true, help强制重新转换即使JPG已存在) args parser.parse_args() src_dir Path(args.src) dst_dir Path(args.dst) if not src_dir.is_dir(): logger.error(源目录不存在: %s, src_dir) sys.exit(1) batch_convert(src_dir, dst_dir, skip_existingnot args.force) if __name__ __main__: main()运行方式python convert.py input output # 强制重新转换 python convert.py input output --force4.3 关键参数解析与选择逻辑有几个参数值得多聊几句它们直接影响输出JPG的质量和兼容性。JPG质量参数quality我默认设为92。很多人习惯默认95或100但实测下来92在画质和文件体积之间是最平衡的点。JPG编码是面向感知的92这个档位下人眼几乎看不出和原图的差别但文件体积比95档位小10%到15%。如果追求极致的档案保留可以设到95但体积会涨不少。如果只是准备发到社交平台或者做临时预览85就够了。关于输出色彩模式HEIC解码后Pillow拿到的通常是RGB或RGBA模式。JPG不支持透明通道如果图片带Alpha通道直接保存会报错我们在保存前可以做个处理if img.mode in (RGBA, LA, P): img img.convert(RGB)这个细节在处理iPhone截屏图片时会遇到因为部分截图存储为带透明通道的P形式。还有一个参数是exif前面说过了务必传原始字节而不是getexif()对象。4.4 批量运行与日志观察写完后我把同事的几百张HEIC拿来做实际测试输出目录分门别类日志会一条条打印每个文件的处理状态2025-01-15 14:32:01 [INFO] 找到 328 个HEIC文件 2025-01-15 14:32:01 [INFO] [1/328] 转换中: IMG_0001.HEIC 2025-01-15 14:32:01 [INFO] [2/328] 跳过已存在: IMG_0002.HEIC这里发现了一个有趣的现象328个文件中有相当一部分在第一次转换时就已经处理过了第二次因为有了“跳过已存在”的逻辑瞬间完成。这种跑批工具加了跳过逻辑和没加体验完全两个档次。跑完以后Windows资源管理器里选中所有JPG查看缩略图和EXIF信息都正常双击打开速度比HEIC源文件快很多。同事说“终于不用再装一堆乱七八糟的看图软件了”我觉得这就是工具的价值。5. 常见问题与排查技巧实录5.1 HEIC解码失败与libheif版本问题实际使用中最常见的报错是OSError: cannot identify image file或者KeyError: class PIL.PngImagePlugin.PngImageFile前者通常意味着pillow-heif没有正确注册也就是没有调用pillow_heif.register_heif_opener()。如果注册了还是失败大概率是libheif版本太低。较老的libheif对iOS 17以后拍摄的部分HEIC文件解码不完整会出现解码成功但输出图像花屏或区域绿屏。这种情况升级pillow-heif即可pip install --upgrade pillow-heif我建议至少使用0.15.0以上版本兼容性稳定很多。还有一个冷门坑部分HEIC文件实际上存的是HEIF序列也就是连拍照片或实况照片。这类文件解码后可能拿到多帧图像Pillow默认取主帧但有些编码异常的实况图会导致解码卡住甚至内存溢出。如果遇到可以在Image.open之后加一个超时机制或者跳过特定文件用异常处理兜住即可。5.2 输出照片的缩略图预览异常转换完成后Windows资源管理器缩略图可能仍然显示为空白。我把这类问题整理成一个速查表现象可能原因处理方法JPG缩略图空白未安装HEIC扩展系统没有加载解码器安装微软商店的HEIF Image ExtensionsJPG缩略图空白资源管理器缩略图缓存损坏清理缩略图缓存或重启资源管理器JPG能打开但无缩略图文件保存时未正确写EXIF/缩略图信息用Pillow保存时检查exif参数在微信/钉钉中无法预览JPG文件名特殊字符或超大尺寸检查文件名必要时调低尺寸关于缩略图有一个容易被忽略的点Windows对JPG缩略图的生成依赖EXIF中的缩略图信息。如果EXIF中有缩略图段资源管理器可以不加载原始像素数据而直接显示缩略图。一些在线转换工具生成的JPG没有这个段在Windows上就会出现“能打开但无缩略图”的尴尬。而我用Pillow写出的JPG会包含基于EXIF生成的缩略图信息实测在Win10/Win11上缩略图显示正常。5.3 大量文件转换的性能优化几百张图片转换单线程耗时可能比较长。实测值供参考一张1200万像素HEIC转JPG单线程大约0.3到0.5秒300张图大概2到3分钟。如果觉得慢可以上多线程并发。Python的多线程受GIL限制但如果切换成进程池模式可以绕过GIL实现接近线性的加速。我这里用concurrent.futures.ProcessPoolExecutor改造批量处理部分from concurrent.futures import ProcessPoolExecutor, as_completed def worker(args): src_path, dst_path, quality args try: convert_one(src_path, dst_path, quality) return src_path, True, None except Exception as e: return src_path, False, e def batch_convert_parallel(src_dir, dst_dir, workers4): tasks [] for src_path in list_heic_files(src_dir): dst_path dst_dir / src_path.relative_to(src_dir).with_suffix(.jpg) tasks.append((src_path, dst_path, JPG_QUALITY)) with ProcessPoolExecutor(max_workersworkers) as pool: futures [pool.submit(worker, t) for t in tasks] for future in as_completed(futures): src, ok, err future.result() if ok: logger.info(完成: %s, src.name) else: logger.error(失败: %s | %s, src.name, err)多进程模式下需要注意每个子进程都需要重新注册heif opener所以在worker的最前面也要调用pillow_heif.register_heif_opener()。我当时没注意这个问题子进程直接报错找不到解码器排查了好一会儿。CPU核心数多的机器4到8个worker是理想区间。再往上走磁盘IO和内存带宽会成为瓶颈收益递减。5.4 周边场景微信dat图片、前端展示HEIC等写转换工具的时候我顺便研究了一下和图片格式兼容性相关的几个周边问题这里共享一下排查结论。“微信dat文件转换为jpg”是搜索热度很高的问题。微信PC版会把聊天中的图片缓存在本地为了防篡改把原图做了异或加密并改成.dat扩展名。如果要用工具批量还原微信接收的图片本质上是做异或解密而不是格式转换。和HEIC转JPG的思路不同但外层的“批量处理、智能跳过、完整性检查”的设计架构是通用的。“前端正常的如何展示heic后缀的图片”这个问题也经常有人问。网页端展示HEIC目前没有浏览器原生支持通常做法是前端上传前后端用sharp或者ffmpeg把HEIC转成WebP或JPG再展示。如果确实需要纯前端处理可以引入libheif的WASM编译版本但体积比较大体验一般。我的建议是后端统一转码不要在浏览器端给用户添麻烦。还有人在ArcGIS里问“怎么把tif影像导出为jpg”本质上也是格式转换但类型不同tif是栅格影像格式转换时还要考虑位深、坐标系、透明信息等。这和HEIC转JPG是两回事但都印证了一个趋势——JPG作为通用交换格式的地位短期内很难被动摇。5.5 自己的几个避坑建议工具写完之后我总结了几条个人觉得值得提醒自己的事项一是不要相信“输出目录已经存在同名文件就等于转换成功”。文件存在不代表内容完整所以保留“强制重新转换”的按钮--force参数非常有必要。遇到可疑文件时强制重跑一遍就能解决。二是转换结果要抽检。尤其是第一次在新环境运行随机挑几张图片对比原图和JPG的尺寸、位深、EXIF信息确认无误再批量处理大目录。三是定期备份源文件。虽然工具设计了不修改源文件的逻辑但批量操作过程中难免出现误操作比如源目录和目标目录配置反了。每次运行前做好目录检查运行后快速看一下生成文件的修改时间能省掉很多麻烦。我个人在实际操作中的体会是写这样一个小工具技术门槛并不高真正有价值的是把边界情况处理得足够细致。批量任务跑起来的稳定性、异常文件的容忍度、输出结果的完整性这些才是区分“能用”和“好用”的关键。如果你也在被HEIC格式兼容性困扰完全可以参考这个方案自己动手写一个。几百行代码换来的是以后每次处理苹果照片都不用再求助于付费工具和在线网站。再进一步如果想把这个工具分享给家人用还可以用PyInstaller打包成exe双击就能运行那就更加一劳永逸了。本文还有配套的精品资源点击获取
返回列表