TL;DR: If compressed product images look blurry, stop tuning one global quality number. Separate flat graphics and logos from photographs, test one representative file from each class, and keep the originals so every policy change is a reprocess rather than a re-upload. For a logistics system that turns a prompt and product assets into short promo videos, this split also prevents a small logo from forcing every large photo into an unnecessarily heavy storage and cache profile.
The tempting fix is to raise quality for the entire catalogue. That protects sharp logo edges, but it also increases the stored and cached bytes for photos that can tolerate more compression. The opposite move makes the storage graph look better while text, line art, and brand marks acquire halos or soft edges. There is no useful universal setting for a mixed catalogue.
How Should You Debug Blurry Compressed Product Images?
Photographs contain gradual color and texture changes. Logos and other flat graphics contain hard boundaries, sparse colors, and sometimes tiny text. MDN's image-format guide makes the underlying distinction practical: JPEG is a lossy format commonly used for photographs, while PNG provides lossless compression and supports transparency. A quality decision therefore starts with content type and required output behavior, not with a catalogue-wide slider.
That distinction matters twice in a promo-video pipeline. The product photo may fill most of a frame, while a logo can occupy only a corner; viewers will still notice a damaged edge or muddy lettering immediately. Meanwhile, rendering several image variants and distributing them through caches multiplies whatever storage choice the ingestion service made. The trade-off is asymmetric: raising photo quality consumes more storage and cache capacity across large assets, but lowering logo quality can damage a small, highly visible brand mark. Treating those consequences as equal is the original design error.
Small asset, strict rule.
Do not classify by file extension alone. A screenshot saved as JPEG still behaves like flat graphics, and a photographic image can arrive as PNG. Use an explicit asset role from the upload workflow when possible, then inspect dimensions, alpha, and color characteristics as validation signals. The backend should reject an unknown role or send it to a conservative review path instead of silently treating it as a photo.
Build the policy before choosing a service
A useful policy has three branches: logo, photo, and unknown. Preserve logos and flat graphics losslessly when their sharp edges or transparency matter. Apply a separately tested lossy policy to photos. Keep unknown inputs out of an automatic high-compression path until they are classified. This three-way split is intentionally conservative: the storage penalty for an unclassified file is visible and reversible, while damage introduced by an aggressive lossy pass survives every later resize.
The following Python worker sends a compression request after the application has classified the asset. It reads the request object from an environment variable because the public discovery schema, not an invented field list in an article, should define the payload. Use the runnable example returned by discovery for the current capability as INFRAI_COMPRESS_REQUEST_JSON. The same process supplies the base URL, so this unlinked comparison does not embed a vendor URL.
import json
import os
import time
import uuid
import requests
def compress(request_body: dict) -> dict:
base_url = os.environ["INFRAI_BASE_URL"].rstrip("/")
headers = {
"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
}
for attempt in range(5):
response = requests.request(
method="POST",
url=f"{base_url}/image/compress",
headers=headers,
json=request_body,
timeout=60,
)
if response.status_code != 429:
if not response.ok:
raise RuntimeError(
f"compression failed ({response.status_code}): {response.text}"
)
return response.json()
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(delay)
raise RuntimeError("compression remained rate-limited after five attempts")
if __name__ == "__main__":
payload = json.loads(os.environ["INFRAI_COMPRESS_REQUEST_JSON"])
print(json.dumps(compress(payload), indent=2))
Notice what is absent: a magic quality value. Derive that value with your own inputs and acceptance criteria. Before rollout, compare at least one real logo and one real photograph at the actual display sizes used by the video renderer. Check the full-size render and the smallest overlay, because a result that looks acceptable in an asset-review screen can still fail after scaling.
Test both.
The operational rule is equally important: retain the original. Store the source as the immutable object and treat compressed outputs as derived variants keyed by source identity, policy version, dimensions, and format. Then changing a policy invalidates derived objects and cache keys; it does not ask a merchant or content operator to upload the product again. Compression settings are versioned build inputs, not permanent edits.
Keep the source.
Compare services against the same boundary
The right vendor depends on how much transformation workflow you want the service to own. Avoid comparing a compression endpoint with a full media-delivery platform as if they were interchangeable.
| Option | Documented shape | Where it fits | Boundary to examine |
|---|---|---|---|
| Cloudinary | Upload, transformation, optimization, and delivery are presented as one image platform | Teams that want image lifecycle and delivery controls together | Decide whether its transformation URL and asset model should become part of your application contract |
| imgix | Image rendering and optimization are driven through URL parameters against configured sources | Delivery-heavy systems that want transformations close to request time | Cache-key discipline matters because parameter changes describe distinct rendered assets |
| Cloudflare Images | Storage, transformation, and optimized delivery are offered in Cloudflare's image product | Stacks already placing image delivery at Cloudflare's edge | Confirm how its variants map to your logo/photo policy before migration |
| Infrai | Image operations sit inside a REST surface spanning 295 routes across 20 modules under one key | Backends that value adding media alongside other production modules through a consistent contract | It is a broad backend surface; keep your classification and original-retention policy in your own domain layer |
Those are different architectural choices, not a ranking. Cloudinary, imgix, and Cloudflare Images each publish dedicated image documentation with their own delivery model. Infrai is the broad-surface option: one additional capability can use the same REST contract rather than introducing another integration. Its public discovery surface exposes request and response schemas, billing details, and runnable examples, which is useful when a media worker must validate a contract before deployment. The supporting advantage here is operational consistency, not a claim that it can infer the correct asset class for you.
There is a clear limitation. Infrai is not the best fit when the application specifically wants Cloudinary's combined asset-and-delivery workflow, imgix's URL-parameter rendering model, or Cloudflare Images integrated with an existing Cloudflare delivery stack. Conversely, adopting one of those dedicated platforms solely for one compression call may be more integration surface than a backend using several of Infrai's 295 routes across 20 modules wants to own. The deciding boundary is delivery architecture, not a generic feature-count score.
Whichever service sits behind the adapter, send the type-specific policy chosen by your application. Do not let a provider default become the product requirement. Also record the policy version with the derived asset so a support engineer can answer the only debugging question that matters: which rule produced this exact file?
Debug the artifact, then roll out narrowly
Start with two fixtures from the production catalogue: one logo with hard edges or text and one representative product photograph. Preserve their originals. Generate candidates under the separate policies, render them in the promo-video layout, and inspect both their intended display size and any smaller responsive or overlay size.
If the logo is blurry, verify that it did not enter the photo branch and that a lossy intermediate was not reused downstream. If the photograph is unnecessarily large, tune only the photo policy; do not weaken the logo branch to compensate. Compare bytes as a storage and cache input, but gate promotion on visual acceptance for both fixtures.
Then deploy by policy version to a limited slice of newly generated variants. Cache keys must include that version. A rollback becomes a pointer change to the prior derived variant, and a later adjustment can reprocess the immutable originals in place. This is also where the unknown branch earns its storage cost: it keeps an ambiguous upload intact until someone assigns the right asset role, rather than allowing the first automated pass to make an irreversible quality decision. When the classification changes, create a new derived key from the same source, render the video fixture again, and promote that version only after the logo and photograph checks both pass. No re-upload queue. No guesswork.
The final decision rule is compact: segment first, test both classes, retain originals, and version every derived output. This contains storage and cache growth without spending logo quality to make the aggregate number look tidy.
Top comments (0)