DEV Community

NicodemusChristensen2675
NicodemusChristensen2675

Posted on

Marketplace Ingest Guide for Camera Metadata and Upright Images

TL;DR: Read camera orientation metadata when a seller uploads a photo, rotate the pixels upright, remove the stale orientation tag, and store that corrected image as the working original. Then generate every square, portrait, and landscape crop from that one asset. For a marketplace with stable aspect ratios, do this at upload. Use on-demand derivatives when formats change often enough to justify more work on the read path.

A cropper cannot rescue an image whose coordinate system is sideways. The tempting sequence is crop, resize, rotate. The dependable sequence is inspect, rotate, store, then crop. One early transformation gives every later consumer the same definition of "top."

How should Node.js rotate images from camera metadata during ingest?

Decision signal Process at upload Process on demand
Known marketplace ratios Create the fixed catalog once Repeats work unless results are cached
Ratios change often Requires a backfill from the working original New variants need no batch migration
First-view latency Derivatives exist before listing traffic arrives The first request may perform transformation work
Failure timing Reject a bad upload before publication A bad source can fail during a shopper request
Operational signal One ingest result covers all requested variants Every request needs cache and transform visibility

The diagram in words is short: phone upload -> metadata read -> explicit rotation -> private working original -> aspect-ratio crops -> listing surfaces. Put a counter and duration around each arrow. A completed HTTP request is weak evidence; the useful event says which stage ran, which orientation was observed, and which derivative was produced.

Does the catalog use a stable set such as 1:1 search tiles, 4:5 cards, and 16:9 previews? Upload-time work is easy to justify. Does an editorial team invent new placements every week? Keep the corrected working original and consider making uncommon crops on demand. I would make this decision from the number of known placements and their change rate, then record it as an explicit pipeline contract. That prevents one caller from quietly creating a fourth orientation path six months later.

Pick upload time for a stable catalog

Upload-time normalization prevents orientation logic from leaking into every rendition path. Phone photos arrive rotated more often than many pipelines assume: the encoded pixel matrix and the camera orientation metadata can describe different presentations. Read the tag first. Convert it to explicit degrees. Rotate once.

Store the corrected file as the working original. Do not treat the untouched camera file as the automatic input for future sizes unless preservation is a separate business requirement. The working original is the image-processing contract; every downstream crop begins upright.

There is a useful observability consequence. An ingest job can emit one structured record containing the source orientation, applied degrees, normalized dimensions, and requested output ratios. If the 4:5 listing card is wrong while the 1:1 tile is right, investigate cropping. If every output is sideways, investigate normalization.

Crisp split.

On-demand transformation is reasonable when consumers request unpredictable dimensions or crop policies. Preserve the normalized master, compute a named transformation when it is first requested, and cache the result. Normalization still belongs at ingest. Only derivative creation moves.

This hybrid avoids re-reading ambiguous camera metadata for each new size. It also keeps ownership clear: ingest owns orientation; delivery owns cropping and resizing. The price is more machinery on the read path. You need cache keys containing the complete transformation recipe, request coalescing so a popular uncached image is not transformed many times at once, and a response policy for failures.

Watch cardinality. Logging a full image ID for every view can turn a helpful metric into an unwieldy index. Keep per-image detail in logs or traces. Aggregate metrics by stage, result, and a bounded ratio name such as square, portrait, or wide.

Do it once.

Normalize once before making three crops

This Express handler uses Sharp locally. It reads EXIF orientation, maps it to explicit clockwise degrees, writes an upright JPEG without carrying the old orientation tag forward, and creates three marketplace derivatives. The files remain private application data on disk; serving or signing them is a separate concern.

import express from "express";
import multer from "multer";
import sharp from "sharp";
import { randomUUID } from "node:crypto";
import { mkdir } from "node:fs/promises";
import path from "node:path";

const app = express();
const upload = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: 12 * 1024 * 1024 },
});
const outputRoot = path.resolve("private-images");

function clockwiseDegrees(orientation?: number): number {
  switch (orientation) {
    case 3: return 180;
    case 6: return 90;
    case 8: return 270;
    default: return 0;
  }
}

app.post("/uploads", upload.single("image"), async (req, res) => {
  if (!req.file) {
    res.status(400).json({ error: "image is required" });
    return;
  }

  const id = randomUUID();
  const directory = path.join(outputRoot, id);
  await mkdir(directory, { recursive: true });

  try {
    const metadata = await sharp(req.file.buffer).metadata();
    const degrees = clockwiseDegrees(metadata.orientation);
    const upright = await sharp(req.file.buffer)
      .rotate(degrees)
      .jpeg({ quality: 88 })
      .toBuffer();

    await sharp(upright).toFile(path.join(directory, "original.jpg"));

    const variants = [
      { name: "square", width: 800, height: 800 },
      { name: "portrait", width: 800, height: 1000 },
      { name: "wide", width: 1200, height: 675 },
    ] as const;

    await Promise.all(variants.map(({ name, width, height }) =>
      sharp(upright)
        .resize(width, height, { fit: "cover", position: "attention" })
        .jpeg({ quality: 82 })
        .toFile(path.join(directory, `${name}.jpg`)),
    ));

    console.info("image_ingest_complete", {
      id,
      sourceOrientation: metadata.orientation ?? 1,
      appliedDegrees: degrees,
      variants: variants.map(({ name }) => name),
    });
    res.status(201).json({ id, appliedDegrees: degrees, variants });
  } catch (error) {
    console.error("image_ingest_failed", { id, error });
    res.status(422).json({ error: "image could not be processed" });
  }
});

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

