Skip to main content

django-exiffield

PyPI GitHub Workflow Status (master) Coveralls github branch PyPI - Python Version PyPI - License

django-exiffield extracts exif information by utilizing the exiftool.

Requirements

  • Python 3.10 or newer
  • exiftool
  • Django >= 5.2 (5.2 LTS, 6.0 and 6.1 are tested)

Installation

  1. Install django-exiffield

    pip install django-exiffield
    
  2. Make sure exiftool is executable from you environment.

Integration

Let's assume we have an image Model with a single ImageField. To extract exif information for an attached image, add an ExifField, specify the name of the ImageField in the source argument

from django.db import models

from exiffield.fields import ExifField


class Image(models.Model):
    image = models.ImageField()
    exif = ExifField(
        source='image',
    )

and create a migration for the new field. That's it.

After attaching an image to your ImageField, the exif information is stored as a dict on the ExifField. Each exif information of the dictionary consists of two keys:

  • desc: A human readable description
  • val: The value for the entry.

In the following example we access the camera model

image = Image.objects.get(...)
print(image.exif['Model'])
# {
#     'desc': 'Camera Model Name',
#     'val': 'DMC-GX7',
# }

As the exif information is encoded in a simple dict you can iterate and access the values with all familiar dictionary methods.

Denormalizing Fields

The ExifField stores a generic exif document, so JSON lookups such as exif__Model__val work, but they are not indexable like dedicated columns. The ExifField provides a convenient way to denormalize certain values using the denormalized_fields argument. It takes a dictionary with the target field as key and a simple getter function of type Callable[[Dict[Dict[str, str]]], Any]. To denormalize a simple value you can use the provided exiffield.getters.exifgetter

from django.db import models

from exiffield.fields import ExifField
from exiffield.getters import exifgetter


class Image(models.Model):
    image = models.ImageField()
    camera = models.CharField(
        editable=False,
        max_length=100,
    )
    exif = ExifField(
        source='image',
        denormalized_fields={
            'camera': exifgetter('Model'),
        },
    )

There are more predefined getters in exiffield.getters:

exifgetter(exif_key: str) -> str
Get an unmodified exif value.

get_type() -> str
Get file type, e.g. video or image

get_datetaken -> Optional[datetime]
Get when the file was created as datetime. The result is timezone-aware when the exif data contains a parseable offset (OffsetTimeOriginal/OffsetTime) or a GPSDateTime value, which is UTC by definition; otherwise it is naive.

get_orientation -> exiffield.getters.Orientation
Get orientation of media file. Possible values are LANDSCAPE and PORTRAIT.

get_sequencetype -> exiffield.getters.Mode
Guess if the image was taken in a sequence. Possible values are BURST, BRACKETING, TIMELAPSE and SINGLE.

get_sequencenumber -> int
Get image position in a sequence.

Timezones

ExifTool stores DateTimeOriginal as local time without a timezone, so get_datetaken can only return a naive datetime for files that do not carry a usable offset or GPS time. An unparseable offset raises an ExifError instead of silently producing a naive datetime. With USE_TZ = True, wrap the value with django.utils.timezone.make_aware using the timezone the photos were taken in, e.g. in a custom getter:

from zoneinfo import ZoneInfo

from django.utils import timezone

from exiffield.getters import get_datetaken


def get_datetaken_aware(exif):
    taken = get_datetaken(exif)
    if timezone.is_naive(taken):
        taken = timezone.make_aware(taken, ZoneInfo('Europe/Berlin'))
    return taken

Development

This project uses uv for packaging and managing all dependencies. ruff handles formatting and linting and mypy is used for type checking.

Clone this repository and run

uv sync

to create a virtual environment containing all dependencies. The test suite requires exiftool to be installed and executable. Afterwards, you can run the test suite using

uv run pytest

and the linters using

uv run ruff format --check .
uv run ruff check .
uv run mypy

This repository follows the Conventional Commits style.

Why is the exif data not editable?

ExifField sets editable=False on purpose: the data describes the file it was extracted from. Making it editable raises questions the library cannot answer — should changes be written back into the file? What happens to edits when a new file is uploaded? To keep the stored data consistent, the field is managed by the library and overwritten whenever a new file is extracted.

If you need to display or edit exif values in a form, keep the extracted data separate from user input:

  • To display values read-only, exclude the field from the form and render instance.exif in the template.
  • To let users edit values, add a regular editable field (e.g. a JSONField) and copy the extracted values into it once, so later user edits are kept:
from django.db import models

from exiffield.fields import ExifField


class Image(models.Model):
    image = models.ImageField()
    exif = ExifField(source='image')
    exif_notes = models.JSONField(default=dict, blank=True)

    def save(self, *args, **kwargs):
        created = self.pk is None
        super().save(*args, **kwargs)
        if created and self.exif:
            # seed the editable copy once, then leave user edits alone
            self.exif_notes = self.exif
            super().save(update_fields=['exif_notes'])

The second save() is needed because the exif data is extracted in the field's pre_save handler, i.e. during the first save(). After that, user edits — including clearing the field — are preserved.

Asynchronous extraction

Exif extraction runs synchronously in pre_save by default. If exiftool is slow or your storage is remote, pass sync=False and run the extraction from a background task:

class Photo(models.Model):
    image = models.ImageField()
    exif = ExifField(source='image', sync=False)
from celery import shared_task


@shared_task
def extract_exif(pk):
    photo = Photo.objects.get(pk=pk)
    Photo._meta.get_field('exif').update_exif(photo, commit=True)

Enqueue the task after the file has been saved (e.g. from a post_save receiver) so the file is committed to storage before exiftool reads it. commit=True saves the extracted data and fills denormalized_fields; with commit=False only the in-memory instance is updated. The same pattern works with RQ, Django-Q and other background workers.

Metadata

Release files for django-exiffield 4.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-exiffield 4.0.0
File Size Uploaded
django_exiffield-4.0.0.tar.gz 5.0 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-exiffield 4.0.0
File Interpreter ABI Platform
django_exiffield-4.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 5.0 MB

Release files / django_exiffield-4.0.0.tar.gz

Download URL django_exiffield-4.0.0.tar.gz
Size 5.0 MB
Tags Source
SHA-256 checksum
How to use checksums
dfb62e95e301f665f602852031c661a3e0c2e54074860fe4ba682772e3c416d8
BLAKE2b-256 checksum
How to use checksums
9415508b612da114141a2da4f0d6f1db7cc201c6c2a505fbd20bf673d26c81ea
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / django_exiffield-4.0.0-py3-none-any.whl

Download URL django_exiffield-4.0.0-py3-none-any.whl
Size 9.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
51d2232b0578bd3e029b07267e3ffc2ea889b3885799a80070fa58e0bb254f93
BLAKE2b-256 checksum
How to use checksums
ad159e4577be33b2dc27cb65d328657fd1a2efc6505dd9bd23648f2fdcc1da4e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

This release

4.0.0 This release

2 release files

3.0.0

2 release files

2.1.0

2 release files

2.0.0

2 release files

1.1.0

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.1

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page