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

Last spring I packaged a sticker pack, uploaded it, and the listing looked broken on every device. The cover was a screenshot I had cropped by hand at some arbitrary resolution, and the thumbnail was a different aspect ratio entirely. On Gumroad the card view showed a stretched mess. On my own product page the cover bled into the description. I spent an evening re-exporting images at three different sizes before I admitted the problem was not my art, it was that I had no fixed output spec.

So I wrote one. It lives in pack_product.py, and it produces exactly two raster files per product: a cover at 1280×720 and a thumbnail at 600×600. This post is about why those two numbers, what each one is actually for, and why I stopped generating anything else.

The cover: 1280×720

The cover is the landscape image. It appears in marketplace cards, link previews, and the top of your product page. Almost every place a landscape cover gets displayed uses a 16:9-ish slot, so 1280×720 lands in those slots without cropping or letterboxing.

The other reason for 1280 specifically: it is wide enough that text on the cover stays readable after a platform scales it down. I tested covers at 800×450 and the subtitle turned to mush on mobile. At 1280 wide, a title set at 60 to 80 pixels in Pillow survives the downscale. That matters because my covers are rendered, not designed in an editor. pack_product.py draws the title and subtitle straight onto the canvas with Pillow, so the font sizes are fixed in the script and the canvas size has to be big enough to carry them.

One practical note from running this on a 1 vCPU, 2 GB RAM box: rendering a 1280×720 JPEG with Pillow is cheap, a fraction of a second and a few tens of MB of peak RSS. The memory discipline in the rest of the pipeline, one process at a time, gc.collect() between items, matters for the asset generation steps, not for the packaging step. Do not over-engineer this part.

The thumbnail: 600×600

The thumbnail is the square. It exists because squares and landscapes get used in different places, and a cropped landscape is not a substitute for a real square. If you take your 1280×720 cover and center-crop it to square, you lose the edges, and the edges are usually where the title sits. I shipped a product once where the cropped thumbnail cut the product name in half on both sides. That was the moment I decided to render the square separately.

600×600 is the size because it is big enough to survive display at 300 or 400 pixels on a storefront, small enough that it adds almost nothing to your zip or your upload time. The square layout in the script is different from the cover layout: centered title, subtitle underneath, no wide composition. Same brand colors, different geometry. That is the whole trick, two layouts, two fixed canvases, rendered from the same product data in one command:

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

That writes my-product.zip, my-product-cover.jpg (1280×720) and my-product-thumb.jpg (600×600) into a _system folder. No manual export, no cropping, no guessing.

Why fixed sizes at all

Because variation is where the failures come from. Every time I exported a cover by hand, the dimensions drifted. 1200 one time, 1280 the next, 1920 when I was feeling fancy. Platforms rescale whatever you give them, but they rescale badly when the aspect ratio does not match the slot, and some crop instead of scale. A fixed spec removes the entire class of problem.

There is a batching argument too. I have 39 products packaged with this script, 1,700 assets made in the parent pipeline, and 58 live on Adobe Stock right now. When every product has the same two image outputs at the same two sizes, the packaging step is boring and identical every time. Boring and identical is what you want at the packaging layer, because your attention belongs on the assets and the metadata, not on re-deciding what a cover looks like for the fortieth time.

The metadata point is worth one paragraph here because it is the same principle. All three stock channels, Adobe with its 5-column CSV header, Vecteezy with 4 columns, Dreamstime with 15, reject a wrong header instantly. make_metadata.py writes the exact header each one demands and nothing else. Fixed format, zero drift, same as the fixed canvases. The pattern that works is: decide the spec once, encode it in a script, never touch it again.

What I would do differently

If I were starting today I would still pick 1280×720 and 600×600. I would not add a third size, a banner or a square social card, until a specific platform actually demands it. Every extra output is one more thing to keep consistent across a catalogue, and on a small server with 2 GB of RAM the habit that keeps you alive is doing less per item, not more. Render at the resolution you need, inspect previews at 60 dpi instead of 300, and measure peak RSS with resource.getrusage so you know your real ceiling instead of guessing at it.

The honest summary: the two sizes are not magic numbers, they are a decision I made once so I would never make it again. 1280×720 for the landscape slots, 600×600 for the square slots, rendered by Pillow in one command, identical across all 39 products. The first sale this pipeline produced was $0.99, and I remember it more clearly than any of the larger ones, because it proved the boring packaging layer was not the thing holding the products back.

All of this is baked into the kit I built from this work. pack_product.py with the two fixed canvases is in there, alongside make_metadata.py, upload_tracker.py and md2pdf.py, the same scripts that packaged those 39 products, validated on 1 vCPU and 2 GB RAM with no GPU. If you want the packaging layer solved so you can spend your time on the assets, that is the tool.

Get digital asset factory


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

Top comments (0)