在 Laravel 中验证并转换 HEIC 图像
过去几年售出的每一部 iPhone 默认都使用 HEIC 格式拍摄照片。在画质相同的情况下,HEIC 文件的体积大约只有 JPEG 文件的一半——这也是苹果改用该格式的原因——但 Chrome 和 Firefox 浏览器并不支持渲染这种格式。任何支持从手机上传照片的应用都必须处理这一问题,而在此之前,Laravel 的图像组件并未提供相关支持:HEIC 文件在到达驱动程序之前就会被拒收。
Laravel 13.24 现已支持接收 HEIC、HEIF 和 AVIF 格式的输入,新增了用于输出的 toHeic() 方法,并让图像验证规则能够识别这三种格式。本文将介绍如何接收手机拍摄的照片,并将其转换为浏览器支持的格式进行展示。
服务器所需配置
PHP 本身并不具备 HEIC 解码能力。该功能依赖于 ImageMagick 的 HEIF 委托(delegate),而该委托又是基于 libheif 构建的;因此,你需要安装已编译包含该委托的 Imagick 扩展。无论使用哪个版本的 PHP,GD 驱动程序都无法读取 HEIC 格式。
许多发行版软件包提供的 ImageMagick 并不包含 HEIF 委托,因此请务必进行检查,切勿想当然:
php -r "print_r(Imagick::queryFormats('HEI*'));"如果数组为空,说明扩展已安装但无法访问这些文件。在 Debian 和 Ubuntu 上,该代理组件包含在 libheif1 中;而在 macOS 上,Homebrew 的 ImageMagick 方案(formula)已将其包含在内。AVIF 格式的兼容性较好,因为如果 PHP 在构建时链接了 libavif,GD 库也能对其进行解码。
如果尚未安装 Intervention Image,请务必安装,因为它为这两种驱动程序提供支持:
composer require intervention/image:^4.0验证上传内容
图像规则会将文件与一份固定的图像类型列表进行比对,该列表现已包含 heic、heif 和 avif:
$request->validate([
'photo' => ['required', 'image', 'max:12288'],
]);无需进行任何配置。现在,直接从“相机胶卷”上传 iPhone 拍摄的照片即可通过验证;而在之前,此类操作会因“照片字段必须是图像”的错误提示而失败。
如果您希望明确指定允许的文件类型,mimes 规则也支持同样的扩展名:
'photo' => ['required', 'mimes:jpg,png,webp,heic', 'max:12288'],请注意,根据客户端的不同,上传的 HEIC 文件可能带有多种 MIME 类型,其中 image/heic 和 image/heif 是最常见的两种。image 和 mimes 规则会根据文件内容来识别类型,而不是盲目信任浏览器提供的信息,因此这两种类型的文件最终都会被归入同一位置。
上传时进行转换
接收文件仅仅是整个流程的一半。如果直接按原样存储 HEIC 文件,大多数访客将无法正常查看图片(会显示为损坏的图像)。解决方法是在处理上传请求时进行格式转换,利用图像 API 只需几行代码即可实现:
use App\Models\Photo;
use Illuminate\Http\Request;
public function store(Request $request)
{
$request->validate([
'photo' => ['required', 'image', 'max:12288'],
]);
$path = $request->image('photo')
->usingImagick()
->orient()
->scale(width: 2000)
->toWebp()
->quality(80)
->store('photos');
return Photo::create(['path' => $path]);
}有两点值得注意。使用 usingImagick() 是因为默认驱动程序是 GD,而 GD 无法处理 HEIC 格式的输入。此外,orient() 方法会读取方向元数据并据此进行旋转;这一点对于手机拍摄的照片尤为重要,因为竖屏照片通常是以横屏画幅存储并附带旋转标记的。
保存的文件会自动获得正确的扩展名。store() 方法会根据输出格式生成一个基于哈希值的文件名,因此上传的 HEIC 文件在转换为 WebP 后,会保存为 photos/{hash}.webp。
提供 AVIF 格式并设置 WebP 备用方案
如果你希望在支持 AVIF 的环境下使用这种体积更小的格式,可以基于同一个源文件生成这两种版本。由于每次转换操作都会返回一个新的实例,因此进行分支处理时,两者之间不会出现状态污染(即不会相互影响):
$source = $request->image('photo')->usingImagick()->orient()->scale(width: 2000);
$avif = $source->toAvif()->quality(70)->storeAs('photos', "{$id}.avif");
$webp = $source->toWebp()->quality(80)->storeAs('photos', "{$id}.webp");然后让浏览器来选择:
<picture>
<source srcset="{{ Storage::url("photos/{$photo->id}.avif") }}" type="image/avif">
<img src="{{ Storage::url("photos/{$photo->id}.webp") }}" alt="{{ $photo->caption }}">
</picture>在视觉质量相当的情况下,AVIF 的文件体积通常比 WebP 小 20% 到 30%,但代价是编码速度较慢。如果采用同步上传方式,编码耗时会直接体现在请求响应中;因此,当需要生成多种尺寸的图片时,将其作为队列任务处理会是一个不错的选择。
生成 HEIC
也可以通过 toHeic() 或 optimize('heic') 输出 HEIC 格式:
Image::fromPath(storage_path('app/photo.jpg'))
->usingImagick()
->toHeic()
->quality(80)
->store('photos');这是一个比单纯读取 HEIC 更具体的应用场景,但也确实存在:例如创建将在 Apple 设备上打开的归档文件或导出文件,或者在处理流程中全程保留原始格式。
heif 别名在各处都会被统一规范化为 HEIC,因此 optimize('heif') 的输出结果与 optimize('heic') 相同;文件将以标准的 .heic 扩展名存储,且 mimeType() 会返回 image/heic。此外,Image::extension() 也能识别某些客户端发送的 image/x-heic 和 image/x-avif MIME 别名,并将其映射为 heic 和 avif,而不是将其视为无法识别的格式。
当格式不受支持时
如果文件以组件无法处理的格式传递给驱动程序,系统将抛出 ImageException 异常,并在异常信息中指明该不受支持的格式类型:
The image format [image/tiff] is not supported.首先利用图像规则进行验证,几乎可以捕获所有此类问题;但如果 HEIC 文件被送入一个未配置 HEIF 委托(delegate)的 Imagick 构建版本中,也会出现同样的异常情况。这属于部署层面的问题而非用户端问题,因此在发布流程中就应检查服务器上的委托配置,而不是等到收到错误报告时才发现问题。
对于版本低于 13.24 的情况,或者需要在 Laravel 框架之外进行转换时,可以通过 PHP 代码将 HEIC 转换为 JPEG,从而实现手动转换。