DEV Community

Cover image for Converting HEIC/HEIF Image in Node.js with Sharp Library
Up9t
Up9t

Posted on • Edited on

Converting HEIC/HEIF Image in Node.js with Sharp Library

Goal: At the end of this article, you should be able to convert heic/heif image extension to jpeg/webp/png in your Nodejs project without using any external cloud service. I'll provide a practical github repository for this, see the end.

I want to share my experience using the Sharp library from the Node Package Manager (NPM) to convert images with heic extension. First, let's talk about the problems.

Background

There are many problems. First is, Why did I have to mind the .heic extension?

The problem comes from the iPhone users, they produce this .heic extension as the output of the image from their camera. This behaviour is different from an Android phone, which usually outputs .jpeg or .jpg.

This makes thing more complicated on the backend side because browsers don't usually support displaying .heic image out-of-the-box. As a backend engineer, I'm the one who takes the responsibility for converting the images that aren't supported by the browser to display, to one that is widely compatible. In addition to that, I have to resize the image so it can load faster when displayed on the browser. Most of my customers are using iPhones, that's why I really have to consider this as an issue.

I had to either convert it to .jpeg or .webp. Webp is a good choice because it has a smaller file size than jpeg.

The other problem is, Sharp library doesn't come out-of-the-box with support for converting the .heic extension to something else. So, what should I do in this scenario?

Solution

On the sharp's documentation, I was excited to find out that it may has the capability to process images with the heic extension. It is possible because Sharp actually uses a library called libvips as the underlying backend for processing the image.

Libvips has heic support, but why doesn't my Sharp library support it?

That's because, When we install the sharp library with npm install sharp, it'll also download the libvips prebuilt binary inside the node_modules directory. Sharp will use that binary, but it has limited support for the variety of image extensions. What we can do to make it to have a wider support is to do a custom, manual installation of the libvips. I'm talking about building libvips from source.

Let's do it!

Essentially, you only need 2 steps:

  1. Install node-gyp and node-addon-api in your existing Node.js project.
  2. Build libvips from source and configure sharp to use our custom built.

1. Install node-gyp and node-addon in your existing node project.

This step is pretty straightforward.

npm add node-gyp node-addon-api
Enter fullscreen mode Exit fullscreen mode

That's it, first step is done. Now let's go to the second step.

2. Build libvips from source and configure sharp to use our custom built.

Here's how we're going to do it:

  1. Install essential build tools.
  2. Install ninja & meson (build from source).
  3. Install libvips and its dependencies.
  4. Test the libvips installation.
  5. Reconfigure sharp to use our custom built libvips.

Phew, that's a lot of steps. Let's go through each of them one by one.

I assume you were using Debian-based linux.

I highly recommend you to use a Docker container with a Debian image. I will provide a fullly working Dockerfile below.

  1. Install essential build tools

    Here's where we're going to install git, Python, and other essential build tools.

    # Install dependencies
    sudo apt-get update && \
    sudo apt-get install -y curl python3 git build-essential pkg-config
    
  2. Install ninja & meson (build from source).

    Install ninja:

    # Install ninja
    git clone --branch=v1.13.2 --depth=1 https://github.com/ninja-build/ninja.git && \
    cd ninja && \
    ./configure.py --bootstrap && \
    chmod +x ninja && \
    sudo mv ninja /usr/local/bin/ninja
    

    Now, install meson:

    # Install meson
    git clone --branch=1.10.2 --depth=1 https://github.com/mesonbuild/meson.git && \
    cd meson && \
    ./packaging/create_zipapp.py --outfile meson.pyz --interpreter '/usr/bin/env python3' && \
    chmod +x meson.pyz && \
    sudo mv meson.pyz /usr/local/bin/meson
    
  3. Install libvips and its dependencies.

    Let's install its dependencies first.

    # install build dependencies
    sudo apt-get update && \
    sudo apt-get install -y --no-install-recommends \
    python3 build-essential pkg-config libglib2.0-dev libexpat1-dev libheif-dev \
    liblcms2-dev libjpeg-dev libpng-dev libwebp-dev libexif-dev libde265-dev libx265-dev
    

    Now, install and build libvips.

    This may take a while

    # Build libvips
    git clone --branch=v8.18.2 --depth=1 https://github.com/libvips/libvips.git && \
    cd libvips && \
    meson setup build --prefix /usr/local -Dmagick=disabled && \
    cd build && \
    meson compile && \
    meson test && \
    meson install && \
    sudo ldconfig && \
    cd .. && \
    rm -rf libvips
    
  4. Test the libvips installation.

    At this stage, you will have libvips installed on your machine, you can test if it was installed properly.

    vips --version
    # vips-8.18.2
    

    Check if our vips installation support heif/heic.

    vips -l | grep heif
    # You'll see output something like this:
    # VipsForeignLoadHeif (heifload_base), load a HEIF image, priority=0
    # VipsForeignLoadHeifFile (heifload), load a HEIF image (.heic, .heif, .avif), priority=0, is_a, get_flags, header, load
    
  5. Reconfigure sharp to use our custom built libvips.

    npm explore sharp -- npm run build
    

