DEV Community

Franco Zanardi
Franco Zanardi

Posted on

Fast, Declarative Open Graph Image Generation in Python

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:

  1. 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.
  2. 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:
Open graph image for this post generated by dev.to

Setup

First, let's install pictex:

pip install pictex
Enter fullscreen mode Exit fullscreen mode

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, and Inter-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")
Enter fullscreen mode Exit fullscreen mode

Partial result:

First partial result: author name, date and author picture

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")
Enter fullscreen mode Exit fullscreen mode

Partial result:

Second partial result: includes author data + dev.to logo

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")
Enter fullscreen mode Exit fullscreen mode

Partial result:

Third partial result: includes author data + dev.to logo + post title

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"
)
Enter fullscreen mode Exit fullscreen mode

Final result:

Final result: open graph image generated with python

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.

Top comments (0)