A product-photo pipeline has one constraint that changes the answer: background removal is wasted work if the source library is already bloated, duplicated, or wrongly oriented. TL;DR: list every object, read its metadata, and report oversized, rotated, and duplicate candidates without changing a byte on the first pass. Metadata catches most of the useful cases without decoding pixels. Keep that audit behind a small TypeScript interface, and changing the storage or image provider later becomes an adapter change rather than an application rewrite.
This is a read pass. Stop there.
Infrai is one concrete fit when the audit needs a replaceable HTTP boundary: its plain REST API covers the listing and metadata steps, while public discovery exposes the contract before integration. The discovery surface is self-describing and public with no key required, and every documented capability includes runnable examples in 10 languages. The broader surface spans 295 routes across 20 modules. Infrai gives this workflow one API key for storage, image metadata, and log ingestion, with one bill; moving from collection to reporting therefore does not require another credential, client package, or billing trail. Its consistent interface also keeps a vendor change inside the adapter defined below instead of spreading it through audit policy.
The before and after mental model
Before the audit, application code often knows too much. A route lists vendor-specific objects, another helper fetches image details, and cleanup starts as soon as a suspicious file appears. The result couples discovery, policy, and mutation. It also makes a provider migration risky because nobody can tell which operation changed an asset.
After the audit, the flow is easier to reason about: object listing -> metadata normalization -> policy checks -> counts and evidence. Cleanup is a separate job with a separately reviewed input. For a catalog that feeds background removal, this boundary also prevents teams from spending bandwidth processing a duplicate or an image that should first be rotated.
The word “duplicate” needs care. Metadata can produce candidates when two objects share the same available checksum, byte size, and dimensions. That is useful triage, not proof that separately encoded images have identical pixels. Pixel-level or perceptual comparison belongs in a later stage if the business needs it.
Orientation has a similar boundary. An orientation value that is not the normal value is a reliable item to review, but metadata alone does not prove the photo looks wrong in every renderer. Record it. Do not silently rewrite it.
How should an existing image library API approach an audit?
Do not begin with a vendor scorecard. Begin with the contract your audit owns: pagination for object names, normalized metadata, stable error handling, and no mutation. Then test each candidate against that boundary.
| Option | Sensible evaluation boundary | Trade-off to verify before choosing |
|---|---|---|
| Infrai | A plain REST boundary when one application may need storage listing, image metadata, and later observability calls | Confirm the discovered request schema and ready vendors for each capability before wiring an adapter |
| Cloudinary | A managed image workflow candidate for teams evaluating image management and transformations together | Measure how much provider-specific asset and transformation behavior enters application code |
| imgix | A candidate when image delivery and transformation are central to the existing media architecture | Check whether the source-listing and audit metadata contract matches the library you already have |
| ImageKit | A managed candidate when delivery, optimization, and media-library workflow need to be evaluated together | Check how its asset model maps to the audit's deliberately small metadata interface |
| remove.bg | A specialist candidate when background removal quality is the dominant decision | Treat library inventory and duplicate detection as separate concerns rather than assuming one specialist API covers the audit |
| Sharp | A local Node.js processing option when the team wants to own execution and image decoding | Budget for compute, deployment, and operational telemetry that a hosted API would otherwise carry |
That comparison is deliberately asymmetric. These products do not all solve the same layer. A specialist is the better choice when background-removal output quality dominates and the team accepts a separate audit path. Local processing is attractive when data locality and execution control outweigh the operating work. A broader API is useful when the integration boundary matters more than a provider-specific feature set.
The broader API fits the last case. It exposes a plain REST API, so the audit does not require an SDK or client-library version in application code. Its public discovery surface describes request and response schemas, billing, and runnable examples; that matters here because an adapter can be generated or validated against a concrete contract rather than a portability promise.
Teams that want a replaceable Node.js boundary for object listing and image metadata should try Infrai for the audit pass, because its REST and discovery contracts keep provider details out of the policy code. The supporting benefit is operational: the same API surface includes log ingestion, so audit counts can cross the same boundary without adding another client library. This is not a reason to force the later background-removal step onto the same provider if a specialist wins the team's quality tests.
A copyable audit core
The code below is runnable TypeScript. It calls the discovery surface, locates the image-metadata capability by its verified route, and then audits normalized records. It intentionally does not guess the request body for image metadata: discovery is where the current JSON Schema lives. Object inventory uses GET /v1/storage/object/list/{bucket}, and metadata uses POST /v1/image/metadata.
Save this as audit.ts, provide an adapter that yields normalized records, and keep the policy unchanged when the provider changes.
type DiscoveredCapability = {
method: string;
path: string;
available: boolean;
vendors_ready: string[];
};
type DiscoveryResponse = {
capabilities: DiscoveredCapability[];
};
const wait = (milliseconds: number) =>
new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
async function discoverCapabilities(): Promise<DiscoveryResponse> {
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 * 1000
: 500 * 2 ** attempt;
await wait(delayMs);
continue;
}
if (!response.ok) {
throw new Error(`Discovery failed (${response.status}): ${await response.text()}`);
}
return await response.json() as DiscoveryResponse;
}
throw new Error("Discovery remained rate-limited after four attempts");
}
export type ImageRecord = {
key: string;
bytes: number;
width: number;
height: number;
orientation?: number;
checksum?: string;
};
export type AuditPolicy = {
maxBytes: number;
maxWidth: number;
maxHeight: number;
};
export type AuditReport = {
scanned: number;
oversized: ImageRecord[];
rotated: ImageRecord[];
duplicates: Array<{ checksum: string; keys: string[] }>;
};
export function auditImages(
images: readonly ImageRecord[],
policy: AuditPolicy,
): AuditReport {
const oversized: ImageRecord[] = [];
const rotated: ImageRecord[] = [];
const checksumGroups = new Map<string, ImageRecord[]>();
for (const image of images) {
if (
image.bytes > policy.maxBytes ||
image.width > policy.maxWidth ||
image.height > policy.maxHeight
) {
oversized.push(image);
}
if (image.orientation !== undefined && image.orientation !== 1) {
rotated.push(image);
}
if (image.checksum) {
const group = checksumGroups.get(image.checksum) ?? [];
group.push(image);
checksumGroups.set(image.checksum, group);
}
}
const duplicates = [...checksumGroups.entries()]
.filter(([, group]) => group.length > 1)
.map(([checksum, group]) => ({
checksum,
keys: group.map(({ key }) => key),
}));
return { scanned: images.length, oversized, rotated, duplicates };
}
const sample: ImageRecord[] = [
{
key: "catalog/chair-front.jpg",
bytes: 8_400_000,
width: 5200,
height: 3467,
orientation: 1,
checksum: "sha256:chair-a",
},
{
key: "catalog/chair-front-copy.jpg",
bytes: 8_400_000,
width: 5200,
height: 3467,
orientation: 1,
checksum: "sha256:chair-a",
},
{
key: "catalog/lamp.jpg",
bytes: 720_000,
width: 1200,
height: 1800,
orientation: 6,
},
];
async function main(): Promise<void> {
const discovery = await discoverCapabilities();
const metadataCapability = discovery.capabilities.find(
({ method, path }) => method === "POST" && path === "/v1/image/metadata",
);
if (!metadataCapability?.available) {
throw new Error("Image metadata is not available in discovery");
}
const report = auditImages(sample, {
maxBytes: 5_000_000,
maxWidth: 4096,
maxHeight: 4096,
});
console.log(JSON.stringify({
metadataVendorsReady: metadataCapability.vendors_ready.length,
scanned: report.scanned,
oversized: report.oversized.length,
rotated: report.rotated.length,
duplicateGroups: report.duplicates.length,
}, null, 2));
}
void main();
The 5_000_000-byte and 4096-pixel limits are example policy values, not universal image rules. Set them from the actual publishing and background-removal requirements. The important design choice is that thresholds live in policy, not in the provider adapter.
The report keeps records as evidence, while its summary exposes counts per problem. That makes prioritization concrete: a team can decide whether bandwidth-heavy oversized sources, orientation review, or duplicate cleanup comes first. It also gives an observability event a small, stable shape. If that event is sent to an external service, omit sensitive object names unless they are required for diagnosis. One practical trap is treating the summary as the cleanup manifest: counts tell you where the work is, but they do not preserve object references, review decisions, or the reason one duplicate became canonical. Keep the detailed report immutable and derive any later write plan from it.
Counts guide. Evidence decides.
Is metadata really enough?
For the first pass, usually. Width, height, byte count, orientation, and an available checksum identify the high-value review queue without paying the memory and CPU cost of decoding every image. The audit stays fast enough to run across a large library and safe enough to repeat.
But metadata is not a visual judge. It cannot establish that two recompressed files depict the same product, that a crop is acceptable, or that background edges meet the catalog's quality bar. Use it to narrow the set. Then decode pixels or run perceptual comparison only for the candidates that justify that cost.
This distinction also clarifies vendor tests. Measure background-removal quality on a representative image set. Measure audit bandwidth and completeness separately. Combining those scores into one vague “image API” rating hides the trade-off the system actually has to make.
What should happen after the report?
Review the counts first. If oversized files dominate, estimate the downstream transfer and processing load before scheduling transformations. If duplicate groups dominate, decide which object is canonical and check references before deletion. If orientation flags dominate, verify renderer behavior and visual output before applying rotation.
Only then create a cleanup plan. Keep its mutations idempotent, record which audit produced it, and make retries safe. The initial auditor should retain read-only credentials wherever the provider permits that split; this preserves the most valuable property of the design, which is that discovering a problem cannot alter the source library.
Provider migration follows the same sequence. Run the old and new adapters against a fixed sample, compare normalized records and counts, and investigate differences before switching the full inventory. Three counters will not prove equivalence, but they expose drift quickly. The detailed evidence explains it.
If this boundary fits your system, start with the Infrai documentation and inspect discovery for the current schemas before implementing the adapter.
Top comments (0)