At this point, you should be able to convert the heic/heif file to webp, jpeg, png and vice versa.

Code Example

// server.ts

import { readFile } from "node:fs/promises";
import express from "express";
import multer from "multer";
import sharp from "sharp";

const fileStore = multer({
  // ...multer options here with disk storage
});

const app = express();

app.post("/convert", fileStore.single("image"), async (req, res) => {
  const filePath = req.file?.path;
  const formatToMimeType = {
    webp: "image/webp",
    jpeg: "image/jpeg",
    jpg: "image/jpeg",
    png: "image/png",
  } satisfies Partial<Record<keyof sharp.FormatEnum, string>>;

  const FALLBACK_FORMAT = "webp";
  const format =
    (req.body?.format as keyof typeof formatToMimeType) || FALLBACK_FORMAT;

  const content = await readFile(filePath).catch(() => null);

  if (!content) {
    res.status(400).send("failed to read content");
    return;
  }

  // call sharp as usual.
  // resize the image to 640px height.
  // set either the height or the width, sharp will maintain the aspect ratio.
  const transformer = sharp(content)
    .resize({
      height: 640,
    })
    .toFormat(format);

  res.setHeader("content-type", formatToMimeType[format]);

  transformer.on("error", (err) => {
    console.error("failed to transform image", err);
  });

  transformer.pipe(res);

  // clean up

  return;
});

app.listen(8080);
Enter fullscreen mode Exit fullscreen mode

The minimum frontend example:

<!-- index.html -->

<form action="http://localhost:8080/convert" method="post" enctype="multipart/form-data">
    <input type="file" name="image">
    <select name="format">
        <option value="jpeg">JPEG</option>
        <option value="png">PNG</option>
        <option value="webp">WEBP</option>
    </select>
    <input type="submit" value="submit">
</form>
Enter fullscreen mode Exit fullscreen mode

Try to check this repository I made, if you find any problem: https://github.com/up9t/image-conversion-node-demo

Dockerfile

Here's the full Dockerfile for building and running your nodejs app with sharp.

Make sure you have already include node-gyp, node-addon-api, and sharp packages in your package.json file.

Here's the Dockerfile looks like:

ARG NINJA_VERSION=v1.13.2
ARG MESON_VERSION=1.10.2
ARG LIBVIPS_VERSION=v8.18.2

FROM node:25-trixie AS base
FROM node:25-trixie-slim AS base-slim
FROM debian:trixie AS debian-base

# Install meson and ninja
FROM debian-base AS install-tools
ARG NINJA_VERSION
ARG MESON_VERSION
WORKDIR /build
# Install dependencies
RUN apt-get update && \
    apt-get install -y curl python3 git build-essential pkg-config
# Install ninja
RUN git clone --branch=${NINJA_VERSION} --depth=1 https://github.com/ninja-build/ninja.git && \
    cd ninja && \
    ./configure.py --bootstrap && \
    chmod +x ninja && \
    mv ninja /usr/local/bin/ninja
