Dev.to WebDev πŸ›  Dev πŸ‘ 0 πŸ“– 4 min read

Converting HEIC, WebP and AVIF to JPG without losing EXIF: where the metadata lives

Most "convert this image to JPG" code looks like decode, then encode, and it works until someone notices that every converted photo now says it was taken today, has no location, and the portrait shots are lying on their

Most "convert this image to JPG" code looks like decode, then encode, and it works until someone notices that every converted photo now says it was taken today, has no location, and the portrait shots are lying on their side.

The reason is simple: image encoders encode pixels. SkiaSharp, System.Drawing, most Python one-liners: you hand them a bitmap, you get a JPG back, and the EXIF block, the XMP packet and the file's own timestamps are gone. If you want them in the output, you have to dig them out of the source container yourself and write them back.

I had to do exactly this for PlainJPG, a small Windows converter for WebP, HEIC and AVIF. Here's where the metadata actually sits in each format, and the three gotchas that cost me the most time.

WebP: RIFF chunks

A WebP file is a RIFF container: RIFF, a little-endian size, WEBP, then a flat list of chunks, each a FourCC plus a 4-byte little-endian length (padded to an even size).

  • EXIF holds a raw TIFF structure (starting II*\0 or MM\0*).
  • XMP (note the trailing space) holds the XMP packet.
  • VP8X is the extended header. Bit 0x10 of its first byte says the image has alpha, bit 0x02 says it's animated.

Walking the chunk list takes about twenty lines. The alpha flag is useful too: it's how you decide whether a WebP should become a PNG instead of a JPG.

HEIC and AVIF: items inside the meta box

HEIC and AVIF are both HEIF, which is ISO base media (the MP4 box format). Metadata isn't a chunk you stumble over; it's an item:

  1. Find the top-level meta box (a FullBox, so skip 4 bytes of version and flags).
  2. In iinf, list the items. EXIF is an item of type Exif. XMP is an item of type mime whose content type is application/rdf+xml.
  3. In iloc, look up where each item's bytes are: one or more extents, either as absolute file offsets (construction method 0) or relative to the idat box (method 1).
  4. Concatenate the extents.

Gotcha 1: the HEIF Exif item isn't bare TIFF. It starts with a 4-byte big-endian offset to the TIFF header, and some writers put Exif\0\0 in front of the TIFF as well. Skip the offset, skip the optional prefix, and only then do you have something you can drop into a JPEG.

Writing it back

JPEG. EXIF goes in an APP1 segment (FF E1, a 2-byte big-endian length, Exif\0\0, then the TIFF bytes). XMP goes in a second APP1 whose payload starts with http://ns.adobe.com/xap/1.0/\0. Insert them right after SOI, or after the JFIF APP0 if your encoder wrote one.

Gotcha 2: APP1 has a 64 KB ceiling. The length field is 16 bits and counts itself, so the payload can't exceed 65,533 bytes. Some phones embed large maker notes or thumbnails; if the block doesn't fit, you need a plan (skip it, or strip the thumbnail) rather than writing a corrupt file.

PNG. EXIF has its own chunk, eXIf, containing the TIFF bytes. XMP goes in an iTXt chunk with the keyword XML:com.adobe.xmp. Put both right after IHDR and compute each chunk's CRC over the type and data.

Gotcha 3: orientation, twice

Phones rarely rotate pixels. They store the sensor's orientation and set EXIF Orientation (1 to 8) so viewers rotate on display.

If your converter bakes the rotation into the pixels (sensible, because plenty of software ignores the tag), you must then set Orientation to 1 in the EXIF you copy across. Otherwise a viewer that honours the tag rotates the already-upright image again.

HEIF adds a twist: rotation and mirroring are container properties (irot and imir), and a decoder that follows the spec applies them, so the pixels it hands you are already display-oriented. iPhone HEICs usually also carry an EXIF Orientation value. Apply that one as well and every portrait photo ends up sideways. For HEIF, trust irot/imir and treat the EXIF tag as informational.

Don't forget the file's own dates

EXIF DateTimeOriginal is what photo apps sort by, but File Explorer sorts by the file system's created and modified times, and a new file gets "now". After writing the output, copy them over:

File.SetLastWriteTimeUtc(target, File.GetLastWriteTimeUtc(source));
File.SetCreationTimeUtc(target, File.GetCreationTimeUtc(source));

A quick check

After converting, open the JPG's Properties, then Details in Windows. If "Date taken" is filled in and portrait photos stand upright in both Photos and a browser, the EXIF made it across and the orientation was handled once, not twice.

If you'd rather not write any of this, PlainJPG does it for whole folders of WebP, HEIC and AVIF on Windows, offline. There's also a plain-English guide to batch converting HEIC to JPG on Windows for the non-developers in your life who keep sending you .heic files.

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.