Skip to content

About

A pure Go library for reading and writing EXIF, IPTC, and XMP metadata from any image format.

Topics

Resources

Contributing

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Latest commit

 

History

341 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GoMetadata

GoMetadata

Go Reference CI License: MIT Go: 1.27+ codecov Release

A pure Go library for reading and writing EXIF, IPTC, and XMP metadata from any image format. GoMetadata provides a single, unified API over all three metadata standards — EXIF 3.1 (CIPA DC-008-2026, reads 3.0 and 2.x / TIFF 6.0), IPTC IIM 4.2, and XMP (ISO 16684-1) — across 13 container formats including JPEG, TIFF, PNG, WebP, HEIF/AVIF, and the major RAW formats (CR2, CR3, NEF, ARW, DNG, ORF, RW2).

Developers searching for a Go EXIF library, a Go IPTC parser, or a way to read and write XMP metadata in Go will find that GoMetadata handles all three in a single import. Format detection is by magic bytes, not file extension. All parsers are fuzz-tested and race-clean.

Installation

go get github.com/FlavioCFOliveira/GoMetadata

Requires Go 1.27 or later. No non-stdlib runtime dependencies.

Usage

Reading common fields

package main

import (
	"fmt"
	"log"

	gometadata "github.com/FlavioCFOliveira/GoMetadata"
)

func main() {
	m, err := gometadata.ReadFile("photo.jpg")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("Camera:", m.CameraModel())
	fmt.Println("Make:  ", m.Make())
	fmt.Println("Lens:  ", m.LensModel())

	if lat, lon, ok := m.GPS(); ok {
		fmt.Printf("GPS: %.6f, %.6f\n", lat, lon)
	}
	if t, ok := m.DateTimeOriginal(); ok {
		fmt.Println("Shot:", t)
	}
	if num, den, ok := m.ExposureTime(); ok {
		fmt.Printf("Exposure: %d/%d s\n", num, den)
	}
	if f, ok := m.FNumber(); ok {
		fmt.Printf("Aperture: f/%.1f\n", f)
	}
	if iso, ok := m.ISO(); ok {
		fmt.Println("ISO:", iso)
	}

	fmt.Println("Caption:  ", m.Caption())
	fmt.Println("Copyright:", m.Copyright())
	fmt.Println("Keywords: ", m.Keywords())
}

Writing and modifying metadata

Write and WriteFile preserve all image data and all metadata not explicitly changed. WriteFile performs an atomic in-place update via a temporary file and rename.

package main

import (
	"log"
	"time"

	gometadata "github.com/FlavioCFOliveira/GoMetadata"
)

func main() {
	m, err := gometadata.ReadFile("photo.jpg")
	if err != nil {
		log.Fatal(err)
	}

	m.SetCaption("Grand Canyon, South Rim")
	m.SetCopyright("2024 Jane Smith")
	m.SetCreator("Jane Smith")
	m.SetKeywords([]string{"landscape", "canyon", "arizona"})
	m.SetGPS(36.0544, -112.1401)
	m.SetDateTimeOriginal(time.Date(2024, 9, 14, 7, 32, 0, 0, time.UTC))

	if err := gometadata.WriteFile("photo.jpg", m); err != nil {
		log.Fatal(err)
	}
}

Skipping segments for faster reads

Use ReadOption helpers to skip segments you do not need. Skipping the MakerNote is the single biggest speed win for cameras with large proprietary blocks.

m, err := gometadata.ReadFile("photo.jpg",
	gometadata.WithoutMakerNote(),
	gometadata.WithoutIPTC(),
	gometadata.WithoutXMP(),
)

Raw segment access and building from scratch

When you need direct access to the raw bytes of a segment, or want to construct a Metadata value to embed in a new file:

// Raw segment bytes — useful for forwarding to another library or logging.
exifBytes := m.RawEXIF()
xmpBytes  := m.RawXMP()
iptcBytes := m.RawIPTC()

