A Python Script That Draws Your Own Marketing Graphics

marketing/generate-images.py draws a profile picture, cover photo, and three announcement posts as hand-coded SVG, then shells out to ImageMagick to rasterize them — no design tool involved.

A Python Script That Draws Your Own Marketing Graphics

Six PNG files — a 1080×1080 profile picture, a 1640×924 cover photo, three 1200×1200 announcement posts, and a 1200×630 link-preview banner — all come out of one Python script, marketing/generate-images.py, with no design software involved anywhere in the pipeline. The script builds each image as raw SVG markup — a fish, some waves, a gradient built from precomputed color bands — then calls out to ImageMagick's convert to rasterize it. It's not a templating system pointed at a design tool's export API; it's SVG as a string, assembled with f-strings.

One script draws the whole marketing kit
One script draws the whole marketing kit

convert file.svg file.png — six brand-consistent assets, zero design software.

Gradients and transparency, faked because the renderer doesn't support them

The most interesting constraint in the script is a comment buried in the bg_bands helper: "the renderer ignores gradients." Rather than fight that, every gradient in the whole marketing kit is precomputed as a stack of solid horizontal bands:

def bg_bands(w, h, top=DEEP2, bottom=DEEP, steps=24):
    t = tuple(int(top[i:i+2], 16) for i in (1, 3, 5))
    b = tuple(int(bottom[i:i+2], 16) for i in (1, 3, 5))
    out = []
    for i in range(steps):
        f = i / (steps - 1)
        c = '#%02x%02x%02x' % tuple(round(t[j] + (b[j] - t[j]) * f) for j in range(3))
        out.append(f'<rect x="0" y="{h*i/steps:.1f}" width="{w}" height="{h/steps+1:.1f}" fill="{c}"/>')
    return '\n  '.join(out)

Twenty-four solid rectangles, each a slightly different interpolated color, stacked to fake a smooth vertical fade. The same trick handles the "faded transparent" look on wave overlays: mix() blends two hex colors at a given opacity fraction and returns a solid color, because the actual opacity attribute wasn't rendering reliably in whatever pipeline eventually consumes these SVGs before the convert step. Working around a rendering limitation with math instead of fighting the tool is the theme of the whole file.

Reusable primitives, not six bespoke images

fish(), wave(), icon_layers(), icon_people(), icon_trophy(), and badge() are each small functions returning an SVG fragment, composed differently per output image:

def badge(x, y, w, h, label, size, fill='#ffffff', bg=CYAN, text_fill=DEEP):
    return (f'<rect x="{x-w/2}" y="{y-h/2}" width="{w}" height="{h}" rx="{h/2}" fill="{bg}"/>'
            + text(x, y + size*0.35, label, size, text_fill))

badge_row() then lays out a centered row of these, sizing each pill to its own label width — the same building block produces the three feature badges on the cover photo and the "NOUVO !" badge on the features-announcement post, without either image's code knowing about the other's layout.

Kreol Morisien first, English as the fallback

The primary copy across all the announcement images and facebook-post.md is written in Kreol Morisien — the language the actual target audience ( Mauritian anglers) uses on Facebook day to day — with an English version kept as block 5 in the same file for anyone the page reaches outside that audience. That's a product decision as much as a marketing one: a fishing app built specifically for Mauritius loses credibility fast if its own announcement posts read like they were translated from somewhere else.

Rendering is one subprocess call

def render(name, svg):
    open(f'{name}.svg', 'w').write(svg)
    subprocess.run(['convert', f'{name}.svg', f'{OUT}/{name}.png'], check=True)
    print('wrote', name)

No image library import, no PIL, no headless browser screenshot pipeline — just a .svg file written to disk and convert invoked as an external process. That's a small dependency footprint for a script that only needs to run occasionally, whenever the feature list changes enough to warrant a new announcement post.

Series: Fishing Tracker Pro. Next: the admin-only tool that turns those images and the weekly leaderboard into an actual Facebook post.