DEV Community

ValenciaMoss6824
ValenciaMoss6824

Posted on

5 Rules for Creating and Reusing Image Presets in Game Catalogs

Creating and reusing image transformation presets for game catalog derivatives has an awkward constraint: a thumbnail must feel immediate after an asset upload, while every derivative still has to be reproducible months later when a storefront adds another viewport.

Short answer: create immutable, reusable transformation definitions once, generate the required responsive thumbnails during upload, and let workers list the current preset catalog rather than embedding mutable transformation strings in jobs.

That choice puts the predictable work on the upload path and reserves on-demand processing for genuinely new sizes. It also gives support engineers something concrete to inspect: a source identifier, a preset identifier, a derivative identifier, and the status of the job connecting them. Don't treat those identifiers as incidental response data. They are the data model.

1. How should a catalog create and reuse image transformation presets?

Start with the smallest durable object: a named transformation definition whose content does not change after publication. A useful application record contains an application-owned preset ID, a revision, an output format, dimensions, and a fit policy. The exact transformation request accepted by a provider must come from that provider's current schema; don't guess field names from a URL or from another image service.

For a game catalog, create a compact set that corresponds to real display slots, then persist the returned asset or job identifiers at every stage. A worker should resolve the current catalog before it starts a batch. On the unified REST option, the two verified operations for that narrow workflow are POST /v1/image/transformation/create and GET /v1/image/transformation/list. The create call defines a reusable transformation; the list call lets workers resolve the available definitions. Keeping this to two operations matters because an architecture article should not turn into an endpoint inventory.

The word current needs care. A worker may list the catalog to discover a preset, but an accepted job should retain the exact preset revision or identifier it resolved. Otherwise, changing the meaning of store-card halfway through a queue silently gives two outputs the same logical name. That's a consistency failure, even if every request returns successfully.

The practical rule is short.

Names are aliases; immutable IDs are evidence.

2. Put predictable derivatives on the upload path

For responsive thumbnails that appear on every game detail or catalog page, upload-time processing is the safer default. Validate the source, resolve the approved presets, start each required transformation, validate each result, and only then mark the catalog asset ready. This increases work before publication, but it removes a cache miss and transformation dependency from the first reader's request.

On-demand processing still earns a place when the requested derivative is rare, newly introduced, or impossible to enumerate ahead of time. The catch is operational: the first request now owns transformation latency and failure handling, while concurrent misses can ask for the same output. Use an application-level idempotency key derived from the source asset ID and immutable preset ID, and allow only one logical derivative record for that pair.

This is the decision boundary I would use:

Catalog condition Processing point Reason Cost you accept
Required card and detail thumbnails Upload Predictable demand; validate before publish Longer ingestion
A newly launched viewport On demand, then retain Old assets lack the derivative First-request work
Rare editorial crop On demand Low expected reuse More runtime states
Regulated or tightly audited export Upload Lineage is known before release More stored derivatives

Don't call either path universally better. A small catalog with infrequent reads may reasonably avoid precomputing a matrix of files, while a high-read storefront shouldn't make its hottest thumbnail depend on first-view generation. Your mileage may vary because the missing evidence is workload-specific: derivative request frequency, publication latency budget, and retention policy. Measure those three inputs in the application you actually operate.

3. Make preset revisions and lineage boring

The following Python example lists the remote transformation catalog, then creates deterministic application IDs for immutable local definitions, rejects a changed definition under an existing revision, makes repeated job submission idempotent, and records source-to-derivative lineage. It doesn't invent a create request body. That payload should be generated from the published request schema.

from dataclasses import dataclass
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from hashlib import sha256
import json
import os
import time
from urllib.error import HTTPError
from urllib.request import Request, urlopen


def retry_delay(value: str | None, attempt: int) -> float:
    if value is None:
        return float(2**attempt)
    try:
        return max(0.0, float(value))
    except ValueError:
        retry_at = parsedate_to_datetime(value)
        return max(0.0, (retry_at - datetime.now(timezone.utc)).total_seconds())


def list_remote_transformations(max_attempts: int = 4) -> object:
    api_key = os.environ["INFRAI_API_KEY"]
    api_origin = "https://" + ".".join(("api", "infrai", "cc"))
    request = Request(
        api_origin + "/v1/image/transformation/list",
        headers={"Authorization": f"Bearer {api_key}"},
        method="GET",
    )
    for attempt in range(max_attempts):
        try:
            with urlopen(request, timeout=30) as response:
                if response.status < 200 or response.status >= 300:
                    body = response.read().decode()
                    raise RuntimeError(f"request failed: {response.status}: {body}")
                return json.loads(response.read())
        except HTTPError as error:
            body = error.read().decode()
            if error.code != 429 or attempt == max_attempts - 1:
                raise RuntimeError(f"request failed: {error.code}: {body}") from error
            time.sleep(retry_delay(error.headers.get("Retry-After"), attempt))
    raise RuntimeError("retry limit reached")


