How to Read EXIF Capture Time from HEIC/HEIF Photos in Ruby on Rails
Back to Blog

How to Read EXIF Capture Time from HEIC/HEIF Photos in Ruby on Rails

3 min read
Ruby on Rails

If you've ever uploaded an iPhone photo and noticed your captured_at field missing, you're not alone. HEIC/HEIF files store metadata differently than JPEGs, and most Ruby EXIF libraries can't read them. Here's how to fix that.

Quick Summary

  • Use ExifTool (through the mini_exiftool or exiftool gem), it works with HEIC/HEIF files directly
  • You don't need to decode or convert images (libheif or libvips) just to read metadata
  • Always convert timestamps to UTC and check multiple EXIF date fields for reliability
  • When using Active Storage, call blob.open to access a temporary file path for ExifTool

Why HEIC/HEIF can be tricky

Here's the issue in plain terms:

  • HEIC stores metadata inside QuickTime-style boxes, not the standard EXIF segments used by JPEG
  • Most Ruby EXIF libraries (like exifr) only understand JPEG/TIFF files
  • ExifTool is a Perl-based tool that can read just about anything, including HEIC/HEIF, RAW, and Live Photo sidecars

Setup

Install the ExifTool CLI on your machine/container:

macOS (Homebrew):

text
brew install exiftool

Debian/Ubuntu:

text
sudo apt-get install -y libimage-exiftool-perl

Alpine:

text
apk add exiftool

Then add one of these gems to your Gemfile:

text
gem 'exiftool' # lightweight wrapper; also shells out to exiftool

And install them:

text
bundle install

Reading EXIF data from a file

Here's a simple Ruby method using the exiftool gem:

text
require 'exiftool'

def extract_capture_time(path)
  data = Exiftool.new(path).to_hash # symbolized keys
  raw = data[:date_time_original] || 
        data[:create_date] || 
        data[:media_create_date] || 
        data[:modify_date]
  
  return nil unless raw
  
  case raw
  when Time then raw.utc
  when DateTime then raw.to_time.utc
  else (Time.parse(raw.to_s) rescue nil)&.utc
  end
end

That's it! ExifTool takes care of all the format quirks, so this works for HEIC, JPEG, and even Live Photos.

Reading EXIF with Active Storage

When your image lives in Active Storage (e.g., S3 or Disk), just open it temporarily before reading EXIF:

text
def extract_capture_time_from_blob(blob)
  blob.open do |file|
    extract_capture_time(file.path)
  end
end

This downloads the file into a Tempfile, passes its path to ExifTool, and cleans up afterward—no extra libraries required.

Normalizing captured_at in your models

When you save your capture time, normalize it to UTC and support multiple sources:

text
def normalized_captured_at(file_path: nil, client_value: nil)
  exif_time = file_path && extract_capture_time(file_path)
  
  client_time = begin
    case client_value
    when Time then client_value
    when DateTime then client_value.to_time
    when String then Time.iso8601(client_value) rescue Time.parse(client_value)
    end
  rescue ArgumentError
    nil
  end
  
  (exif_time || client_time)&.utc
end

This way, even if the photo doesn't have EXIF data, you can still fall back to a timestamp sent from the frontend.

Troubleshooting

No DateTimeOriginal: Some devices only set CreateDate or MediaCreateDate. Always try fallbacks.

Nil/empty type from browser: Client uploads may send application/octet-stream. Fix on the frontend by inferring a proper MIME type from the filename before direct upload.

It worked for JPEG but not HEIC: Ensure you're using ExifTool, not EXIFR, for HEIC files.

References

If you'd like to make this reusable, you can wrap the logic into a small service class, ImageMetadataExtractor that returns either a UTC Time or nil. Once you have this in place, your app can reliably extract captured_at from any image, regardless of format. 📸

Read the full article with images on dev.to

Originally published on dev.to

Enjoyed this article? Share it with others!