For any web project—be it a blog, an e-commerce site, or a documentation page—dynamically generating Open Graph (OG) images is a crucial feature for social sharing. A compelling preview image can significantly increase click-through rates. The challenge is creating these images efficiently and at scale.
Traditionally, developers in the Python ecosystem face two primary options:
- Manual Rendering (e.g., with Pillow): This approach involves calculating the precise
(x, y)
coordinates for every line of text and every icon. It’s fragile, a longer title can break the entire layout and becomes a nightmare to maintain. - Headless Browsers (e.g., with Selenium/Playwright): This involves rendering an HTML/CSS template in a headless browser and taking a screenshot. Whil…
For any web project—be it a blog, an e-commerce site, or a documentation page—dynamically generating Open Graph (OG) images is a crucial feature for social sharing. A compelling preview image can significantly increase click-through rates. The challenge is creating these images efficiently and at scale.
Traditionally, developers in the Python ecosystem face two primary options:
- Manual Rendering (e.g., with Pillow): This approach involves calculating the precise
(x, y)
coordinates for every line of text and every icon. It’s fragile, a longer title can break the entire layout and becomes a nightmare to maintain. - Headless Browsers (e.g., with Selenium/Playwright): This involves rendering an HTML/CSS template in a headless browser and taking a screenshot. While powerful, it’s notoriously slow and resource-intensive, often requiring heavy dependencies and complex infrastructure.
This article introduces a third approach: a declarative, high-performance method using pictex
, a Python library for visual composition. We’ll show how to build complex, high-quality images without manual coordinate math or the overhead of a browser.
To demonstrate this, we’ll undertake a practical exercise: recreating the default Open Graph image from dev.to.
The Goal: A Real-World Example
Our objective is to write a Python function that generates a high-fidelity recreation of the dev.to card. This will serve as our case study for building maintainable and dynamic image templates.
This is the Open Graph image generated dynamically by dev.to for this post:
Setup
First, let’s install pictex
:
pip install pictex
You will also need a few assets for this project:
- An Author Image: A square profile picture. We’ll assume you have an image named
author.webp
. - The dev.to Logo: We’ll use a WEBP version named
logo.webp
. - Font Files: We’ll use “Inter” for a clean look. Make sure you have
Inter-Bold.ttf
,Inter-SemiBold.ttf
, andInter-Regular.ttf
from Google Fonts.
Step 1: Building the Author Info Component
A key principle of a declarative approach is building complex UIs from small, independent components. Let’s start with the author info block in the bottom-left. It’s a Row
containing a circular author image and a Column
with the name and date.
from pictex import *
# The author's avatar, made circular with .border_radius("50%")
avatar = Image("author.webp").size(70, 70).border_radius("50%")
# The author's name and the date, stacked vertically
author_details = (
Column(
Text("Franco Zanardi").font_size(28).font_family("Inter-SemiBold.ttf"),
Text("Oct 12").font_size(24).font_family("Inter-Regular.ttf").color("#656565")
)
.gap(2) # A 2px gap between name and date
)
# Combine the avatar and details in a horizontal Row
author_info = (
Row(avatar, author_details)
.gap(15)
.vertical_align('center') # Vertically align the avatar with the text block
)
# Save this partial result
Canvas().background_color("white").padding(30).render(author_info).save("partial_result_1.png")
Partial result:
Step 2: Assembling the Bottom Bar
The bottom bar contains our author_info
on the left and the dev_logo
on the right. A Row
with .horizontal_distribution('space-between')
is the ideal tool for this. It automatically pushes its children to the opposite ends of the container.
To make this work reliably, we set the Row
’s width to "100%"
to ensure it expands to fill its parent container.
from pictex import *
# (author_info definition from Step 1)
# ...
dev_logo = Image("logo.webp").size(70, 70)
# 'space-between' pushes the author info to the left and the logo to the right
bottom_bar = (
Row(author_info, dev_logo)
.size(width="100%") # Make the row take up the full parent width
.horizontal_distribution('space-between')
.vertical_align('center')
)
Canvas().background_color("white").padding(30).size(width=1000).render(bottom_bar).save("partial_result_2.png")
Partial result:
Step 3: Creating the Main Content Layout
Now for the core of our design. The main area is a Column
that holds the title and the bottom_bar
.
from pictex import *
# (bottom_bar definition from Step 2)
# ...
title_text = (
Text("Fast, Declarative Open Graph Image Generation in Python")
.font_family("Inter-Bold.ttf")
.font_size(62)
.color("black")
.line_height(1.2)
)
main_content_layout = (
Column(title_text, bottom_bar)
.size("100%", "100%")
.background_color("white")
.padding(65, 80)
.vertical_distribution('space-between')
)
Canvas().size(width=1000, height=500).render(main_content_layout).save("partial_result_3.png")
Partial result:
The Final, Reusable Function
Let’s wrap this logic in a dynamic function to generate a card for any post, making it a practical tool.
from pictex import *
def create_devto_card(title: str, author_name: str, author_image_path: str, date: str, output_path: str):
"""Generates a dev.to-style social media card."""
top_bar = Row().size("100%", 25).background_color("black")
avatar = Image(author_image_path).size(70, 70).border_radius("50%")
author_details = Column(
Text(author_name).font_size(28).font_family("Inter-SemiBold.ttf"),
Text(date).font_size(24).font_family("Inter-Regular.ttf").color("#656565")
).gap(2)
author_info = Row(avatar, author_details).gap(15).vertical_align('center')
dev_logo = Image("logo.webp").size(70, 70)
bottom_bar = Row(author_info, dev_logo).size(width="100%").horizontal_distribution('space-between').vertical_align('center')
title_text = Text(title).font_family("Inter-Bold.ttf").font_size(62).color("black").line_height(1.2)
main_content_layout = (
Column(title_text, bottom_bar)
.size("100%", "fill-available")
.background_color("white")
.padding(65, 80)
.vertical_distribution('space-between')
)
final_layout = Column(top_bar, main_content_layout).size(1000, 500)
canvas = Canvas()
image = canvas.render(final_layout)
image.save(output_path)
print(f"Image saved to {output_path}")
# --- Use the function ---
create_devto_card(
title="Fast, Declarative Open Graph Image Generation in Python",
author_name="Franco Zanardi",
author_image_path="author.webp",
date="Oct 12",
output_path="result.png"
)
Final result:
Conclusion
This exercise demonstrates that a declarative, component-based approach is a highly effective solution for dynamic image generation. Instead of imperative drawing commands, we describe the desired layout, and the library handles the complex rendering logic. This results in code that is not only more readable and maintainable but also significantly faster and less resource-intensive than browser-based alternatives.
If this approach interests you, consider visiting the pictex
repository on GitHub.
Happy coding.