My first 40 uploads had no cover. I zipped the files, ran the batch, pushed them out. Every one of those packs looked like a folder someone forgot to finish. No preview grid image, no hero. Buyers saw a filename. First sale came later, on a pack that had both images, and it was $0.99. Small number, but it told me the packaging was doing work the zip alone could not.
That is why pack_product.py renders two images and not one: a cover at 1280Ã720 and a thumbnail at 600Ã600. Different jobs, different shapes. Here is what each one actually does and why I stopped trying to make one image cover both.
Two images, two jobs
A marketplace grid is not a product page. Grid slots get cropped to a square. Product page hero slots are wide. If you hand the same file to both, one of them gets letterboxed or center-cropped and your title text ends up cut in half.
So the script writes both, every run:
-
my-product-cover.jpgat 1280Ã720 -
my-product-thumb.jpgat 600Ã600
Same source art, two crop decisions. The cover keeps the wide composition. The thumb is composed for square, because a 16:9 image squeezed into a square slot loses the edges, and the edges are usually where I put the readable title.
1280Ã720 is the wide one
16:9 is the shape most product pages and preview slots want. 1280 wide is enough that the image stays sharp on a 1366 px laptop without being heavy. 720 tall keeps the file under a few hundred KB as a quality-85 JPEG, which matters when you are listing hundreds of packs and the upload form is on a slow connection.
At 1280Ã720 I can fit a title line and a subtitle line without shrinking the type to unreadable. That is the whole reason the script takes three arguments beyond the folder name:
python pack_product.py my-product "My Product" "A short subtitle" 9
The 9 is the price. The title and subtitle get drawn onto the cover. If I tried to fit that same text into 600Ã600 without reflowing, it would eat the art.
600Ã600 is the grid one
Square is the safe shape almost everywhere. Grid tiles crop to square, avatar-style slots crop to square, and a square thumbnail stays readable when the platform renders it at 200 px. A 600Ã600 source downsampled to 200 is a clean 3:1 reduction. A 1280Ã720 downsampled into a 200Ã200 square is a crop plus a rescale, and the result looks like a mistake.
600 is also the smallest size where fine text in a thumbnail still resolves after the platform's own compression. I tested smaller. It did not survive.
So the rule in my head is simple. Wide grid or hero, use the cover. Square tile or list row, use the thumb. Never swap them.
What both renders cost on 1 vCPU and 2 GB
This pipeline ran on 1 vCPU, 2 GB RAM, no GPU. Pillow is the only image dependency in pack_product.py, plus reportlab for the PDF path. Two third party packages total for the whole kit. make_metadata.py and upload_tracker.py use the standard library alone, no install.
Rendering two JPEGs per pack is cheap, but it is not free, and two habits keep it inside the box:
- One process at a time. Never fan out image jobs. On one core, parallel jobs just queue and each one holds its own memory.
-
gc.collect()after each asset. Pillow buffers do not always come back fast enough on their own.
Measure peak RSS, not average:
resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
Average memory tells you nothing about whether the box will survive item 300. Peak tells you the real ceiling. For heavy render steps, one file per subprocess so the memory goes back to the OS.
Also render at the resolution you need. Inspecting a preview at 60 dpi beats rendering it at 300 dpi and then throwing it away.
One command, deterministic names
pack_product.py writes the zip, the cover and the thumb into a _system folder next to where you run it. Three files, fixed names, so a re-run overwrites instead of piling up cover-final-v2.jpg. Point it elsewhere with --dir or the PACK_ROOT environment variable. No editing the script.
That determinism matters more than it sounds. I have 1,700 assets made so far and 58 live on Adobe. The ones that stalled were the ones where I had to guess which cover was current. Fixed names remove the guess.
Do not re-render what you already shipped
The other half of this is not doing work twice. upload_tracker.py is the ledger:
python upload_tracker.py pending items.txt
python upload_tracker.py done "asset-001.jpg"
python upload_tracker.py status
Running a batch twice is the fastest way to get an account flagged. The tracker makes it structurally hard. Covers follow the same logic. If the pack exists in packs/ and the ledger says done, the script does not need to touch it again.
Keep research/ forever, too. Notes and scrape logs are raw material for the next product. Deleting them to clean up is the mistake this project made once.
Where this fits
The two sizes are a small decision, but they are the kind that decides whether a pack looks finished. A zip with a real cover and a real thumbnail sells. A bare file does not. That is the whole argument for both images, and for the memory habits that let a 1 vCPU box render them all night.
This came out of the same pipeline that packaged 39 products, 856 MB of output, on 1 vCPU and 2 GB, no GPU. Roughly 19,000 lines of pipeline code sit above these four scripts, and about 300 pages of research behind the rules they enforce.
If you want the working tools instead of rebuilding them: make_metadata.py writes the exact Adobe 5 column, Vecteezy 4 column and Dreamstime 15 column headers the upload forms demand, upload_tracker.py stops double posting, md2pdf.py turns a doc into a product, and pack_product.py makes the cover and thumb above. Two dependencies. Standard library for the rest.
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)