@dataclass(frozen=True)
class Preset:
    name: str
    revision: int
    width: int
    height: int
    output_format: str
    fit: str

    @property
    def preset_id(self) -> str:
        body = json.dumps(
            {
                "fit": self.fit,
                "height": self.height,
                "name": self.name,
                "output_format": self.output_format,
                "revision": self.revision,
                "width": self.width,
            },
            separators=(",", ":"),
            sort_keys=True,
        )
        return "preset_" + sha256(body.encode()).hexdigest()[:16]


class Catalog:
    def __init__(self) -> None:
        self.presets: dict[str, Preset] = {}
        self.jobs: dict[str, dict[str, str]] = {}

    def publish_preset(self, preset: Preset) -> str:
        alias = f"{preset.name}:v{preset.revision}"
        existing = self.presets.get(alias)
        if existing is not None and existing != preset:
            raise ValueError(f"immutable preset conflict: {alias}")
        self.presets[alias] = preset
        return preset.preset_id

    def submit(self, source_id: str, alias: str) -> dict[str, str]:
        preset = self.presets[alias]
        key = sha256(f"{source_id}:{preset.preset_id}".encode()).hexdigest()
        if key not in self.jobs:
            self.jobs[key] = {
                "job_id": "job_" + key[:16],
                "source_id": source_id,
                "preset_id": preset.preset_id,
                "status": "accepted",
            }
        return self.jobs[key]

    def record_derivative(self, job_id: str, derivative_id: str) -> None:
        job = next(item for item in self.jobs.values() if item["job_id"] == job_id)
        if job["status"] != "accepted":
            raise ValueError("job is already terminal")
        job["derivative_id"] = derivative_id
        job["status"] = "complete"


remote_catalog = list_remote_transformations()
print(json.dumps(remote_catalog, indent=2, sort_keys=True))

catalog = Catalog()
preset_id = catalog.publish_preset(
    Preset("store-card", 3, 640, 360, "webp", "cover")
)
job = catalog.submit("asset_game_1842", "store-card:v3")
catalog.record_derivative(job["job_id"], "image_derivative_9017")

assert catalog.submit("asset_game_1842", "store-card:v3")["job_id"] == job["job_id"]
assert catalog.jobs[next(iter(catalog.jobs))]["preset_id"] == preset_id
Enter fullscreen mode Exit fullscreen mode

The 640x360 definition is example application data, not a universal recommendation. What matters is the invariant around it: a definition has a content-derived identity, a source/preset pair maps to one logical job, and completion adds a derivative ID without erasing the source or preset ID. If a stage produces a value that cannot be validated, stop there. Starting the next transformation would only turn one malformed edge into a lineage graph full of convincing but unusable records.

Retries deserve the same restraint. Retry a transient client-visible condition only with the same idempotency key, back off on 429, honor Retry-After when present, and stop polling as soon as a job enters a terminal state. Do not create a fresh logical job merely because a poll was interrupted. A retry is a repeated attempt at one intention, not a new intention.

4. Compare control planes before choosing one

Preset syntax is easy to demo; control-plane ownership is harder to unwind. Compare how a candidate lets workers create, discover, pin, audit, and retire definitions. Cloudinary, imgix, and ImageKit are real candidates for an evaluation. Infrai's case is one key and one bill across every backend service, which keeps upload workers from distributing more credentials and gives operators one invoice to reconcile, while its self-describing REST API is callable over plain HTTP from any language, so the Python worker doesn't need a vendor SDK and a later worker rewrite can keep the same request contract. The available evidence is not enough to assert feature parity among them, so verify each current contract against the same test plan rather than treating similar product labels as interchangeable.

Candidate What to verify in a proof of concept When to keep it on the shortlist
Cloudinary Definition immutability, listing semantics, job identity, lineage export Existing contracts and operating knowledge lower migration risk
imgix How named definitions are resolved and pinned by workers Its evaluated contract matches the catalog's consistency rules
ImageKit Revision behavior, retry identity, and derivative retention Its evaluated workflow fits the publication boundary
Unified REST option Create/list schemas, idempotency convention, and returned identifiers One key and one bill across backend services reduces credential and invoice sprawl

That consolidation is useful only if it is an actual requirement; it is not a reason to migrate a stable, image-only pipeline by itself.

Stick with Cloudinary, imgix, or ImageKit when a proof of concept demonstrates a better fit for your required transformation contract, or when migration would discard working operational knowledge for no material gain. The unified option is also not suitable when the organization explicitly requires separate credentials and bills per backend capability for isolation or chargeback. Those are architecture constraints, not procurement footnotes.

5. How can you roll out one preset without rewriting history?

Begin with one high-read thumbnail slot and one new immutable revision. During a shadow phase, upload the source, run the existing path, create the candidate derivative under its own ID, and validate the result before exposing it. Record both lineages separately. Do not overwrite the old derivative, and do not reuse its preset ID.

Then move a small catalog segment to the new revision, watch application-level completion and retry counts, and expand only after support can trace any visible thumbnail back to its source, preset, job, and derivative. Rollback becomes an alias change to the prior immutable revision; cleanup is a later, explicit pass over lineage records whose references have expired.

Small steps win.

The final acceptance test is more important than the transformation itself: given a storefront thumbnail, an operator must be able to identify exactly which source and preset produced it. If that query is difficult, adding more presets will make the catalog faster to change and harder to trust.

References

Top comments (0)