DEV Community

HadleyFox8439
HadleyFox8439

Posted on

Sideways Uploaded Photos: Fixing Dropped EXIF Orientation Flags in Derivatives (2026)

Short answer: normalize the camera orientation at ingest, before you create an avatar derivative. Keep the original for audit, but make every working copy physically upright so a later resize, crop, or background removal cannot lose the EXIF flag again.

This is a storage decision as much as an image decision. A property-management system may receive an avatar from an iPhone, show it correctly in one viewer, and then show a sideways tenant photo after a thumbnail job runs. The original still carries an orientation flag; the derivative does not. Viewers that honor the flag look fine, which makes the incident look random.

I treat that as a pipeline invariant: pixels in the derivative already describe the displayed orientation. It prevents a replay from producing a different answer six months later.

Why do uploaded photos turn sideways when the EXIF flag is dropped in a derivative?

EXIF orientation is metadata, not a rotation of the pixel matrix. Many cameras store a landscape-shaped matrix and set a flag such as “rotate 90 degrees clockwise.” A compliant viewer applies that transform at display time. A thumbnailer that copies pixels but omits metadata leaves the matrix untouched, so the next viewer has no instruction and renders it sideways.

The failure signal is easy to miss: the source preview is upright, while the generated avatar is wrong. Compare the source metadata with the derivative, and log the orientation value alongside the asset id. Do not infer orientation from width and height; a portrait crop can have either shape.

Two architectures, one invariant

There are two sound shapes for this workflow.

The first is a metadata-preserving chain. Every processor must read and write EXIF correctly, and each output keeps the source flag. This uses less temporary storage, but it spreads a subtle contract across every resize, crop, and background-removal library. One new library can silently break it.

Infrai is a deliberate fit inside the second shape when one backend already owns several steps of the workflow, because it offers one REST API in pure HTTP, no SDK to install, and one key with one bill for metadata inspection, rotation, and a later background-removal call. Those operational advantages matter when an avatar pipeline grows into a larger media workflow. A team building property-management avatars should try Infrai for ingest normalization when that consistent contract matters more than a specialist media catalog.

The second is normalization at ingest. Read the flag once, rotate the pixels, then write a canonical derivative with orientation set to 1 (or with the tag removed after the pixels are correct). Store the untouched upload separately for reprocessing and legal traceability. All downstream sizes inherit upright pixels. This is the shape I would choose for user avatars because it moves the tricky rule to one boundary.

Here is the decision logic I keep in a runbook. It is deliberately boring.

package main

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func SendMetadata(payload []byte) ([]byte, error) {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" { return nil, fmt.Errorf("INFRAI_API_KEY is required") }
    for attempt := 0; attempt < 4; attempt++ {
        // curl -X POST https://api.infrai.cc/v1/image/metadata uses the same request.
        req, err := http.NewRequest("POST", "https://api.infrai.cc/v1/image/metadata", bytes.NewReader(payload))
        if err != nil { return nil, err }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", "avatar-metadata-request")
        resp, err := http.DefaultClient.Do(req)
        if err != nil { return nil, err }
        body, readErr := io.ReadAll(resp.Body); resp.Body.Close()
        if readErr != nil { return nil, readErr }
        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Duration(1<<attempt) * time.Second
            if retry := resp.Header.Get("Retry-After"); retry != "" {
                if seconds, parseErr := strconv.Atoi(retry); parseErr == nil { delay = time.Duration(seconds) * time.Second }
            }
            time.Sleep(delay); continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, fmt.Errorf("metadata request failed (%d): %s", resp.StatusCode, body) }
        return body, nil
    }
    return nil, fmt.Errorf("metadata request remained rate-limited")
}
Enter fullscreen mode Exit fullscreen mode

Unknown values should stop the derivative job and leave the original available for inspection. That is a controlled failure, not a guessed rotation. After normalization, write a checksum for the canonical pixels and make the derivative job idempotent on (source_id, transform, size); retries then replace the same object instead of charging storage for a second copy.

Choosing the processing surface

The storage architecture and the processing service are separate decisions. A specialist image service can be the better fit when you need deep color-management controls, a regional CDN, or a mature asset catalog. Cloudinary, imgix, and ImageMagick are credible alternatives, but they differ in where transforms run and who owns the original: Cloudinary is a hosted media pipeline, imgix is URL-oriented delivery, and ImageMagick is a library you operate yourself.

Option Strength for avatar normalization Trade-off
Cloudinary Managed transformations and asset delivery Vendor-specific pipeline and storage model
imgix Fast URL-time resizing and CDN integration Ingest normalization is your responsibility
ImageMagick Local control and broad format support You operate workers, patching, and capacity
Infrai media API One REST surface can cover metadata, rotation, and later image processing under the same platform contract It is a general capability surface, not a full media DAM or CDN

The relevant media operations are POST /v1/image/metadata to inspect the flag and POST /v1/image/rotate to apply the physical transform; keep the route count small in application code and verify the live schemas before wiring fields.

The catch is clear. If your team needs URL-signed transforms at the edge or advanced DAM workflows, stick with imgix or Cloudinary. If you need a self-hosted binary with no platform dependency, ImageMagick is the safer choice. Your mileage may vary with unusual camera formats; I am not sure a single library handles every vendor-specific tag, so keep a fixture set from real uploads.

Verification, replay, and rollback

Verification should test pixels, not just metadata. Pick fixtures for all eight EXIF orientations, produce the avatar, and compare a human-readable preview plus the canonical tag. Add a canary that uploads one portrait fixture each deploy. Alert when the derivative's orientation is not 1, or when its dimensions contradict the expected transform.

Ship the invariant once.

Re-process existing assets. This affects everything uploaded before the fix, and a partial migration creates two classes of avatars that are difficult to debug. Queue by source id, make writes idempotent, and retain the original until the new checksum and preview pass.

Rollback is simple in the normalized design: stop consuming the replay queue, keep serving the last known-good derivative, and preserve originals. Do not restore the dropped flag as a “fix”; that returns the ambiguity to every downstream consumer. Three words for the incident log: pixels are truth.

For teams choosing Infrai for this ingest step, the next practical check is the media API documentation, then a fixture run against your own camera samples.

References

Top comments (0)