Short answer: treat a merchant menu image as an immutable source plus explicitly generated derivatives; validate each lifecycle transition, and compress only after the background decision is recorded. That ordering keeps a cheap derivative from becoming the only copy when a restaurant disputes a crop.
Food-delivery onboarding has a peculiar storage bill. The pixels are not the only cost: retained originals, duplicate uploads, cache churn, and failed transformations all leave objects behind. I model those terms before choosing an image library. A 4 MB phone photo that is fetched twice a day is usually less interesting than thousands of abandoned 4 MB originals kept for a year. Retention policy moves the dominant term.
What should a menu-photo pipeline validate before compression?
Start with a quarantine prefix and a content-addressed key. Read the byte stream, verify its declared media type against a decoder, and reject files whose dimensions or decoded pixel count exceed a policy limit. EXIF orientation needs normalization before any crop or background mask; otherwise a portrait dish can become a sideways thumbnail while the metadata still says it is upright.
Background cleanup is a decision, not a filter. Store the source, the mask or trim box, the operator or model version, and the resulting derivative as separate records. A plain white background may satisfy a storefront rule, while a transparent background can create halos on a dark app theme. Keep the original long enough to re-run the decision when that presentation rule changes.
Keep it deterministic.
The validation record should be boring and machine-checkable. I use fields such as source_sha256, width, height, decoded_pixels, orientation_applied, background_mode, encoder, quality, and created_at. A failed check goes to quarantine with a reason; it does not silently overwrite the source.
from dataclasses import dataclass
@dataclass(frozen=True)
class ImageFacts:
source_sha256: str
width: int
height: int
decoded_pixels: int
orientation_applied: bool
background_mode: str
encoder: str
quality: int
def admissible(facts: ImageFacts, max_pixels: int = 40_000_000) -> bool:
return (
facts.width > 0
and facts.height > 0
and facts.decoded_pixels <= max_pixels
and facts.orientation_applied
and facts.background_mode in {"keep", "white", "transparent"}
and 1 <= facts.quality <= 100
)
That 40-million-pixel value is a policy example, not a universal standard. Tune it against decoder memory and the largest legitimate menu board; your mileage may vary.
How do background cleanup, lifecycle validation, and compression interact?
They form a state machine, not three independent jobs. received -> decoded -> oriented -> cleaned -> encoded -> published makes it possible to ask which transition produced a bad asset. Idempotency comes from the source hash and a transformation specification hash, so a retry writes the same derivative key instead of another anonymous object.
The cache should point at a versioned derivative, never at a mutable filename such as menu.jpg. Publish a manifest containing dimensions, media type, byte length, and an integrity digest. A CDN can then cache immutable URLs while a catalog update switches the manifest atomically.
I once treated a successful encoder exit code as validation. That was too weak: a truncated upload can still decode a small preview, and a color-profile mismatch can pass a byte-level check. The useful test is a decode-after-write check on the stored derivative, followed by a perceptual spot check for transparency halos and text loss. In a real onboarding batch, I would retain the failing object's source hash, decoder status, and transformation specification, then replay the exact specification against a clean copy. If the replay differs, the problem is deterministic data or configuration; if it matches but the visual review still fails, the acceptance rule is incomplete. That distinction matters because deleting and retrying without those records turns an intermittent-looking issue into an untraceable replacement image, and the merchant sees only that yesterday's dish looked right while today's card has a gray fringe.
Retention is an engineering choice, not a cleanup cron
Keep originals during the merchant dispute and moderation window; retain derivatives according to traffic and regeneration cost. Delete orphaned derivatives only after a mark-and-sweep job has compared object keys with live catalog manifests. Measure bytes by state, not just total bucket size, so the team can see whether quarantine or abandoned originals is growing.
The catch is operational: retaining every source forever simplifies reprocessing but increases privacy, legal, and storage exposure. A short retention window is unsuitable when merchants need historical evidence for a takedown appeal. In that case, keep a content digest and an access-controlled archive, then stick with a shorter hot-storage period for serving derivatives.
| Decision | Helps | Cost or risk |
|---|---|---|
| Keep lossless source | Reprocessing and dispute evidence | Highest retained bytes |
| Store one high-quality derivative | Lower serving and cache bytes | Less room for future crops |
| Generate format variants on demand | Fewer idle objects | CPU spikes and cold misses |
| Immutable versioned URLs | Predictable cache behavior | Manifest updates are required |
Compression should follow display constraints. Use a modern browser-supported format where it improves bytes, but preserve a fallback when a client or moderation tool cannot decode it; MDN's media format guidance is a useful compatibility starting point. Quality is not a single magic number. Test representative dishes with fine text, steam, and dark sauces, then set separate budgets for thumbnails and zoom views.
Failure modes worth rehearsing
A retry storm can multiply objects if keys include timestamps. A background model can erase a pale plate edge. A lifecycle rule can delete the only source before a derivative is proven decodable. A CDN can serve an old image after a merchant correction if the URL is mutable.
Measure twice.
Run these as table-top exercises with counters: quarantine rate, decode-after-write failures, derivative bytes per published image, cache hit ratio, and age of unreferenced objects. Alert on trends rather than a single noisy upload. I am not sure any one metric captures perceived food quality, so pair byte metrics with a small human review sample and record the reviewer rubric.
This design is intentionally vendor-neutral. Libraries and object stores differ in decoder coverage, lifecycle granularity, and event semantics; the interface should hide those differences while keeping the manifest portable. Choose a service with the consistency and retention controls your dispute policy requires, and reject one that cannot expose integrity metadata or deterministic object keys.
Top comments (0)