// Build a Metadata value from scratch (no source image required).
import "github.com/FlavioCFOliveira/GoMetadata/format"

blank := gometadata.NewMetadata(format.JPEG)
blank.SetCameraModel("Custom Device")
blank.SetCopyright("2024 Example Corp")

Examples

Reading RAW file metadata

examples/raw-inspector — extract camera identification, shooting parameters, GPS, and descriptive fields from any RAW format (CR2, CR3, NEF, ARW, DNG, ORF, RW2). Format is detected automatically from magic bytes.

// WithoutMakerNote skips the costliest part of EXIF parsing —
// the manufacturer-specific IFD — when only standard tags are needed.
m, err := gometadata.ReadFile(path, gometadata.WithoutMakerNote())

fmt.Printf("Format:      %s\n",     m.Format())
fmt.Printf("Make/Model:  %s %s\n",  m.Make(), m.CameraModel())
fmt.Printf("Lens:        %s\n",     m.LensModel())

if num, den, ok := m.ExposureTime(); ok { fmt.Printf("Shutter: 1/%d s\n", den/num) }
if f, ok := m.FNumber();             ok { fmt.Printf("Aperture: f/%.1f\n", f) }
if iso, ok := m.ISO();               ok { fmt.Printf("ISO: %d\n", iso) }
if fl, ok := m.FocalLength();        ok { fmt.Printf("Focal: %.0f mm\n", fl) }
if wb, ok := m.WhiteBalance();       ok { fmt.Printf("WB: %d\n", wb) }   // 0=auto 1=manual
if fl, ok := m.Flash();              ok { fmt.Printf("Flash fired: %v\n", fl&0x01 != 0) }
if lat, lon, ok := m.GPS();          ok { fmt.Printf("GPS: %.6f, %.6f\n", lat, lon) }
if alt, ok := m.Altitude();          ok { fmt.Printf("Altitude: %.1f m\n", alt) }

Batch copyright stamping

examples/copyright-stamp — walk a directory tree and embed copyright, creator, caption, and keywords into every image. Setters write to all non-nil metadata components (EXIF, IPTC, XMP) in one call.

m, err := gometadata.ReadFile(path)
// Distinguish corrupt / truncated files from hard I/O errors.
var corrupt *gometadata.CorruptMetadataError
var truncated *gometadata.TruncatedFileError
switch {
case errors.As(err, &corrupt):   /* skip */
case errors.As(err, &truncated): /* skip */
}

// Each setter writes to all non-nil metadata layers simultaneously:
// SetCopyright → EXIF tag 0x8298 + IPTC dataset 2:116 + XMP dc:rights
m.SetCopyright("© 2025 Jane Smith. All rights reserved.")
m.SetCreator("Jane Smith")
m.SetCaption("Grand Canyon at sunset")
m.SetKeywords([]string{"landscape", "canyon", "arizona"})

gometadata.WriteFile(path, m) // atomic: temp file + rename

Stream pipeline (no disk I/O)

examples/stream-transcode — read metadata from stdin, update fields, write to stdout. No temporary files. Works with any io.ReadSeeker and io.Writer — net/http, bytes.Buffer, object-store streams.

m, err := gometadata.Read(os.Stdin)  // *os.File implements io.ReadSeeker

m.SetCaption("...")
m.SetGPS(48.8566, 2.3522) // Paris

// Seek back so Write can re-read the original image bytes from the same handle.
os.Stdin.Seek(0, io.SeekStart)

// PreserveUnknownSegments passes APP/chunk segments the library
// does not recognise through byte-for-byte (e.g. ICC profiles).
gometadata.Write(os.Stdin, os.Stdout, m, gometadata.PreserveUnknownSegments(true))
stream-transcode -caption "Night shot" -copyright "2025 J. Smith" < input.jpg > output.jpg

JSON metadata export

examples/gallery-sidecar — parse images and emit a JSON array for static site generators, search indexes, or API responses. Optional fields use Go pointer types so absent values serialise as null.

type imageRecord struct {
    File        string   `json:"file"`
    Format      string   `json:"format"`
    Model       *string  `json:"model,omitempty"`
    CapturedAt  *string  `json:"captured_at,omitempty"` // RFC3339
    Latitude    *float64 `json:"latitude,omitempty"`
    Longitude   *float64 `json:"longitude,omitempty"`
    ISO         *uint    `json:"iso,omitempty"`
    // ...
}

m, _ := gometadata.ReadFile(path, gometadata.WithoutMakerNote())
rec := imageRecord{File: path, Format: m.Format().String()}
if t, ok := m.DateTimeOriginal(); ok { s := t.Format(time.RFC3339); rec.CapturedAt = &s }
if lat, lon, ok := m.GPS();       ok { rec.Latitude = &lat; rec.Longitude = &lon }
if iso, ok := m.ISO();            ok { rec.ISO = &iso }
gallery-sidecar -pretty photo1.jpg photo2.nef photo3.heic

Multi-format round-trip smoke test

examples/multi-format-roundtrip — read, modify, write, and re-read across all supported formats. Exits non-zero on any mismatch. Useful as a pre-release integration test.

m, _ := gometadata.ReadFile(path)

// Check format before writing — not all container variants support write.
if !format.SupportsWrite(m.Format()) { /* skip */ }

m.SetCaption("roundtrip-test")
m.SetGPS(51.5074, -0.1278)

tmp, _ := os.CreateTemp(filepath.Dir(path), "roundtrip-*"+ext)
defer os.Remove(tmp.Name())

gometadata.WriteFile(tmp.Name(), m)

m2, _ := gometadata.ReadFile(tmp.Name())
fmt.Printf("PASS/FAIL %s (%s): caption=%v\n",
    path, m.Format(), m2.Caption() == "roundtrip-test")

Supported features

Feature Details
Metadata standards EXIF 3.1 (CIPA DC-008-2026, reads 3.0 and 2.x / TIFF 6.0), IPTC IIM 4.2, XMP (ISO 16684-1)
Read Whichever of EXIF / IPTC / XMP the container carries, across all 13 formats (IPTC is carried by JPEG, the TIFF-based formats, and PNG "Raw profile type iptc" text chunks)
Write All three standards across all 13 container formats, including BigTIFF (magic 0x002B); preserves unmodified metadata byte-for-byte. All TIFF-based formats (TIFF, DNG, CR2, NEF, ARW, ORF, RW2) use a copy-and-relocate serializer that preserves image-data blocks (strips/tiles/MakerNote blobs) at corrected offsets; BigTIFF uses a native 64-bit encoder and a container-width-aware relocator.
Atomic writes WriteFile uses temp file + rename — no partial writes
Format detection Magic bytes only; file extension is never consulted
Selective write Write encodes only the segments that were modified and copies the others byte for byte; a Read and Write with no change returns the image unchanged. The Set* methods mark what they change; after editing m.EXIF, m.IPTC or m.XMP directly, call m.MarkModified(gometadata.SegmentIPTC) (or the segment you changed). TIFF-based formats lay their IFDs out again whenever something is written
MakerNote (read) Canon, Nikon, Sony, Sony Ericsson, Olympus, OM System, Epson, Agfa, Panasonic, Pentax, Ricoh, DJI, FujiFilm, Leica, Samsung, Sigma, Minolta, Casio, Kyocera, Sanyo, Apple; the layout is detected from the MakerNote signature, then from the Make
MakerNote (edit) m.DeleteMakerNoteTag("CameraSerialNumber") removes a tag and its value; m.SetMakerNoteTag("0x0096", "text") replaces a value of the same or another size. Offsets inside the MakerNote are kept valid, the EXIF is the only segment rewritten, and image data is untouched. JPEG, PNG, WebP, HEIF, AVIF, TIFF, DNG, CR2, NEF, ARW and ORF; a MakerNote whose layout or offsets cannot be edited safely is refused with ErrMakerNoteNotEditable and left unchanged
Convenience getters 30+ typed getters with explicit source-priority resolution
Convenience setters 15+ setters that write to all applicable non-nil components simultaneously
Priority resolution Each getter documents its source order (e.g., EXIF > XMP); the caller always gets one answer
Lazy parsing WithoutEXIF(), WithoutIPTC(), WithoutXMP(), WithoutMakerNote() skip unwanted work
Allocation budget Zero/near-zero heap allocation in parsing fast paths; sync.Pool for reusable buffers
Fuzz testing 37 fuzz targets covering all parsers and format extractors/injectors (see SECURITY.md)
Race safety Clean under go test -race ./...
Corpus coverage 3,000+ real-world images tested, 0 failures

Supported formats

Format Extension(s) Read Write EXIF IPTC XMP
JPEG .jpg, .jpeg Yes Yes Yes Yes Yes
TIFF .tif, .tiff Yes Yes² Yes Yes Yes
PNG .png Yes Yes Yes Yes⁵ Yes
WebP .webp Yes Yes Yes No Yes
HEIF .heif, .heic Yes Yes Yes No Yes
AVIF .avif Yes Yes Yes No Yes
Canon CR2 .cr2 Yes Yes² Yes Yes Yes
Canon CR3 .cr3 Yes Yes Yes No Yes
Nikon NEF .nef Yes Yes² Yes Yes Yes
Sony ARW .arw Yes Yes² Yes Yes Yes
Adobe DNG .dng Yes Yes² Yes Yes Yes
Olympus ORF .orf Yes Yes³ Yes Yes Yes
Panasonic RW2 .rw2 Yes Yes⁴ Yes Yes Yes

² Copy-and-relocate write (format/tiff/relocate.go). Image strips, tiles, and non-thumbnail JPEG blocks are enumerated, copied verbatim to fresh absolute offsets in the output stream, and all offset entries are patched. DNG additionally relocates SubIFD structures and their out-of-line values (RATIONAL, SRATIONAL, etc.). CR2 and NEF route through the same path; MakerNotes are copied verbatim, and those whose value offsets count from the TIFF header (Canon, Sony, Panasonic, Olympus "OLYMP", ...) are rebased when they move. SubIFDs of every chained IFD, SubIFD arrays and SubIFD chains are carried. NEF extends the Nikon Type-3 MakerNote blob to cover PreviewIFD and NikonScanIFD. ARW rebases Sony MakerNote TIFF-absolute offsets and relocates the SR2Private block verbatim. Use format.SupportsWrite(id) to check programmatically. BigTIFF sources (magic 0x002B) are written through the same TIFF row: exif.Encode emits a native 16-byte-header, 64-bit-offset BigTIFF IFD skeleton, and the relocator's raw-offset scans, image-block enumeration, and SubIFD handling are all container-width-aware, so image data and metadata round-trip byte-for-byte just as they do for classic TIFF.

³ ORF copy-and-relocate write: the non-standard IIRO/IIRS magic bytes are patched to standard LE TIFF before relocation and restored in the output. Both IIRO (Olympus DSLRs) and IIRS (older compacts) are supported.

⁴ RW2 copy-and-relocate write: the Panasonic 16-byte device GUID header is preserved and all absolute IFD0 offsets are rebased after GUID insertion.

⁵ PNG IPTC: PNG has no native IPTC chunk. IPTC stored in a tEXt, zTXt or iTXt chunk keyed "Raw profile type iptc" is read, and Write rewrites that chunk in place when the IPTC is modified; Write never adds one.

Performance

Benchmarks run with go test -run '^$' -bench <name> -benchmem -count=5 <package> (Go 1.27.3, macOS, Apple M4, GOMAXPROCS=10).
All figures are the median of five runs, as benchstat reports it; allocation counts are stable across runs.

Reproducing the benchmarks

Run the full suite:

go test -bench=. -benchmem -benchtime=2s ./...

Scope to a single package (e.g. the EXIF parser):

go test -bench=. -benchmem -benchtime=2s ./exif/...

Run a single named benchmark:

go test -bench=BenchmarkParseEXIF -benchmem -benchtime=2s ./exif/...

Results vary by machine. The figures in this README were collected on macOS (Apple M4) with Go 1.27.3 and GOMAXPROCS=10. Pin GOMAXPROCS to make comparisons across machines more meaningful:

GOMAXPROCS=10 go test -bench=. -benchmem -benchtime=2s ./...

For lower noise, run multiple iterations and pipe through benchstat:

go install golang.org/x/perf/cmd/benchstat@latest
go test -bench=. -benchmem -benchtime=2s -count=5 ./... | benchstat /dev/stdin

benchstat computes the median and confidence interval across the five runs, which is more reliable than any single measurement.

End-to-end read

Scenario Time/op Memory/op Allocs/op
Progressive JPEG (no metadata) 226 ns 208 B 1
JPEG — EXIF + IPTC + XMP combined 18.3 µs 24.2 kB 56
Real-world JPEG corpus file 3.51 µs 7.9 kB 15
Concurrent reads (parallel goroutines) 12.0 µs 708 B 9

Write

Operation Time/op Memory/op Allocs/op
JPEG — metadata update 394 ns 112 B 3
PNG — pass-through 155 ns 80 B 6

Metadata format parsers

Format Operation Time/op Memory/op Allocs/op
EXIF Parse — minimal TIFF (width, height, orientation) 170 ns 344 B 4
EXIF Parse — camera tags 1.61 µs 2.6 kB 8
EXIF Encode 126 ns 80 B 2
EXIF IFD tag lookup — 100-entry set (binary search) 3.7 ns 0 B 0
IPTC Parse 121 ns 480 B 4
IPTC Encode 168 ns 304 B 2
IPTC Field accessor 17 ns 48 B 1
XMP Parse 1.40 µs 1.2 kB 8
XMP Encode 1.19 µs 3.2 kB 3

Container format parsers

Format Operation Time/op
JPEG Segment extraction 136 ns
JPEG Segment injection 337 ns
JPEG Real corpus file (full parse) 1.67 µs
PNG Extraction 260 ns
PNG Extraction — compressed XMP (zlib) 484 ns
TIFF Extraction 49 ns
WebP Extraction 78 ns
HEIF / AVIF Extraction 190 ns
Sony ARW Extraction 28 ns
Canon CR2 Extraction 28 ns
Adobe DNG Extraction 27 ns
Nikon NEF Extraction 27 ns
Canon CR3 Extraction 82 ns
Olympus ORF Extraction 46 ns
Panasonic RW2 Extraction 45 ns

Internal primitives

Component Operation Time/op
sync.Pool buffer Get + Put (4 kB) 7.1 ns
sync.Pool buffer Get + Put (64 kB) 6.9 ns
RIFF Read chunk header 18.6 ns

Design choices behind the numbers

Technique Effect
sync.Pool-backed buffers (internal/iobuf) Amortises heap allocation to zero after warm-up
Lazy parsing (WithoutEXIF, WithoutIPTC, WithoutXMP, WithoutMakerNote) Skips unwanted segments entirely; MakerNote skip is the largest win on RAW files
Binary search in IFD entry set Tag lookup in a 100-entry IFD costs 3.7 ns and 0 allocations
Lazy map init for extended XMP Map is only allocated when extended XMP is actually present
Magic-byte format detection Dispatch adds no measurable overhead; no string allocation

API reference

Full documentation is available at pkg.go.dev/github.com/FlavioCFOliveira/GoMetadata.

License

MIT

About

A pure Go library for reading and writing EXIF, IPTC, and XMP metadata from any image format.

Topics

Resources

Contributing

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages