You select fifty product images, click Convert, and receive a ZIP.
The download looks successful. After extraction, there are only forty-eight images. Two inputs had the same basename, and the export code reused their output path.
Or perhaps two conversions failed and the interface quietly archived everything else.
These are different failures, but they create the same experience: a complete-looking delivery that does not account for the user's original request.
An archive is a container. It does not establish that every input produced a valid result, that filenames stayed distinct, or that the browser saved the download successfully.
Here is a reference design for a browser batch export whose result can be checked. The proposed manifest and code below are engineering examples, not claims about features currently implemented in BatchSet.
1. Separate conversion, packaging, and delivery
A single “Done” state often hides several stages:
| Stage | Useful completion condition |
|---|---|
| Conversion | Every selected item has a settled success or failure status |
| Packaging | The expected successful outputs were added and archive generation resolved |
| Delivery | A download was initiated or an explicit file-writing operation completed |
| Recipient verification | The archive opens and its contents match expectations |
For a conventional browser download, the application should not assume that triggering an anchor proves a file was saved. The user can cancel, a browser can block the action, or storage can become unavailable.
Use language that matches what the application knows. “Archive ready” and “Download started” are useful states even when the application cannot verify the final disk write.
For conversion progress, count settled items and show how many succeeded. If packaging then takes several seconds, label that stage instead of leaving the interface at a mysterious 100%.
2. Give each input a stable identity
Filenames are not unique identifiers.
These can all appear in one batch:
camera-a/product.jpg
camera-b/product.jpg
product.png
Converting each to product.webp creates a collision even if every conversion succeeds.
Assign an item ID and preserve the user's input order before starting asynchronous work. Store results against that identity, not whichever filename or completion index happens to be convenient.
A proposed result record might include:
{
"id": "item-002",
"inputIndex": 1,
"inputName": "product.jpg",
"status": "success",
"outputName": "0002-product.webp",
"outputBytes": 184230
}
The bytes are illustrative. In a real export, read them from the produced Blob.
Keeping identity separate from display naming also helps with retries. A replacement result can update the same item without accidentally counting it as another input.
3. Choose output names before generating the archive
Some archive APIs treat a name as a path. A slash can create directories; duplicate names can replace earlier entries. JSZip documents its entry behavior in the file API.
For a simple flat export, generate names yourself rather than accepting arbitrary paths from the input.
This example uses a unique input index, a conservative ASCII stem, and an extension selected from the known output MIME type:
function archiveImageName(inputName, inputIndex, outputType) {
if (!Number.isSafeInteger(inputIndex) || inputIndex < 0) {
throw new RangeError("Invalid input index");
}
const extensions = new Map([
["image/jpeg", "jpg"],
["image/png", "png"],
["image/webp", "webp"],
]);
const extension = extensions.get(outputType);
if (!extension) throw new Error("Unsupported output type");
const leaf = String(inputName).split(/[\\/]/).pop();
const stem = leaf.replace(/\.[^.]*$/, "")
.normalize("NFKD")
.replace(/[^a-zA-Z0-9_-]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 60) || "image";
const prefix = String(inputIndex + 1).padStart(4, "0");
return `${prefix}-${stem}.${extension}`;
}
This policy intentionally sacrifices some filename fidelity for predictability. The original name can remain in a separate manifest. Unique indices prevent collisions even when different names normalize to the same stem.
It is a naming policy for application-generated exports, not an archive-extraction security system. It does not prove compatibility with every filesystem or third-party uploader. Confirm your destination's naming rules.
Only use a verified output type. An extension derived from the requested format can mislabel an encoder fallback.
4. Decide whether partial export is allowed
There are two useful policies:
- Complete batch required: block packaging until every item succeeds or the user resolves failures.
- Partial export allowed: offer successful files with a clearly visible list of failures and a partial label.
Neither should be implicit.
For a product catalog, missing images may create broken listings. For an informal cleanup task, exporting the successful files and retrying the rest might save time.
If partial export is allowed, explain the count before download:
Partial export: 48 of 50 images succeeded. Two failed inputs are listed in the report.
Use actionable failure categories where possible: unsupported input, decoding failure, export failure, or cancellation. Avoid turning every exception into a generic “something went wrong.”
Do not mark a still-pending input as failed just to make the counts add up. Packaging should begin only after results have settled, or after cancellation has been handled according to an explicit policy.
5. Put accountability inside the package
A UI warning disappears when a ZIP is forwarded to another person. A small manifest can travel with the files.
For the proposed workflow, I would include:
- Selected, successful, and failed item counts.
- The output settings relevant to the transformation.
- One record per selected input.
- Each successful output's archive name and measured bytes.
- A bounded, user-facing failure category for failed items.
Avoid raw stack traces, temporary object URLs, or source download URLs with tokens. The manifest is part of the deliverable and may be shared beyond the original user.
JSON is convenient for software that checks packages. A short human-readable summary can help recipients who do not use developer tools. Choose what the workflow needs rather than adding sensitive information merely because it is available.
Counts should reconcile: selected equals succeeded plus failed once all items are settled. The number of archived image entries should equal succeeded. Manifest and summary files are additional entries, so distinguish image count from total archive entry count.
6. Compression and packaging have separate costs
JPEG, PNG, and WebP already use compression. ZIP is useful for packaging them together, but another compression layer does not guarantee substantial savings.
For an image-heavy export, a sensible reference starting point is to store image entries without additional ZIP compression and then measure the trade-off on representative batches. Text manifests may compress well even when images do not.
JSZip's generation options describe archive output types and compression choices. Its limitations also explain that generateAsync retains the generated result in memory.
That means “the conversions finished” does not establish that packaging will fit within the browser's remaining memory. Completed output Blobs, decoded resources that were not released, and the archive result can overlap.
Treat large-batch packaging as its own resource decision. Release unnecessary previews, avoid base64 copies, consider smaller groups, and investigate a suitable streaming architecture where the expected workload justifies it.
An option named streamFiles is not, by itself, a promise that the final Blob is written directly to disk without accumulation. Read the complete library behavior and test the actual path you ship.
7. Freeze an export snapshot
If the user edits output settings while packaging runs, which settings should the archive use?
My proposed rule is to package an immutable snapshot of settled results. Changes after that snapshot start a new export rather than mutating the current one halfway through.
This prevents a package containing early files from one format and late files from another unless mixed output is an explicit feature.
Retain the snapshot identity for retries of packaging. A ZIP generation failure should not require recompressing every image if valid outputs are still available. Conversely, do not reuse stale outputs after settings change.
Allow a user to dismiss progress without implying that a running operation has stopped. Cancellation behavior must match the library and worker capabilities actually available.
8. Verify the artifact, not only the callback
For release testing, use batches that include duplicate basenames, mixed extensions, non-ASCII names, one corrupt input, and an intentionally oversized workload.
Then check:
- The chosen complete-or-partial policy is visible.
- Every selected item is accounted for.
- Output names are distinct and use correct extensions.
- The archive extracts in representative recipient software.
- Extracted files decode and have expected dimensions.
- Manifest byte counts match extracted file sizes.
- Retrying packaging does not create missing or duplicated items.
A hash can additionally check that extracted bytes match the intended bytes. It does not establish that an image looks correct or that the image belongs to the right product. Those need other checks.
Use the ZIP as a delivery container
BatchSet's Bulk Image Converter offers browser-based conversion of mixed image inputs and a ZIP download.
When preparing a batch for handoff, check its successful count and inspect the extracted results before replacing your originals. A convenient container becomes a reliable delivery only when its contents match the work you intended to finish.
Top comments (0)