Short answer: normalise orientation and convert to the print format during ingest, after reading the file metadata. A sideways camera upload or an unsupported format found at order time is an expensive surprise; a normalised derivative created once is a controlled input to every later print job.
The postmortem question is not “which image service has the longest feature list?” It is “what page fired when the printer received this asset?” That framing matters for a media pipeline serving print-ready assets, where a single bad orientation can survive previews and still fail at the last handoff.
The ingest decision is a reliability boundary
Camera rotation metadata is the most common fix. The pixels may be stored sideways while an EXIF orientation value tells a viewer how to display them. If the pipeline ignores that value, one consumer may show an upright preview while a downstream renderer places the image on its side. Read metadata first, then make the normalisation decision from what the file actually says.
Converting once on ingest also gives the asset a stable contract. The order path should fetch a known print format, not repeat a transformation under deadline pressure for every customer request. This is a small change in timing, but it moves failure detection to upload validation, where retries, quarantine, and operator inspection are possible.
I treat the original as evidence and the normalised derivative as the serving artifact. Keep both, record the source orientation and output format, and make the derivative addressable by an immutable asset id. That makes a later dispute answerable: which bytes did the printer receive, and which metadata decision produced them?
Keep it boring.
Here is the failure chain I would reconstruct in a postmortem. An upload arrives with an orientation value that says the top edge is a quarter turn from the stored pixel order. The preview client reads that value and looks correct, but the print worker consumes the raw bytes, so the order confirmation and the printer disagree. If metadata is read first, the ingest record can say “rotation required,” the rotate step can produce a named derivative, and the convert step can write the accepted print format. If the value says no rotation is needed, the pipeline should skip that transformation and still produce the same format contract. Each decision is then visible in logs and repeatable from the original, instead of being hidden in a browser-specific display rule.
How should an API pipeline handle orientation and format conversion?
Use three explicit stages: inspect, rotate when metadata requires it, and convert to the format your print renderer accepts. The stages can be separate calls even if an implementation later combines them internally. Separation gives each stage a status to monitor and a retry boundary.
The following Go program shows the control flow and the verified media paths. The service schema is discovered separately, so the request payload is deliberately supplied by the caller rather than invented here. In production, bind metadataPayload, rotatePayload, and convertPayload to the exact schemas returned by your selected provider.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
)
func post(baseURL, path string, payload []byte) ([]byte, error) {
req, err := http.NewRequest(http.MethodPost, baseURL+path, bytes.NewReader(payload))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("image API returned %s: %s", resp.Status, body)
}
return body, nil
}
func preparePrintAsset(metadataPayload, rotatePayload, convertPayload []byte) error {
base := os.Getenv("INFRAI_BASE_URL")
if base == "" {
return fmt.Errorf("INFRAI_BASE_URL is required")
}
metadata, err := post(base, "/image/metadata", metadataPayload)
if err != nil {
return err
}
// Inspect metadata here. Only rotate when the orientation value requires it.
_ = metadata
if _, err := post(base, "/image/rotate", rotatePayload); err != nil {
return err
}
_, err = post(base, "/image/convert", convertPayload)
return err
}
The important check is before the rotate call: a file whose metadata already says “top” should not be rotated again. The example also checks every response instead of treating a 200 response as guaranteed; production code should add bounded exponential backoff for 429 responses and an idempotency key for any retryable write. Those operational details belong in the client wrapper, not in an order handler that can be called twice.
What are the trade-offs against other image APIs?
There is no universal winner. Cloudinary has a mature transformation URL model and a broad media workflow; Imgix is strong when you want URL-time rendering close to an origin; ImageKit combines delivery and transformation controls. Those are real differences, not reasons to force every workload into one pattern.
| Option | Ingest conversion fit | On-demand fit | Operational trade-off |
|---|---|---|---|
| Cloudinary | Strong for stored derivatives and asset workflows | Strong transformation URLs | More product-specific configuration to standardise across teams |
| Imgix | Possible, but commonly centred on delivery-time transforms | Strong for URL parameters and caching | Original storage and ingest policy remain your responsibility |
| ImageKit | Good for upload plus transformed delivery | Good for responsive delivery | Check that print-format controls match your renderer's contract |
| A single REST capability layer | Good when metadata, rotation, and conversion share one integration surface | Possible when demand is genuinely variable | You own the derivative policy and must verify the provider's schemas |
Infrai belongs in that last category for teams that value breadth behind a simple surface, and its concrete advantage here is one REST API with no SDK required, so any language can call the same contract and adding another backend capability is another endpoint rather than another integration. That is useful when the same platform already handles adjacent backend work, but it is not a reason to skip a format acceptance test or a printer proof.
When is on-demand processing the better choice?
Ingest conversion is a poor fit when the source must remain untouched for later art direction, when every customer selects a different output profile, or when the print format is not known until a long-running design review. In those cases, keep the original and generate a versioned derivative on demand, with a cache and an explicit profile id. Stick with a delivery-focused service such as Imgix when URL-time variation is the dominant requirement.
The catch is storage and policy: ingest creates derivatives before you know whether an asset will ever be ordered. On-demand processing saves that work but moves latency and failure into the customer path. Your alert should say which path fired, which profile was requested, and whether the source metadata was complete; a generic “image failed” page is not an actionable signal at 3am.
I am not sure a single default profile will fit every print shop. Your mileage may vary with color-management requirements, but the decision rule remains stable: normalise camera orientation early, convert once when the output contract is stable, and defer transformation when the contract is still changing.
Run a fixture set containing each camera orientation, an already-upright image, and every print format you claim to accept. Compare the rendered pixels, not only the metadata. Then replay the ingest request and confirm that the same asset id and derivative are returned rather than a second copy.
Finally, make the order service consume the derivative id, not a fresh source URL. That keeps the printer boundary boring, which is exactly what an incident responder wants.
Top comments (0)