DEV Community

Cover image for One Pillow Script, Two Sizes: Cover Decisions Behind 39 Packaged Products

One Pillow Script, Two Sizes: Cover Decisions Behind 39 Packaged Products

I still have the first cover I rendered with Pillow. It was a 1280×720 JPEG of a product that looked fine in my image viewer and like a blurry mess on Gumroad. The problem was not the art. I had rendered the text layer at screen resolution and let Pillow downsample the whole composition, so the title antialiased into mush. That one bad cover sat on product number one for two days while I rewrote the script that now packages all 39.

Here is what I learned about covers, sizes, and the decisions behind them.

Two sizes, one script, and why both exist

pack_product.py renders exactly two images per product: a cover at 1280×720 and a thumbnail at 600×600. That is not arbitrary. The 1280×720 cover is the wide format that marketplaces and product pages expect. The 600×600 square is what gets cropped to a circle on social cards and looks right in grid views where a wide banner becomes a thin sliver.

The decision that fixed my blurry mess: render text at final resolution. Never compose at a smaller size and resize up, and never render sharp text and then let the cover get scaled down by the platform. Pillow draws text at whatever point size you give it on whatever canvas you give it, so I set the canvas to 1280×720 from the start and pick font sizes that hold up at 600×600 when the thumbnail version resamples the same composition.

What goes on the cover

Every cover gets three things: the product title, a short subtitle, and the count of files inside. That last one comes straight from the command line:

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

That 9 is the file count, and putting it on the cover was one of the better decisions I made. Buyers on asset marketplaces want to know what they are getting before they click. A cover that says "9 files" answers the first question without anyone opening the listing. It also forced me to actually know the count for every product, which caught two packs where I had miscounted during zipping.

The script bundles the zip and renders both images into a _system folder next to where you run it. I made that configurable with --dir or a PACK_ROOT environment variable after I filled up a working directory with 39 covers and could not find anything.

The constraint that shaped everything: 1 vCPU and 2 GB of RAM

None of this ran on a beefy machine. The whole pipeline, covers included, ran on a single-core box with 2 GB of RAM and no GPU. That changes how you write a Pillow script.

The rules I ended up with:

  • One image job at a time. No parallel rendering on a 1-core box, it just thrashes.
  • gc.collect() after each asset, because Pillow image objects hold buffers longer than you expect.
  • Measure peak RSS with resource.getrusage(RUSAGE_SELF).ru_maxrss, not average memory. Average tells you nothing about whether the process dies at asset 30 of 39.
  • Render at the resolution you need. A 600×600 thumbnail does not need a 4000 px source in memory.

The cover renderer follows all of these. It loads the background, draws the text layers, saves both sizes, and releases everything before the next product starts. That is why 39 products packaged on a machine that would choke on a parallel batch.

Where the covers fit in the wider pipeline

Covers are step four. Before any product gets packaged, the assets already went through make_metadata.py, which writes the exact upload CSVs each marketplace demands: Adobe's 5-column header, Vecteezy's 4-column, Dreamstime's 15-column. All three reject a wrong header instantly, so the script writes the header they demand and nothing else. And upload_tracker.py keeps a ledger of what has been posted, because running a batch twice is the fastest way to get an account flagged.

The point is that the cover is not a design decision in isolation. It is the last step of a chain, and if any earlier step is wrong, the prettiest cover in the world sits on top of a rejected upload. That is why the kit is four small scripts and not one monolith. Two of them, make_metadata.py and upload_tracker.py, use only the Python standard library. The other two need exactly two third-party packages: Pillow for covers, reportlab for PDFs.

What I would tell someone starting today

Do not hand-make 39 covers in a design tool. Do not make 39 different cover designs either. One script, one layout, two sizes, variable text. Consistency across a catalogue reads as professionalism even when the layout is simple, and it means packaging product number 40 takes the same thirty seconds as product number 1.

The total output of this pipeline was 39 products, 856 MB, backed by about 19,000 lines of pipeline code and 300+ pages of research. The four scripts are the packaging layer I kept reaching for, so I cleaned them up and documented them as the pipeline starter kit. If you want the same scripts, including the cover renderer with the two sizes and the RAM habits baked in, they are here:

Get pipeline starter kit


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

Top comments (0)