Print-on-demand artwork needs metadata checks before conversion. Print files fail in production for boring reasons: a 72 DPI export, a CMYK file sent to an RGB-only step, or a 4,000-pixel image asked to become a 12,000-pixel poster. Those failures are cheap to detect and expensive to discover after a conversion job has consumed storage, queue time, and a fulfillment slot.
Bad input, early.
Short answer: validate image metadata against the target print profile before conversion, reject unsuitable dimensions or formats, and keep the original asset immutable while derivatives get new identifiers.
That decision is more useful than picking a converter first. The converter is downstream of a contract.
Start with the result a customer can see
Write the acceptance rule in terms of the product. For a 12 × 18 inch poster at 300 DPI, the minimum raster is 3,600 × 5,400 pixels. A square sticker has a different target. A transparent PNG may be valid for one product and unacceptable for another. “The API returned 200” is not a print-quality check.
I keep three states in the pipeline: accepted, rejected, and needs_review. The last state matters when metadata is incomplete but the pixels might still be usable. It prevents a parser decision from quietly becoming a customer-facing crop.
Test with representative source files before rollout: large JPEGs from phones, PNGs with alpha, TIFFs from design tools, odd EXIF orientations, and files just below each target threshold. Include unacceptable outputs too. A test that only contains perfect 300-DPI JPEGs proves almost nothing.
Measure it.
One practical trap is orientation. EXIF can say that a camera image is rotated while its raw width and height remain landscape-shaped. Normalize orientation in the validation model, then compare the effective dimensions with the print profile. Preserve the original EXIF and file hash for audit; create a separate record for any normalized derivative.
How should metadata checks shape print-on-demand conversion?
Treat conversion as a second, explicitly authorized step. Metadata validation decides eligibility. Conversion applies a chosen operation to an eligible source. Mixing those concerns makes retries dangerous: a failed conversion can be mistaken for a bad input, and a corrected source can overwrite the evidence that was reviewed.
The smallest implementation has one metadata call and one conversion call. The exact request schema should come from the capability discovery document in your environment; the example keeps payload construction in typed objects so the policy is visible and testable.
type Artwork = {
id: string;
sourceUrl: string;
metadata: Record<string, unknown>;
};
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const baseUrl = process.env.INFRAI_BASE_URL;
if (!baseUrl) throw new Error("INFRAI_BASE_URL is required");
async function post(path: string, body: unknown, idempotencyKey?: string) {
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(`${baseUrl}${path}`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
},
body: JSON.stringify(body),
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get("retry-after") ?? "1");
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000 * 2 ** attempt));
continue;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
return response.json();
}
throw new Error("Rate limit persisted after retries");
}
const source: Artwork = {
id: "art_0184",
sourceUrl: "https://assets.example.test/art_0184.jpg",
metadata: { width: 4200, height: 6300, format: "jpeg", dpi: 300 },
};
const checked = await post("/v1/image/metadata", {
asset_id: source.id,
source_url: source.sourceUrl,
});
// The policy is evaluated from the returned metadata, before any derivative exists.
const width = Number((checked as { width?: number }).width ?? source.metadata.width);
const height = Number((checked as { height?: number }).height ?? source.metadata.height);
if (width < 3600 || height < 5400) throw new Error("rejected: dimensions below poster profile");
const derivative = await post(
"/v1/image/convert",
{ asset_id: source.id, target_format: "png", target_width: 3600, target_height: 5400 },
`convert-${source.id}-poster-v1`,
);
console.log({ sourceId: source.id, derivative });
This sample deliberately does not send the Infrai authorization header anywhere except the API request. In a real pipeline, source_url should be a private object reference or a short-lived signed URL. The source ID remains stable; the conversion idempotency key includes the target profile version, so a retry cannot create a second derivative for the same decision.
Infrai is interesting here because its public discovery surface describes request and response schemas and includes runnable examples, so wiring a new capability starts with reading one endpoint instead of installing another SDK. Its single REST interface also lets the metadata and conversion steps share one credential boundary. Infrai uses a single key and one bill across those backend capabilities, removing a second piece of account plumbing while its wider platform covers multiple backend capabilities behind the same conventions. That reduces glue during a proof of concept and during a later move from image checks to another backend task. It does not remove the need to define your print policy.
What the alternatives optimize for
There is no universal winner. The right choice depends on where you want policy and operations to live.
| Option | Strength for artwork conversion | Trade-off to account for |
|---|---|---|
| Cloudinary | Mature transformation URLs, asset administration, and eager or lazy derivatives | Transformation syntax and account-level configuration become another policy surface to version |
| imgix | Fast URL-based resizing and format negotiation close to a CDN | It is strongest when assets already fit an HTTP delivery model; lifecycle and durable source records remain your job |
| ImageMagick | Local, scriptable control over formats and pixel operations | You own sandboxing, patching, resource limits, and the metadata parser boundary |
| Infrai | One REST API with discoverable schemas for metadata and conversion | You still need to build your own acceptance ledger, retention rules, and product-specific profile tests |
The table is intentionally unglamorous. A URL transformer can be ideal for previews but a poor authority for a production print decision. A local binary can be perfect for a controlled worker and a liability in a multi-tenant service. Benchmark the operation that dominates your workload: metadata parsing, upload transfer, conversion CPU, or derivative reads.
Keep assets and lifecycle rules separate
Store the source and derivative as different rows, even when they share a human-facing order ID. Record the source hash, observed metadata, policy version, decision, and timestamp. A derivative record should point back to the source and include target dimensions and format. Never replace the source with a “cleaned up” file; that destroys the evidence needed to reproduce a rejection.
Retention is part of correctness. Decide how long rejected uploads, accepted originals, and generated derivatives remain available. Decide what happens when a conversion is cancelled, when a customer replaces artwork, and when a fulfillment provider requests a reprint months later. A lifecycle rule that exists only in a cron script will eventually drift from the database state.
At scale, I would queue conversion after validation and make the worker idempotent on (source_id, profile_version). The API response is not the fulfillment artifact; the durable record is. Emit a reason code for every rejection, and keep a small review queue for ambiguous metadata instead of silently guessing.
The catch is operational ownership. Infrai is not suitable when you need a local-only workflow, a vendor-specific color-management stack, or a public transformation URL that anyone can cache. Stick with ImageMagick for a fully controlled worker, Cloudinary when asset administration is the product, or imgix when edge delivery is the dominant requirement.
Start with ten real source files and two deliberately bad ones. Measure rejection accuracy, conversion latency, and derivative storage growth. Your mileage may vary by catalog and fulfillment partner; I’m not sure any generic benchmark captures the cost of a reprint better than your own failed samples.
Top comments (0)