django-exiffield
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
-
Install django-exiffield
pip install django-exiffield
-
Make sure
exiftoolis 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 descriptionval: 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.exifin 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)
| File | Size | Uploaded | |
|---|---|---|---|
| django_exiffield-4.0.0.tar.gz | 5.0 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|