🖼️ Bulk Personalized Image Generator

One background image plus a CSV, and you get one personalised image per row, zipped, with a manifest CSV that lists every filename. Free, no signup, and no file is uploaded anywhere.

1. The background image

Drop your background image hereor click to choose a PNG, JPG or WebP

Read this before you start: this page makes the image files and the manifest, it does not host them. To put personalised images inside an email you still need to upload the folder somewhere public first, for example a GoHighLevel media library, S3, Cloudinary or any static host. Paste that folder address into the base URL box in step 4 and the manifest will carry a ready to use link for every row.

2. Your CSV

Drop a .csv fileor paste the rows in the box below

One run covers up to 200 rows. The whole ZIP is built in memory, so a bigger batch can run a mid range machine out of RAM. For a longer list, run it in blocks by changing these two numbers. Each block downloads its own ZIP with a manifest.csv covering only that block, so you stack the manifests together at the end.

3. Text layers

Click a column to insert it. Write {{company|there}} to fall back to "there" when that cell is empty.
System fonts only, on purpose. Nothing is downloaded mid render, so the export always matches the preview.
Position
The left column aligns text left, the middle centres it, the right column aligns it right.
Anything too wide for the box is shrunk to fit, down to about an eighth of the size you set. A very long value can still overflow, and the nudge sliders can push a layer past the edge, so step through a few rows before you run the batch. Wrapping stops at ten lines and anything past that is cut off.

Logo per row (optional)

Pick every logo in one go. Choosing files again replaces the whole set rather than adding to it.

Preview of the logo matched to the row being previewed
Match is on the file name, with or without the extension, ignoring case. A row with no match simply gets no logo.

Load a background image in step 1 and the preview will appear here.

4. Output

Column tokens work here too. Anything outside letters, numbers, dot, dash and underscore is replaced, and repeats get -2, -3 on the end so nothing is overwritten inside the ZIP.
Fill this in and the manifest gains an image_url column, so the CSV imports straight into your sending tool. If you run a long list in blocks, stack each block's manifest together first.

🎬 Plus certificates, mockups, batch watermarking & 300+ more free toolsOpen Castwright

What this is for

Anywhere the same picture needs a different name on it. Fifty course certificates as PNGs for a Slack drop. Fifty client named social posts from one branded template. A mockup per prospect for a portfolio push. Ticket art with an attendee name. Conference badges. Every one of those is the same job: take one design, loop a spreadsheet, write a different value into the same spot each time, and end up with a folder of files.

How the positioning works

Every layer is placed with a nine point grid plus a nudge slider, and every measurement is stored as a share of the background rather than as pixels. That is the difference between a preview that matches the export and one that does not. Change the output size from 1200 px to full resolution and the text stays exactly where you put it, at the same relative size, because nothing in the layout is tied to the size of the preview on screen.

The manifest is the part people forget

A folder of images on its own is only half of a campaign. The ZIP also contains manifest.csv, which holds the rows you actually ran with an image_filename column added, and an image_url column when you give a base URL. Upload the images, then feed that manifest to your sending tool and each contact already points at their own picture. Without it you are matching rows to files by hand. A list run in blocks gives you one manifest per block, so stack them before importing.

Honest limits

System fonts only, so the four choices here render the same way in the export as they do in the preview with no font loading step to go wrong. Up to 200 rows in one run, because the archive is assembled in memory before it is handed to you. And it produces files, not hosting: the images still need to live at a public address before an email client can show them.

Your files stay on your machine

The background, the logos and the CSV are read by the page and drawn on a canvas in your own browser. No file you choose is ever sent anywhere. The page does make requests of its own, so here they are in full: it loads two open source libraries, JSZip and Papa Parse, from a public CDN when it opens, and it sends an anonymous page view plus a count of how many images a run produced to Castwright's own analytics. Nothing from your background, your logos or your CSV goes into either one.