Read, write, and synchronise EXIF/IPTC/XMP image metadata in JPEG, PNG, and TIFF
Project description
svk-img-metadata
Read, write, and synchronise descriptive image metadata — EXIF, IPTC-IIM, and XMP — across JPEG, PNG, and TIFF, in mostly-pure Python (Pillow, piexif, defusedxml; no native exiv2/libheif, no ExifTool).
It focuses on the descriptive / rights / location fields you actually curate (caption, creator, copyright, keywords, location, dates, rating…), not camera-technical tags. A single unified model maps each logical field to its correct home in all three standards, so you can read from wherever the value lives and write it back everywhere it belongs.
Installation
pip install svk-img-metadata
Requires Python 3.10+.
Usage
Read
import svk_img_metadata as svk
meta = svk.read("photo.jpg")
print(meta.description) # a caption (as a LangAlt; str() gives x-default)
print(meta.creator) # ["Jane Doe"]
print(meta.keywords) # ["sky", "sea"]
print(meta.city, meta.country)
print(meta.gps) # GPSCoord(latitude=59.33, longitude=18.06, altitude=None)
print(meta.present_fields()) # which canonical fields are set
Values are merged across the three standards with XMP > IPTC > EXIF precedence. The raw per-standard views are available too (meta.exif, meta.iptc, meta.xmp).
Write
from svk_img_metadata import read
from svk_img_metadata.model import LangAlt, MetaDate, GPSCoord
from datetime import datetime, timezone
meta = read("photo.jpg")
meta.description = LangAlt("Sunset over the archipelago")
meta.creator = ["Jane Doe"]
meta.keywords = ["sunset", "sea"]
meta.copyright = "© 2026 Jane Doe"
meta.date_created = MetaDate(datetime(2026, 7, 12, 20, 30, tzinfo=timezone.utc))
meta.gps = GPSCoord(59.33, 18.06)
meta.save("photo-tagged.jpg") # or meta.save() to overwrite in place
Canonical fields fan out to every standard that carries them. Pixel data and any metadata the library does not model (unknown EXIF tags, custom XMP namespaces, other Photoshop resources) are preserved. Restrict the carriers with meta.save(path, standards=("xmp",)).
Canonical fields: title, headline, description, creator, creator_title, copyright, rights_usage, credit, source, keywords, date_created, instructions, job_id, sublocation, city, state, country, country_code, gps, rating, label.
Synchronise between standards
Copy one standard's values into the fields the others carry — e.g. populate IPTC/XMP from a camera's EXIF:
meta = read("from-camera.jpg")
meta.sync("exif", targets=["iptc", "xmp"]) # fill empty IPTC/XMP fields from EXIF
meta.sync("exif", overwrite=True) # or force EXIF to win over the merge precedence
meta.save()
See where the standards currently disagree:
meta.diff_standards()
# {"description": {"exif": LangAlt('old caption'), "xmp": LangAlt('new caption')}}
Export XMP (string or sidecar)
xml = meta.to_xmp() # standalone XMP packet as a string
meta.write_xmp_sidecar() # -> photo.xmp (basename convention)
meta.write_xmp_sidecar(naming="fullname") # -> photo.jpg.xmp
sidecar = svk.read_sidecar("photo.xmp") # read a .xmp back into the model
Custom XMP namespaces
svk.register_namespace("https://example.com/ns/1.0/", "acme")
meta.xmp.set("acme:ReviewStatus", "approved")
meta.xmp.get("acme:ReviewStatus") # "approved"
meta.save() # custom property is round-tripped
Validate required metadata
schema = svk.RequiredFields(["description", "creator", "copyright"])
result = meta.validate(schema)
if not result:
print("missing:", result.missing) # e.g. ["copyright"]
# require a field in a specific standard, or raise on failure:
svk.RequiredFields([("copyright", "xmp")])
meta.validate(schema, strict=True) # raises ValidationError if incomplete
Strip metadata (privacy)
meta.strip(fields=["gps"]) # remove location only
meta.strip() # remove all modelled fields
meta.save("clean.jpg")
Format support
| Format | Read | Write |
|---|---|---|
| JPEG | ✅ | ✅ embedded |
| PNG | ✅ | ✅ embedded (EXIF + XMP; PNG has no standard IPTC slot) |
| TIFF | ✅ | ✅ via XMP sidecar (write_xmp_sidecar); embedding not supported |
| HEIC | — | — |
For TIFF, save() raises rather than embedding — use meta.write_xmp_sidecar() to write a .xmp alongside the file (the standard sidecar workflow).
Malformed or hostile input always raises a clean svk_img_metadata.MetadataError (never an unhandled crash); XMP is parsed with defusedxml to block XXE and entity-expansion attacks.
Development
Uses uv:
uv sync --group dev # install project + dev deps
uv run pytest # run the test suite
uv run ruff check . # lint
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file svk_img_metadata-0.1.0.tar.gz.
File metadata
- Download URL: svk_img_metadata-0.1.0.tar.gz
- Upload date:
- Size: 39.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8f830fb0eb57017ec57dee5ae6045731c3d7c41705927f44ffaee7ca8e07fce6
|
|
| MD5 |
d0f7f87021c0d7837b415fa941a2c1dc
|
|
| BLAKE2b-256 |
72960d8046812a5cdb7dcf7d24e9ffbb1b433524672926d4870d879dc32978b6
|
Provenance
The following attestation bundles were made for svk_img_metadata-0.1.0.tar.gz:
Publisher:
publish.yml on vitlais/svk-img-metadata
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
svk_img_metadata-0.1.0.tar.gz -
Subject digest:
8f830fb0eb57017ec57dee5ae6045731c3d7c41705927f44ffaee7ca8e07fce6 - Sigstore transparency entry: 2163682999
- Sigstore integration time:
-
Permalink:
vitlais/svk-img-metadata@dde770f52c01f806333bf66160bdf56e2f045ae9 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/vitlais
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@dde770f52c01f806333bf66160bdf56e2f045ae9 -
Trigger Event:
release
-
Statement type:
File details
Details for the file svk_img_metadata-0.1.0-py3-none-any.whl.
File metadata
- Download URL: svk_img_metadata-0.1.0-py3-none-any.whl
- Upload date:
- Size: 35.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
12d0184f381096489deae19e1cfbb1445f4543dea4c0f9bd7e6df9a1e73a2319
|
|
| MD5 |
a548a84ed40a7746bf5bd7eaf660b25f
|
|
| BLAKE2b-256 |
d4ad43c889cd15a2e34447f95d711eb73c699318aec1433d69a86e7d687fa905
|
Provenance
The following attestation bundles were made for svk_img_metadata-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on vitlais/svk-img-metadata
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
svk_img_metadata-0.1.0-py3-none-any.whl -
Subject digest:
12d0184f381096489deae19e1cfbb1445f4543dea4c0f9bd7e6df9a1e73a2319 - Sigstore transparency entry: 2163683005
- Sigstore integration time:
-
Permalink:
vitlais/svk-img-metadata@dde770f52c01f806333bf66160bdf56e2f045ae9 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/vitlais
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@dde770f52c01f806333bf66160bdf56e2f045ae9 -
Trigger Event:
release
-
Statement type: