Go · JPEG metadata · stdlib-only
Edit the metadata. Leave every other byte alone.
Byte-level parsing and minimal-diff editing of a JPEG's segments, EXIF/TIFF block, XMP packet and IPTC-IIM datasets. Bytes in, bytes out — you own the policy.
go get codeberg.org/elkarrde/exifscalpel@latest
Why it exists
Go's metadata libraries read. This one writes — surgically.
Minimal diff, by default
OverwriteValueInPlace and xmp.Clean preserve segment length, so every JPEG and TIFF offset stays put. Rebuild with Build only when a field count has to change.
Zero runtime dependencies
Standard library only. No go.sum, nothing transitive — and it compiles to GOOS=js GOARCH=wasm without friction.
Primitives, not policy
No flags, printing or file walking. What to strip and what to keep lives in your code; the library does the byte surgery.
Packages
Four packages, each usable on its own.
Only jpeg walks the container. The others take a raw segment payload and return bytes.
Parse · WriteIsEXIF · IsXMP
exif
TIFF / IFD
Parse and edit IFDs. Rebuild, or overwrite a value in place at the same length.
(*Data).BuildOverwriteValueInPlace
xmp
APP1
Field-level XMP surgery, length-preserving. Read arbitrary scalar properties.
Clean · CleanLocationReadProperties
iptc
APP13
Find and remove IPTC-IIM datasets inside a Photoshop 8BIM segment.
Parse · Remove · BuildExamples
A few lines per job.
func sanitize(dst io.Writer, r io.Reader) error { segs, tail, err := jpeg.Parse(r) if err != nil { return err // not a JPEG } kept := segs[:0] for _, s := range segs { if jpeg.IsEXIF(s) || jpeg.IsXMP(s) { continue // policy is yours } kept = append(kept, s) } return jpeg.Write(dst, kept, tail)}// seg.Data is the APP1 Exif payload, edited in placefunc scrub(seg jpeg.Segment) (bool, error) { changed, err := exif.OverwriteValueInPlace( seg.Data, exif.SoftwareTag, nil, ) if err != nil { return false, err } // changed == true: same length, same offsets return changed, nil}var ( film = xmp.Property{Namespace: "http://analogexif.sourceforge.net/ns/", Name: "Film"} lens = xmp.Property{Namespace: "http://ns.adobe.com/exif/1.0/aux/", Name: "Lens"})func filmScan(seg jpeg.Segment) (map[xmp.Property]string, error) { props, err := xmp.ReadProperties(seg.Data, []xmp.Property{film, lens}) // props[film] == "APX 400" // props[lens] == "smc Pentax-M 50mm F1.4" return props, err}Where it fits
Built for these jobs.
Upload sanitizer
Scrub on ingest inside an HTTP handler. Reader in, writer out, no allocation-heavy model.
Reachable todayLocation stripping
Remove GPS from EXIF, location fields from XMP and IPTC — nothing else touched.
Reachable todayMetadata diffing
Parse two files and compare IFD0, Exif and GPS entries by tag and value bytes.
Reachable todayIn-browser strip (WASM)
Strip EXIF before a photo leaves the device. Core is ready; needs a syscall/js shim.
Needs a thin layerProvenance-safe edits
Locate or preserve C2PA APP11 segments; length-preserving edits keep offsets stable.
Needs a thin layer
Used by