Image by Ryland Dean / Unsplash

Issues Encountered When Converting HEIC/HEIF Images Using Sharp

✍🏼 Written on Jan 5, 2023    💡 Updated on Jan 9, 2023
❗️ Note: it has been days since this article was written, please be aware of its timeliness
🖥  Note:Documenting Issues and Solutions Encountered When Compressing HEIC Format Images with Sharp
📚  Also published on Craft: https://www.craft.do/s/mHmLa16eR5YWPi

After iOS 11, Apple’s camera defaulted to the HEIC format, which reportedly significantly reduces photo size without compromising quality. You can check this in 设置-相机-格式, where “High Efficiency” refers to HEIC format and “Most Compatible” refers to the original JPEG format. For more details on HEIC, see here.

The issue this raises is that no web browsers currently support displaying HEIC images, making conversion a necessity.

I often drag or copy-paste photos directly from Apple’s Photos app into Craft. However, uploading such images to a personal blog won’t display them unless converted to a universal format like PNG or JPEG. Here, I chose to use the Sharp library for conversion, opting for WebP (since HEIC-to-PNG results in excessively large files).

GitHub - lovell/sharp: High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, AVIF and TIFF images. Uses the libvips library. High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, AVIF and TIFF images. Uses the libvips library. - GitHub - lovell/sharp: High performance Node.js image proc... https://github.com/lovell/sharp

Converting common formats like JPEG to PNG is straightforward, but HEIC poses challenges because Sharp relies on a platform-native tool called libvips:

GitHub - libvips/libvips: A fast image processing library with low memory needs. A fast image processing library with low memory needs. - GitHub - libvips/libvips: A fast image processing library with low memory needs. https://github.com/libvips/libvips

Without this tool globally installed locally, processing HEIC images will trigger the following error:

1
2
3
4
5
(node:11469) UnhandledPromiseRejectionWarning: Error: source: bad seek to 807962
heif: Unsupported feature: Unsupported codec (4.3000)

(Use `node --trace-warnings ...` to show where the warning was created)
(node:11469) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)

It states that HEIC files cannot be processed.

The first step in troubleshooting is checking the official repository’s issues, where I found similar problems:

heif: unsupported feature · Issue #2924 · lovell/sharp Are you using the latest version? Is the version currently in use as reported by npm ls sharp the same as the latest version as reported by npm view sharp dist-tags.latest? 2.9.1 What are the steps... https://github.com/lovell/sharp/issues/2924

Following the advice, I installed libvips using homebrew:

libvips "A fast image processing library with low memory needs." https://www.libvips.org/install.html

This process takes a while, with logs appearing continuously. Wait patiently for completion without errors.

Confidently reinstalling Sharp npm install sharp, I hoped to see this message:

1
sharp: Detected globally-installed libvips v8.13.3

This indicates Sharp’s installation script detected the locally installed libvips. However, upon reinstalling Sharp, it still used cached files:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> sharp@0.31.3 install /Users/x/Code/test2/node_modules/sharp
> (node install/libvips && node install/dll-copy && prebuild-install) || (node install/can-compile && node-gyp rebuild && node install/dll-copy)

sharp: Using cached /Users/x/.npm/_libvips/libvips-8.13.3-darwin-x64.tar.br
sharp: Integrity check passed for darwin-x64
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN test2@1.0.0 No description
npm WARN test2@1.0.0 No repository field.

added 45 packages from 206 contributors and audited 45 packages in 4.5s

10 packages are looking for funding
run `npm fund` for details

found 0 vulnerabilities

After deleting the cached directory rm -rf /Users/x/.npm/_libvips/ mentioned in the logs and retrying, the system version of libvips still wasn’t utilized:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> sharp@0.31.3 install /Users/x/Code/test2/node_modules/sharp
> (node install/libvips && node install/dll-copy && prebuild-install) || (node install/can-compile && node-gyp rebuild && node install/dll-copy)

sharp: Downloading https://github.com/lovell/sharp-libvips/releases/download/v8.13.3/libvips-8.13.3-darwin-x64.tar.br
sharp: Integrity check passed for darwin-x64
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN test2@1.0.0 No description
npm WARN test2@1.0.0 No repository field.

added 45 packages from 206 contributors and audited 45 packages in 10.727s

10 packages are looking for funding
run `npm fund` for details

found 0 vulnerabilities

I decided to debug the package.

Starting with the logs, I searched for version-checking keywords like Using cached in the node_modules/sharp package, finding relevant code in sharp/install/libvips.js:

1
libvips.log(`Using cached ${tarPathCache}`);

Further investigation revealed a function determining whether to use the system’s built-in libvips or its own pre-compiled binary (which lacks HEIC support):

1
const useGlobalLibvips = libvips.useGlobalLibvips();

This was the crux. Debugging showed that the check for isRosetta returned true. If it returned false, the system’s libvips would be used instead:

Image

A peculiar observation during debugging: process.arch output x64, while logging this code showed sysctl.proc_translated: 1:

Image

Yet, executing the same code directly in the console output sysctl.proc_translated: 0:

Image

Here I concluded that it must be a Node version issue causing it to use its own compiled libvips. So I used nvm to switch the default Node version to the arm64 version, then ran npm i sharp again, and the problem was resolved.

Below is the code that might be used in this process:

1
2
3
4
5
6
7
8
9
10
// 列出 nvm 版本
nvm list
// 输出当前电脑的架构
arch // 如果输出的是 arm64 表示就是 M1 芯片
// 使用最新的稳定版
nvm install stable // 我的机器给我安装了 19.3.0 的 arm64 版本的 node
// 将该 node 作为默认
nvm alias default v19.3.0
// 在当前终端使用 19.3.0 版本的 node
nvm use 19.3.0

Later, I submitted a PR to the author to modify the isRosetta logic:

Get real architecture of M1 Mac regardless of Rosetta by Xheldon · Pull Request #3514 · lovell/sharp Get real architecture of M1 Mac regardless of Rosetta or node version, see #3239 https://github.com/lovell/sharp/pull/3514

However, the author explained that there was a reason for the function’s current implementation. So I suggested that they add a warning during Sharp installation to inform users why the globally installed libvips package wasn’t being used. Eventually, the author accepted my suggestion:

Install: log Rosetta detection, improve related docs · lovell/sharp@5be36c2 "High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, AVIF and TIFF images. Uses the libvips library. - Install: log Rosetta detection, improve related docs · lovell/sharp@5be36c2" https://github.com/lovell/sharp/commit/5be36c2deb735577fc76fa52242836d40df276bd

- EOF -
Originally published at: Issues Encountered When Converting HEIC/HEIF Images Using Sharp - Xheldon Blog