I spent a whole evening last month staring at a Gumroad listing that looked broken. The product was fine. The files were fine. But the cover was a 600×600 square sitting on a page built for a wide banner, and the thumbnail in my own catalogue grid was a 1280×720 rectangle squashed into a square slot. Same image, two shapes, and I had only rendered one. That is the exact reason pack_product.py renders both a 1280×720 cover and a 600×600 thumbnail on every run.
One image cannot serve two shapes
A cover and a thumbnail are not the same picture at two sizes. They are two different crops with two different jobs. The cover is what a buyer sees on a product page, and wide rectangular images read better there because most marketplace layouts give you a horizontal slot. The thumbnail lives in grids, sidebars, and search results where the slot is square. If you render one image and let each site scale it, one of the two placements always looks wrong.
So the script draws twice. It renders a 1280×720 cover with the product title and subtitle laid out for a wide canvas, then renders a 600×600 thumbnail with the layout recomposed for a square. That is two Pillow passes, not one resize. The cost is a few hundred milliseconds per product on my 1 vCPU, 2 GB RAM box, which is nothing next to the cost of a listing that looks amateur.
Why 1280×720 and why 600×600
I picked 1280×720 because it is the 16:9 ratio that nearly every digital product page expects, and because it is large enough to stay sharp when a platform downscales it for retina screens. Going bigger, say 2560×1440, doubles the render time and the file size for no visible gain after the platform compresses it anyway. On a 2 GB RAM server, a bigger canvas also means a bigger Pillow buffer sitting in memory for no reason.
The 600×600 thumbnail is the same logic in reverse. Grid thumbnails on most storefronts display at 200 to 400 pixels. Rendering at 600 gives you headroom without wasting memory. I measured peak RSS with resource.getrusage(RUSAGE_SELF).ru_maxrss while building this, and the difference between rendering at 600 and rendering at 1200 and downscaling was small but real. On a single-core box running one process at a time, with gc.collect() between items, those small differences are what let me package 39 products, about 856 MB of output, without the process ever getting killed.
The rule I learned the hard way: render at the size you need
This applies beyond covers. When I first wrote the render step, I drew everything at 300 dpi and downscaled. It worked, and it was slow, and on one batch the peak memory crept up until a subprocess died partway through a 40-asset run. The fix was boring: render at the resolution you actually need. Inspecting a preview at 60 dpi beats rendering it at 300. The cover and thumbnail split is the same principle applied to packaging. Two small, purpose-built renders beat one big render resized twice.
I also moved heavy render steps into one file per subprocess. When a render finishes, the OS reclaims the memory instead of trusting Python's allocator to give it back. On 2 GB RAM, that habit is the difference between a pipeline that finishes and one that dies at asset 31.
What the packaging step actually does
Running python pack_product.py my-product "My Product" "A short subtitle" 9 does three things. It zips the product files. It renders the 1280×720 cover. It renders the 600×600 thumbnail. All three land in a _system folder, and you can redirect that with --dir or the PACK_ROOT environment variable so nothing is hardcoded.
The zip matters as much as the images. A buyer who opens a folder of loose files immediately wonders if they got everything. A zip with a real cover image next to it reads as a finished product. I sell on Gumroad, and my first sale there was $0.99, which is not a number that changes your life, but it happened on a listing that had a proper cover and a proper zip. I do not claim the images caused the sale. I claim that before I had them, the listings looked unfinished, and unfinished listings are harder to buy.
Why this is a script and not a manual step
I have 1,700 assets made and 58 live on Adobe Stock. At that volume, any step you do by hand becomes the bottleneck. Metadata was the first thing I automated because Adobe's upload CSV has a fixed 5-column header and Vecteezy's has 4 columns, and all three channels reject a wrong header instantly. Packaging was the second. If packaging a product takes more than one command, you will batch less often, and a catalogue that grows slowly is a catalogue that earns slowly.
The whole thing runs on Python 3.9 with two third-party packages, Pillow for the covers and reportlab for PDFs. The metadata and tracking scripts use the standard library alone. No framework, no cloud account, no GPU.
Where this lives
Everything I described here is part of the digital asset factory, the full system built from this work: the prompt engine, the metadata writers for Adobe, Vecteezy, and Dreamstime, the upload tracker that prevents double-posting, and pack_product.py with its two-render cover logic. It is the honest record of what worked, not a promise of what might.
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)