DEV Community

Cover image for à 720 or 600à 600: What Each Size Does in pack_product.py and Why

à 720 or 600à 600: What Each Size Does in pack_product.py and Why

I lost about two hours one night to a cover image that looked fine on my screen and terrible in the marketplace listing. The zip was done, the metadata CSV was clean, and the only thing standing between me and uploading was the product page. The cover rendered at some arbitrary size Pillow picked from the source file, so it came out 1140×842 or something equally useless. It looked stretched in the preview tile and cropped weird on mobile. That was the night I stopped letting pack_product.py guess and made the sizes explicit.

Here is what each of the two sizes in that script actually does, and why they are not interchangeable.

The cover: 1280×720

The cover is the wide image. It is what shows up at the top of a product page and in link previews. 16:9 is the aspect ratio that fits everywhere without letterboxing or surprise cropping: Gumroad thumbnails, dev.to article cards, Twitter and Discord link previews all expect roughly that shape.

I render it at 1280×720 because that is big enough to survive a retina display at full width and small enough that Pillow produces it in well under a second on the 1 vCPU, 2 GB server this whole pipeline runs on. I measured it. A cover render from a 300 dpi source takes about 0.4 seconds and peaks around 90 MB of RSS on that box. If I tried to composite at the full source resolution first and downscale after, peak memory jumped past 600 MB, which matters when the box has 2 GB total and the orchestrator is doing other things.

The practical rule: the cover is a marketing asset. It carries the product title and subtitle as text, drawn by Pillow, centered, on a background sampled from the actual product files so it matches what the buyer gets. It never ships inside the zip.

The thumbnail: 600×600

The thumbnail is the square. Marketplaces and directory listings use square tiles, and a 16:9 image in a square slot gets center-cropped, which usually means your title text gets cut in half. I learned that one the hard way too: a cover with the product name on the right third looked great as a cover and read as a blank beige square in the tile.

600×600 is the size because it is the smallest resolution that still looks sharp in a tile rendered at 300 px on a retina screen, and it keeps file size low. The thumbnails come out around 40 to 60 KB each against 150 to 250 KB for the covers. Across 39 products that difference is real when you are regenerating the whole catalogue.

The thumbnail uses a different layout from the cover, not a resize of it. Same background, same title, but the text block is centered and sized for a square. Resizing the 1280×720 cover down and cropping to square produced unreadable text every time I tried it.

Why both come from one script

pack_product.py does four things in one run: zips the product folder, renders the cover, renders the thumbnail, and writes all three outputs into a _system folder (or wherever you point --dir or PACK_ROOT). Bundling them matters because the failure mode I kept hitting was regeneration drift. I would update a product file, re-zip, and forget the cover still showed the old title. One command, one run, all outputs consistent.

The script takes the product folder, the title, a subtitle, and the file count:

python pack_product.py my-product "My Product" "A short subtitle" 9
Enter fullscreen mode Exit fullscreen mode

The file count gets printed on the cover so buyers know what is in the zip before they open it. Nine files says something different than two.

The memory habits that make this work on a tiny box

The whole pipeline, including these renders, runs on 1 vCPU and 2 GB RAM with no GPU. Three habits keep Pillow from eating the box:

One, render at the resolution you need. 1280×720 and 600×600 are small canvases. The expensive part is loading the source, so I downscale the source immediately after opening it and never hold the full-resolution image longer than one step.

Two, gc.collect() after each product. Without it, peak RSS crept up across a batch of 20 packs until the box started swapping.

Three, one process at a time. Parallelizing the renders on a single core just splits the same CPU between jobs and doubles peak memory. Sequential was faster in wall-clock time on that hardware, which surprised me until I checked ru_maxrss.

Why this matters more than it sounds

Packaging is the difference between a folder of files and a product. I have 1,700 assets made and 58 live on Adobe Stock, and the first sale was $0.99, which is not a number that changes your life, but it proved the pipeline end to end. Every one of those 39 packaged products has a 1280×720 cover and a 600×600 thumbnail generated by this exact script, and I have not touched a cover by hand since.

The sizes are boring. That is the point. Fixed dimensions, fixed layouts, one command, no decisions left to make at 11 pm when you just want the thing uploaded.

This post is one piece of the system. The whole thing, the prompt engine, the metadata CSV builders for Adobe, Vecteezy and Dreamstime, the upload tracker, the PDF renderer, and pack_product.py with these exact sizes, is the digital asset factory. It is the honest version of what I built, not a promise of income.

Get digital asset factory


Building digital products? Free guides & kits for digital product sellers.

Top comments (0)