# Install meson
RUN git clone --branch=${MESON_VERSION} --depth=1 https://github.com/mesonbuild/meson.git && \
    cd meson && \
    ./packaging/create_zipapp.py --outfile meson.pyz --interpreter '/usr/bin/env python3' && \
    chmod +x meson.pyz && \
    mv meson.pyz /usr/local/bin/meson


# Download libvips
FROM base AS download-vips
ARG LIBVIPS_VERSION
WORKDIR /downloads
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    curl git
RUN git clone --branch=${LIBVIPS_VERSION} --depth=1 https://github.com/libvips/libvips.git


# Build libvips
FROM base AS build-vips 
WORKDIR /libvips
# Install build dependencies
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    python3 build-essential pkg-config libglib2.0-dev libexpat1-dev libheif-dev \
    liblcms2-dev libjpeg-dev libpng-dev libwebp-dev libexif-dev libde265-dev libx265-dev
# Copy build tools from the install-tools stage
COPY --from=install-tools /usr/local/bin/ninja /usr/local/bin/ninja
COPY --from=install-tools /usr/local/bin/meson /usr/local/bin/meson
COPY --from=download-vips /downloads/libvips .
# Build libvips
RUN meson setup build --prefix /usr/local -Dmagick=disabled && \
    cd build && \
    meson compile && \
    meson test && \
    DESTDIR=/opt/vips meson install


# Install node modules and build
FROM base AS builder
WORKDIR /app
COPY --from=build-vips /opt/vips/usr/local /usr/local
RUN ldconfig
COPY ./package.json ./package-lock.json .
RUN npm ci
# Rebuild sharp if necessary, the doc says it automatically detects the vips global installation
RUN npm explore sharp -- npm run build
COPY . .
# Remove/comment the line below if you don't need npm run build for your project
RUN npm run build


# Run
FROM base-slim AS runner
ARG USERNAME=node
WORKDIR /app
# Install runtime dependencies
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    libglib2.0-0 libexpat1 libheif1 liblcms2-2 libjpeg62-turbo \
    libpng16-16 libwebp7 libexif12 libde265-0 libx265-215 \
    libfftw3-double3 libwebpmux3 libwebpdemux2 libpangocairo-1.0-0 libpango-1.0-0 \
    libcairo2 libpangoft2-1.0-0 libfontconfig1 libtiff6 librsvg2-2 libopenexr-3-1-30 libopenjp2-7 && \
    rm -rf /var/lib/apt/lists/*
COPY --from=build-vips /opt/vips/usr/local /usr/local
RUN ldconfig
RUN chown ${USERNAME}:${USERNAME} /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/node_modules/sharp ./node_modules/sharp
COPY --from=builder /app/dist ./dist
USER ${USERNAME}
ENTRYPOINT ["npm"]
CMD ["start"]
Enter fullscreen mode Exit fullscreen mode

Conclusion

The default sharpjs package doesn't include vips with heic support, it's up to the developer to build vips from source with wider support.

The above Dockerfile is a good starting point to build a Node.js app with Sharp and Vips support for heic images.

Checkout my github repository if you want to look more into the code in practical: https://github.com/up9t/image-conversion-node-demo

Top comments (4)

Collapse
 
__b64c09a2157ff profile image
amir

hi ,
when i try installing manually, everything is fine. but when i use the dockerfile i get this error:
npm error /usr/local/include/vips/vips8:41:10: fatal error: glib-object.h: No such file or directory
can you tell me what is the issue.

Collapse
 
__b64c09a2157ff profile image
amir

after 3 days of searching and testing, i could fix it. all i should do was just changing the version of libvips and use 8.18.0 ,and thas was it. it worked!

Collapse
 
up9t profile image
Up9t • Edited

Congrats! sorry for not answering, I just open up this website again after a long time and just saw your comment. Thanks for giving the solutions too, hope it would help other people in need. I probably need to update this post.

Collapse
 
leocie profile image
Leon

Hi, have you found a way to get dependency scanners to pick this dependency up? As you're building from source, a scanner which works from pattern matching packages wouldn't see this.