From f5902f4241a0d9fecd310df32a0bd72a16565904 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Mon, 28 Sep 2026 23:56:21 +0000 Subject: [PATCH] Strip metadata from processed images (closes #82) Every output is exported with govips' StripMetadata, so it carries no EXIF, XMP, IPTC or ICC profile, the orig format included: it is always re-encoded, and pixa never serves the source bytes. The image is turned upright with AutoRotate right after decoding, so dropping the orientation tag does not leave it rotated, and a requested size applies to the upright image. An image with an ICC profile is converted to sRGB before export, since clients show an image with no profile as sRGB. No setting turns this off. README.md documents it. Model: opus-5-5 --- README.md | 15 +++++++++++++++ TODO.md | 7 ++++++- internal/imageprocessor/imageprocessor.go | 22 ++++++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 85af3f2..4765703 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,21 @@ with a private address can choose the address it is counted by through its own its own address is trusted too. Setting `trusted_proxies` to the proxy's own address closes this. +### Image Metadata + +pixa decodes and re-encodes every image it serves, and removes all metadata from +the output: EXIF (GPS position, camera make, model and serial number, capture +time, embedded thumbnail), XMP, IPTC and the ICC colour profile. This cannot be +turned off. + +- The `orig` format means the source's own format, not the source's bytes: an + `orig` image is re-encoded and stripped like any other. +- An image with an EXIF orientation is turned upright first, so it displays the + same without the tag; a requested size applies to the upright image. +- An image with an ICC profile is converted to sRGB first, since clients show an + image with no profile as sRGB. Colours outside sRGB, such as the most + saturated ones in a Display P3 photo, are clipped. + ### Source Hosts Source hosts may be allowlisted in the configuration. Non-allowlisted diff --git a/TODO.md b/TODO.md index 486c8eb..56d0745 100644 --- a/TODO.md +++ b/TODO.md @@ -30,6 +30,12 @@ exhaustion # Completed Steps +- 2026-09-28 strip metadata from processed images (closes #82): every output is + exported with govips' `StripMetadata`, so it carries no EXIF, XMP, IPTC or ICC + profile; the image is first turned upright with `AutoRotate` (before sizes are + worked out) and, when it has an ICC profile, converted to sRGB; the `orig` + format is re-encoded and stripped like any other, as pixa never serves the + source bytes; there is no setting to keep metadata; documented in `README.md`. - 2026-09-28 rate limit the login form (closes #66): `POST /` is limited to 5 attempts per minute per client address, and an attempt over the limit is refused with 429 and a `Retry-After` header; the address is the one @@ -251,7 +257,6 @@ exhaustion # Future Steps -- P1: strip EXIF and other metadata from processed images (privacy) - P2: security - referer blacklist - per-IP rate limiting on the image routes diff --git a/internal/imageprocessor/imageprocessor.go b/internal/imageprocessor/imageprocessor.go index 32e7b7a..dd77ac1 100644 --- a/internal/imageprocessor/imageprocessor.go +++ b/internal/imageprocessor/imageprocessor.go @@ -161,6 +161,13 @@ func (p *ImageProcessor) Process( } defer img.Close() + // Turn the image upright now: encode strips the EXIF orientation tag, + // and sizes below must be worked out on the upright image. + err = img.AutoRotate() + if err != nil { + return nil, fmt.Errorf("failed to auto-rotate: %w", err) + } + // Get original dimensions origWidth := img.Width() origHeight := img.Height() @@ -404,6 +411,21 @@ func (p *ImageProcessor) encode( return nil, fmt.Errorf("%w: %s", ErrUnsupportedOutputFormat, format) } + // Stripping drops the ICC profile as well, and clients show an image + // with no profile as sRGB, so convert to sRGB first. "srgb" names + // libvips' built-in profile; govips' own sRGB path variable is set on + // first use but read without a lock, so concurrent requests race on it. + if img.HasICCProfile() { + err := img.TransformICCProfileWithFallback("srgb", "srgb") + if err != nil { + return nil, fmt.Errorf("failed to convert to sRGB: %w", err) + } + } + + // Drop EXIF, XMP, IPTC and the ICC profile. govips ignores this for + // GIF, which carries none of them. + params.StripMetadata = true + output, _, err := img.Export(¶ms) if err != nil { return nil, err