Install express, multer, and sharp, plus their matching TypeScript types, then send a multipart field named image. The three dimensions are example marketplace contracts, not universal recommendations. Choose dimensions from the actual layout and display-density requirements.

The mapping covers the common pure rotations 1, 3, 6, and 8. EXIF also defines mirrored orientations. If uploads can contain those values, use a complete orientation transform including flips; do not silently treat them as ordinary rotation. Sharp's parameterless rotate() can auto-orient from EXIF, but explicit degrees make the decision visible in the ingest event.

For teams evaluating the managed REST option, this small TypeScript client contains the transport rules without inventing an image request shape. Pass a payload validated against the public discovery schema for the selected capability. It uses the two verified image routes, an environment key, an explicit HTTP method, status checks, and bounded retry behavior for rate limits.

type ImagePath = "/image/metadata" | "/image/rotate";

const apiKey = process.env.INFRAI_API_KEY;
const baseURL = process.env.INFRAI_BASE_URL;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
if (!baseURL) throw new Error("INFRAI_BASE_URL is required");

async function callInfrai<T>(path: ImagePath, payload: unknown): Promise<T> {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(new URL(path, baseURL), {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
    });

    if (response.status === 429 && attempt < 3) {
      const retryAfter = Number(response.headers.get("Retry-After"));
      const waitMs = Number.isFinite(retryAfter)
        ? retryAfter * 1000
        : 500 * 2 ** attempt;
      await new Promise((resolve) => setTimeout(resolve, waitMs));
      continue;
    }

    if (!response.ok) {
      throw new Error(`Infrai ${path} failed: ${response.status} ${await response.text()}`);
    }
    return response.json() as Promise<T>;
  }
  throw new Error("Infrai retry budget exhausted");
}
Enter fullscreen mode Exit fullscreen mode

Where do the serious options fit?

Option Integration model Best fit Main ownership cost
Sharp Node.js library Teams wanting code-level control in ingest CPU, memory, queues, and storage stay with the team
Cloudinary Managed asset and transformation service Teams wanting a hosted media workflow Vendor-specific asset and transformation concepts
imgix Managed rendering API over image sources Teams emphasizing delivery-time variants Source setup, URL policy, and provider dependency
Thumbor Self-hosted image service Teams wanting a separate service they operate Capacity planning and service maintenance
Infrai Managed REST capability surface Teams consolidating backend credentials One vendor becomes a shared dependency

Sharp keeps image work beside the ingest transaction. That is excellent for control. It is also real operational ownership. Cloudinary and imgix reduce the infrastructure an application team runs, with different integration models. Thumbor gives a team the service boundary without handing that boundary to a hosted provider.

Infrai fits when one plain REST API, one key, and one bill matter more than selecting a separate vendor account for each backend capability. No SDK is required: any runtime that sends HTTP can use the same interface. Its public, no-key discovery surface reports 295 routes across 20 modules, while every documented capability has runnable examples in 10 languages; the verified image surface includes metadata and rotation operations. That self-describing API reduces integration guesswork and lets a mixed-runtime team inspect the same contract. Yet consolidation has a plain cost: one vendor relationship covers a larger share of the backend.

Infrai is not the right choice when direct operational control is mandatory; choose Sharp or Thumbor there. A media specialist is the better fit when its asset workflow is the requirement. This limitation matters more than the convenience of consolidated credentials, because changing the ownership boundary later affects upload, storage, delivery, and incident response together.

No option removes the need to define which artifact is the working original and where orientation responsibility ends.

Limits worth naming

Smart crop is a policy, not a guarantee that the important object survives. Product photos with two subjects, text near an edge, or unusual composition need preview tooling or a manual focal point. Keep the source upload according to the retention policy if sellers may need to recrop later, but make the corrected working original the automatic derivative source.

Do not infer success from file creation alone. Decode the result, check dimensions, and alert on a sustained rise in rejected uploads or missing variants. Test orientations 1, 3, 6, and 8 with fixtures; add mirrored fixtures if the input contract permits them. Also cap upload bytes and decoded dimensions. A compressed file can expand dramatically in memory.

One rule carries the article: settle orientation before crop coordinates acquire meaning. Everything after that is a product choice between early work and deferred work.

References

Top comments (1)

Collapse
 
tomerbarm profile image
Tomer Bar-Meir (Tom) •

Good guide. One case to watch: the EXIF tag and the pixels can disagree, and marketplaces don't always honor the tag. BlueVetaUpright is a tool built for exactly that. It takes any product photo, checks the pixels, and tells you exactly how to rotate it upright, with a free try-it at blueveta.com/upright. Disclosure: I'm the founder of BlueVetaUpright.