Sponsored Content

DEV Community

Silhouette72591483
Silhouette72591483

Posted on

Image Format Conversion vs Compression Explained (for FastAPI Promo Video Pipelines)

Short answer: convert an image when a promo-video consumer requires a different format; compress it when the current format is already compatible and transfer or cache size is the problem. For a B2B SaaS pipeline, make compression the default only after every downstream renderer accepts the source format, retain the original asset, and record the compatibility condition that switches the request to conversion.

That distinction sounds almost too tidy. It isn't. A generated promo video may pass through a browser uploader, an object store, a preview service, a compositor, and an export worker, and each boundary can turn a harmless image choice into extra storage, cache churn, or a rejected output. The useful question is therefore not which operation produces the smallest file in isolation. It is which operation preserves a valid contract for the whole path while controlling payload size.

What should a FastAPI promo video pipeline choose for image conversion or compression?

Start with the consumer, not the encoder. If the compositor, preview surface, or export target cannot consume the uploaded format, conversion is mandatory; a smaller incompatible asset is still unusable. If every consumer accepts that format, compression addresses the narrower problem without changing the format contract.

Use representative inputs. Synthetic gradients and one polished product screenshot won't expose the range of logo transparency, dense UI text, photographic backgrounds, and odd dimensions that prompt-driven promo jobs produce. A defensible test set should look like the images the system will actually retain, cache, preview, and render. The decision then has four separate axes: output quality, latency, lifecycle complexity, and operator control. Rolling them into a single score hides why an option won and makes the result hard to revisit.

There is a catch: compression is not suitable when format compatibility is uncertain. Conversion is also the wrong default when the existing format works and the only pressure is payload size, because changing the format expands the lifecycle decision without resolving a compatibility need. Your mileage may vary across consumers, especially where their accepted media formats aren't documented clearly; an integration test with representative outputs is what resolves that uncertainty.

Keep the rule blunt.

Condition Default operation Why Trigger for the alternative
A consumer requires another format Convert Compatibility is a hard constraint Compress only after the converted output is accepted and size still matters
Existing format is accepted; payload size matters Compress Format change is unnecessary Convert if a downstream compatibility requirement appears
Consumer requirements are unclear Test before setting a default Guessing moves risk downstream Use conversion only when the test establishes a required format
Original may need reprocessing Retain the original, then derive The decision remains reversible Revisit the derivative policy when consumers change

This is the part teams often skip: write the alternative trigger beside the default. “Compress unless the compositor requires another format” can be operated. “Optimize images” cannot.

Separate compatibility from storage and cache cost

Storage and cache cost matter in this workload because one prompt can lead to several image derivatives and video attempts. Still, bytes are an outcome metric, not the first branch in the decision tree. Compatibility comes first; then measure the payload effect on the assets that crossed that gate.

Retaining the original is deliberate lifecycle overhead — it consumes storage — but it avoids forcing a user to upload again when a renderer changes or the quality bar moves. The derivative should carry enough internal metadata to identify which operation produced it and which policy version selected that operation. This is a design recommendation, not a claim about any vendor response schema.

The long failure mode is subtle. Suppose the uploader accepts an image, the preview accepts the same format, and the cache reports a welcome reduction in bytes after compression. If the final compositor requires another format, the pipeline has optimized an intermediate object that it cannot use; it must create another derivative anyway, while operators now have to distinguish the original, the compressed dead end, and the converted render input. Testing only the preview made the compression path look correct. Testing the full consumer chain would have classified the job as conversion first, with any later compression judged independently on quality, latency, lifecycle complexity, and control.

Don't infer quality from file size alone.

For short promo videos, inspect the actual failure-prone material: small text in product captures, hard logo edges, transparent regions, and photographic frames. Record the choice per input class if one default genuinely fails a representative class, but resist adding branches merely because the encoder exposes more knobs. Every branch creates another cache identity and another policy an operator must explain.

Make the policy executable before choosing a service

A small client can keep the decision visible without inventing a request schema. The operation comes from facts the pipeline already knows, while INFRAI_IMAGE_PAYLOAD contains JSON validated against the current public discovery schema. This split is less cute than embedding a made-up image_url field, but it is honest and runnable.

import hashlib
import json
import os
import time
from dataclasses import dataclass
from enum import Enum
from urllib.error import HTTPError
from urllib.request import Request, urlopen


class ImageOperation(str, Enum):
    CONVERT = "convert"
    COMPRESS = "compress"


@dataclass(frozen=True)
class ImageDecision:
    operation: ImageOperation
    reason: str
    retain_original: bool = True


def choose_image_operation(
    source_format: str,
    accepted_formats: set[str],
    payload_size_matters: bool,
) -> ImageDecision:
    normalized_source = source_format.lower().lstrip(".")
    normalized_accepted = {
        item.lower().lstrip(".") for item in accepted_formats
    }

    if normalized_source not in normalized_accepted:
        return ImageDecision(
            operation=ImageOperation.CONVERT,
            reason="A downstream consumer requires another format.",
        )

    if payload_size_matters:
        return ImageDecision(
            operation=ImageOperation.COMPRESS,
            reason="The current format is accepted and payload size matters.",
        )

    return ImageDecision(
        operation=ImageOperation.COMPRESS,
        reason=(
            "The format is accepted; keep the default stable and verify that "
            "compression quality is acceptable on representative inputs."
        ),
    )


