DEV Community

Cover image for Why pack_product.py Renders Both a 1280Ã 720 Cover and a 600Ã 600 Thumbnail

Why pack_product.py Renders Both a 1280Ã 720 Cover and a 600Ã 600 Thumbnail

I spent an hour last month staring at a Gumroad listing that looked broken. The product zip was fine. The cover was fine. But in the grid view next to my other products, the whole card looked wrong, cramped, cropped, like a listing someone abandoned halfway through. The problem was that I had one image doing two jobs. A 1280×720 cover stretched into a square slot looks bad, and a square image cropped into a wide card looks worse. That is the whole reason pack_product.py renders two images instead of one.

The two slots are not the same shape

Marketplaces and storefronts are inconsistent about what they ask for, but they are consistent about punishing the wrong answer. A wide 16:9 card is the standard for product pages and preview banners. 1280×720 is big enough that nothing looks soft when a platform scales it down, and small enough that Pillow renders it in under a second on the 1 vCPU, 2 GB server this pipeline runs on. A 600×600 thumbnail is the square slot: directory grids, small preview tiles, the little image that shows up when someone shares your link. If you upload a 1280×720 image into a square slot, the platform either crops the sides off or letterboxes it. Both make the product look worse than it is.

So the script renders both from the same source, in the same run, with the same title and subtitle baked 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 at 1280×720, and my-product-thumb.jpg at 600×600 into a _system folder. No second pass, no manual crop in an editor.

Why the same run matters

The obvious alternative is rendering one big image and resizing it with Pillow afterward. I tried that first. Downscaling a 1280×720 cover into a square means either cropping (you lose the subtitle text at the edges) or squashing (everything looks slightly off in a way buyers notice but cannot name). Text baked into an image does not survive resizing well. Rendering the square natively, with the text laid out for a square canvas, keeps both images sharp.

It also keeps the two images consistent. Same colors, same fonts, same subtitle, because they come from the same render function with the same inputs. When I packaged 39 products this way, the catalogue grid looked like one shop instead of 39 unrelated experiments. That consistency is worth more than any individual cover looking clever.

The RAM side of it

Rendering two images per product on a 2 GB box sounds like a place to get burned, but it is fine if you follow the habits that got the whole pipeline to run on 1 vCPU and no GPU:

  • Render at the resolution you need. 1280×720 and 600×600 are both cheap. The mistake is rendering at 3000 pixels and downscaling, which spikes peak RSS for no benefit.
  • Call gc.collect() after each product so Pillow's buffers go back to the OS.
  • One process at a time. Never fan out image jobs in parallel on a single core. Sequential is slower per item and faster overall because nothing thrashes.

I check resource.getrusage(RUSAGE_SELF).ru_maxrss after batches to see the real ceiling. Rendering both images in-process peaked well under what a single 300 dpi render of a preview page cost, which is why the kit's other rule exists: inspect at 60 dpi, render at the size you need.

The zip is the other half

The cover gets the click, but the zip is the product. pack_product.py bundles the files first, then renders the images, so a failed render never leaves you with a zip you cannot list. And the two images are named after the product, my-product-cover.jpg and my-product-thumb.jpg, which matters more than it sounds. When you have 58 assets live on a platform and dozens of finished packs, a folder of files called cover_final.jpg is a junk drawer. Named outputs mean a re-run of the packager overwrites the right files and never the wrong ones.

The same discipline shows up everywhere else in the pipeline. upload_tracker.py exists because the fastest way to get a stock account flagged is running a batch twice and double-posting. make_metadata.py writes the exact CSV headers each platform demands, Adobe's 5-column format, Vecteezy's 4 columns, Dreamstime's 15, because all three reject a wrong header instantly and a rejected CSV costs you a review cycle. Packaging is the same idea: make the boring step deterministic so it never costs you a day.

What this added up to

The pipeline behind these scripts produced 1,700 assets, got 58 live on Adobe Stock, and made its first sale for $0.99. That first dollar is the honest number. It does not prove a business, but it proves the loop works end to end: make the art, build the metadata, package the product with a real cover and thumbnail, list it, get paid. The 1280×720 cover plus 600×600 thumbnail pair is a small piece of that, but it is the piece that makes a listing look like a product instead of a file dump.

I put the four scripts, make_metadata.py, upload_tracker.py, md2pdf.py, and pack_product.py, into a kit. Two third-party packages total, Pillow and reportlab, and two of the scripts need nothing but the standard library. The whole thing ran on the 1 vCPU, 2 GB server. If you want the same packaging layer without rebuilding it, that is what the digital asset factory is: the full system built from this exact work, failures included.

Get digital asset factory


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

Top comments (0)