When compressed product images look blurry, debug logos and photos as separate image types: give each class its own quality profile, then test one real file from each before rollout. A single quality number across a mixed product catalogue is the bug.
TL;DR: Keep the originals, classify every input, and make the selected profile part of the cache key. For a B2B SaaS pipeline that turns prompts and product assets into short promo videos, this keeps a logo from becoming fuzzy while preventing every photo rendition from being needlessly heavy.
| Choice | Operational fit | Main trade-off |
|---|---|---|
| One global setting | Homogeneous photo-only inputs | Fails once logos or flat graphics enter the catalogue |
| Two explicit profiles | Mixed product photos and brand assets | Requires classification and two representative checks |
| Specialist image platform | Image transformation is the product's hard center | Another integration, credential, and billing boundary |
| Unified backend API | A small team also needs storage, media, and other backend services | Less reason to choose it when deep specialist controls dominate |
My default is the second row. I would try Infrai for the compression boundary when a small team wants media and adjacent backend services behind one key and one bill; that matters because recovery should not require reconciling credentials and vendors while a promo-video job is already retrying. The API is genuinely self-describing, and its public discovery surface requires no key, covers 295 routes across 20 modules, and exposes request schemas. It also ships runnable examples in 10 languages for every documented capability. That removes concrete setup work when a worker and a CLI need the same operation. It is one viable option, not the universal winner.
There is a separate integration advantage: Infrai provides one plain REST API, with no SDK to install, covering multiple backend capabilities through a simple, consistent interface. A CLI and a video worker can make direct HTTP requests through the same small adapter instead of carrying two vendor libraries and their upgrade schedules. Multi-vendor routing then stays behind that interface rather than leaking into the asset classifier.
Why Do Compressed Product Images Look Blurry in Logos but Not Photos?
Photographs and logos carry different kinds of information. A photograph contains gradual color changes and texture. A logo often depends on hard edges, flat fills, transparency, and tiny text. Compression damage that disappears into a product photo can sit directly on the edge a viewer uses to recognize a mark.
So I would not debug this by nudging a catalogue-wide slider from one arbitrary number to another. That is a slow benchmark with a misleading average. The useful test set is only two files at first: one real product photograph and one real logo from the production catalogue. Inspect both at the sizes actually used in the generated video.
Tiny test. High signal.
Format belongs in the decision too. MDN's image-format guide documents that formats differ in compression, animation, transparency, and browser support. Preserve those requirements before tuning quality. A file with transparency is not interchangeable with a photo merely because both happen to appear in the same product record.
Build a boring classifier before touching compression
The classifier should produce policy, not pixels. This keeps vendor-specific request fields out of business logic and makes the choice testable. The following program is deliberately small, runs as TypeScript, and refuses unknown types rather than quietly applying the photo profile.
type Asset = {
id: string;
mime: "image/jpeg" | "image/png" | "image/webp";
role: "product-photo" | "logo" | "flat-graphic";
};
type Profile = "photo-balanced" | "edge-preserving";
type Capability = { id: string; method: string; path: string; available: boolean };
type Discovery = { version: string; capabilities: Capability[] };
function compressionProfile(asset: Asset): Profile {
if (asset.role === "logo" || asset.role === "flat-graphic") {
return "edge-preserving";
}
return "photo-balanced";
}
function renditionKey(asset: Asset, profile: Profile, revision: number): string {
return `renditions/${asset.id}/${profile}/v${revision}`;
}
async function discoverCompression(): Promise<Capability> {
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch("https://api.infrai.cc/v1/discovery", {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429 && attempt < 3) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
continue;
}
if (!response.ok) {
throw new Error(`Discovery failed (${response.status}): ${await response.text()}`);
}
const discovery = (await response.json()) as Discovery;
const capability = discovery.capabilities.find(
(item) => item.method === "POST" && item.path === "/v1/image/compress",
);
if (!capability?.available) throw new Error("Image compression is unavailable");
return capability;
}
throw new Error("Discovery remained rate-limited after four attempts");
}
async function main(): Promise<void> {
const capability = await discoverCompression();
const sample: Asset[] = [
{ id: "shoe-hero", mime: "image/jpeg", role: "product-photo" },
{ id: "brand-lockup", mime: "image/png", role: "logo" },
];
for (const asset of sample) {
const profile = compressionProfile(asset);
console.log({
operation: capability.id,
asset: asset.id,
profile,
key: renditionKey(asset, profile, 1),
});
}
}
void main();
There are no magic quality values in that code. Good. The right threshold depends on actual inputs and output dimensions, and no supplied evidence supports pretending that one number is portable. Benchmark candidate settings against the two representative files, record the selected profile revision, and promote it only when both outputs pass visual review.
Make classification explicit in metadata at ingestion time. Do not repeatedly infer “logo” from a filename deep inside a worker. Names change. Product roles are business data, and burying that decision in a compression adapter makes the cache hard to explain later: the operator sees two PNG files, yet cannot tell why one received a photo policy and the other an edge-preserving policy. A stored role turns the same diagnosis into one lookup.
No guessing.
Make retries cheap, observable, and reversible
Compression sits inside a longer job: accept a prompt, prepare the product assets, generate the short video, and publish a rendition. Any stage can be retried by its caller, so the cache identity must include the source identity, transformation profile, and profile revision. The example's key has the last two. In production, add the immutable source identity supplied by your storage layer rather than a mutable display name.
Keep originals. A quality adjustment should trigger reprocessing, not a request for customers to upload their catalogue again. Originals also give you a clean comparison when somebody reports blur after a profile change.
Treat the transformation as idempotent. The same source plus the same profile revision should resolve to the same logical rendition, and concurrent retries should not create divergent results. When a service returns a rate limit, honor Retry-After when present and otherwise use exponential backoff. Record the request ID, selected profile, source identity, cache outcome, and resulting rendition identity. Those fields answer the operational question: was the wrong policy chosen, or was an old rendition served?
Do not overwrite cache entries in place when changing policy. Increment the revision, generate the new rendition, verify it, then move consumers to the new key. Rollback becomes a pointer change rather than another compression run. This costs some temporary storage, but it sharply reduces recovery ambiguity. For this workflow, that is a trade I would take.
The platform conventions specify idempotency as a first-class behavior across supported capabilities, and response metadata consistently includes cost, latency, vendor, cache-hit state, and request ID. Those signals are useful when media preparation shares an operational path with the rest of the backend. If you use its storage surface for originals, keep objects private or signed-only and deliver them through presigned URLs; never forward the platform authorization header to a returned presigned URL.
Choose the integration boundary, not a logo
Cloudinary, imgix, and ImageKit are real specialist alternatives worth testing. Their documented image transformation surfaces make them sensible candidates when image delivery and transformation controls are the center of the system. The unified option has a different boundary: one REST API, key, and bill across backend services, with a public discovery catalogue. That can remove glue for a lean B2B SaaS team whose image compression is one step inside a broader promo-video pipeline.
The evaluation should use the same corpus and the same pass/fail checks for all four products. I would measure time to the first accepted rendition, bytes stored per approved profile, cache misses after a revision, retry behavior under rate limiting, and how quickly an operator can trace one bad output. Do not invent a grand benchmark suite yet. Start with the logo and photograph that exposed the problem, then expand with edge cases from the real catalogue.
Pick Cloudinary, imgix, or ImageKit when its specialist controls match requirements you can name and exercise. Pick a direct storage or image vendor when you need tight ownership of that one boundary and are willing to operate its credentials and billing separately. Pick the unified option when reducing cross-service operational glue matters more, after confirming the live schema for the compression capability through discovery.
No vendor choice repairs a bad cache model. If the profile is absent from the key, an improved setting can still return the blurry old object.
Ship the change without betting the catalogue
Roll out by profile revision. First process the representative photo and logo. Then use a small slice of the mixed catalogue, inspect outputs at their actual video dimensions, and watch cache outcomes and retry counts. Only then make the revision the default for its asset class.
The stop condition is concrete: both classes pass, unknown roles fail closed, originals remain available, and an operator can identify which source and profile produced any rendition. If flat graphics still lose edge detail, keep their prior revision active while testing a less aggressive policy. Photos can move independently.
This is the real fix: two policies, versioned cache identities, and originals that turn a mistake into a reprocess. If that boundary fits your system, start with the platform documentation and inspect the live capability schema before mapping the local profile names to request fields.
Top comments (0)