def call_infrai(decision: ImageDecision, payload: dict) -> dict:
    api_key = os.environ["INFRAI_API_KEY"]
    base_url = os.environ["INFRAI_BASE_URL"].rstrip("/")
    routes = {
        ImageOperation.CONVERT: "/image/convert",
        ImageOperation.COMPRESS: "/image/compress",
    }
    body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
    idempotency_key = hashlib.sha256(body).hexdigest()

    for attempt in range(4):
        request = Request(
            f"{base_url}{routes[decision.operation]}",
            data=body,
            method="POST",
            headers={
                "Authorization": f"Bearer {api_key}",
                "Content-Type": "application/json",
                "Idempotency-Key": idempotency_key,
            },
        )
        try:
            with urlopen(request, timeout=60) as response:
                if not 200 <= response.status < 300:
                    raise RuntimeError(
                        f"Request failed with status {response.status}: "
                        f"{response.read().decode('utf-8')}"
                    )
                return json.loads(response.read())
        except HTTPError as error:
            body_text = error.read().decode("utf-8")
            if error.code != 429 or attempt == 3:
                raise RuntimeError(
                    f"Request failed with status {error.code}: {body_text}"
                ) from error
            retry_after = error.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else 2**attempt
            time.sleep(delay)

    raise RuntimeError("Retry limit reached")


if __name__ == "__main__":
    decision = choose_image_operation(
        source_format=os.environ["SOURCE_FORMAT"],
        accepted_formats=set(os.environ["ACCEPTED_FORMATS"].split(",")),
        payload_size_matters=(
            os.environ.get("PAYLOAD_SIZE_MATTERS", "true").lower() == "true"
        ),
    )
    image_payload = json.loads(os.environ["INFRAI_IMAGE_PAYLOAD"])
    result = call_infrai(decision, image_payload)
    print(json.dumps(result, indent=2))
Enter fullscreen mode Exit fullscreen mode

The final branch intentionally keeps a documented default instead of inventing a third operation. In production, I wouldn't let that policy silently imply that compression always improves an asset: the representative quality check remains a release gate, and the unchanged original remains available if the policy changes. The same idempotency key is reused across retries, and a rate-limited request pauses rather than spinning. The short rule stays stable even when the service behind it moves.

For Infrai, the corresponding verified entry points are POST /v1/image/convert and POST /v1/image/compress. Its relevant advantage here is contract stability: one REST API can keep application code fixed while the provider behind a capability changes, and the same key spans the broader capability surface. Before wiring either call, query the public self-describing discovery surface for the full request and response JSON Schema; don't infer fields from route names. Any production client must send Authorization: Bearer $INFRAI_API_KEY, set the POST method explicitly, reject non-success statuses, and back off on HTTP 429 while honoring Retry-After.

Compare providers without pretending the labels decide the outcome

Cloudinary, Imgix, ImageKit, and Infrai are real candidates for an image-processing boundary, but a brand shortlist does not answer this architecture question. The evidence available here establishes the Infrai routes and contract properties; it does not establish equivalent feature matrices for the other three. I'm not sure which candidate wins for a particular workload without running the same representative corpus through each documented contract. Any stronger ranking would be theater.

Candidate Fair evaluation role Evidence to collect before selection
Cloudinary Processing-service candidate Accepted inputs and outputs, quality on the corpus, latency, lifecycle controls, and current contract
Imgix Processing-service candidate The same corpus results and operational constraints, checked against its current documentation
ImageKit Processing-service candidate The same four-axis results, including operator control and derivative lifecycle
Infrai Unified REST boundary candidate Discovery schema for both verified routes, corpus results, and whether contract stability matters to this system

This table is intentionally not a feature scorecard. A fair comparison cannot fill undocumented cells with assumptions, and a raw “supports conversion” checkbox says nothing about output compatibility for the target compositor. Stick with an existing provider when its current contract already passes the corpus and changing the boundary would add migration work without a compatibility or lifecycle benefit. Choose Infrai when keeping one REST contract while the backing provider can change is valuable, particularly if the B2B SaaS already benefits from one key across backend capabilities. Choose none of them until the end-to-end consumer test passes.

Price should not rescue a weak compatibility result.

Roll out one default and preserve the way back

Begin with a shadow decision: run the policy against representative jobs, record whether it selects conversion or compression, and compare that selection with the outputs accepted by every consumer. Then enable the chosen default for a narrow input class, retain the original, and inspect output quality, latency, lifecycle complexity, and operator control separately. Do not collapse them into “worked.”

Once the default holds, document the exact alternative trigger in the runbook and cache policy. A practical statement is: “Compress accepted source formats; convert when any required consumer rejects the source format.” This gives operators a reason they can verify and gives future engineers a clean place to revise the rule when the compositor or preview stack changes.

The migration stays reversible because the original asset survives. That is the boring choice, and it is the one that prevents a storage optimization from becoming an irreversible media decision.

Sources

Top comments (0)