ScreenshotNeo

BlogHow-to

How to Create an Open Graph Image from a Page Title with Python

Render a page title into a shareable image with Pillow, then publish it and connect it to your page with Open Graph metadata.

By the ScreenshotNeo team4 October 20269 min read

Use Pillow to draw a page title onto an image canvas, save the image, publish it at a stable URL, and set that URL as the page’s og:image value. Open Graph metadata points to the image by URL; it does not contain the image bytes. The example below uses a 1200 × 630 canvas as a design choice, not a universal Open Graph requirement. Choose dimensions for the publishing context where the image will appear.

1. Install Pillow and choose a font

Install Pillow in the Python environment that will run the script:

python -m pip install Pillow

Use a font file that is available in your deployment environment. A path on your laptop may not exist in a container or server, so bundle the font with your application or configure a known path. The script expects a TrueType font at FONT_PATH.

2. Render the title with Pillow

This complete script wraps a title to the available width, measures the lines, adjusts the font size if the title is too tall, centers the text, and writes a PNG. Change the title, font path, output path, colors, and dimensions to match your design.

from pathlib import Path
from PIL import Image, ImageDraw, ImageFont

TITLE = "How to Create an Open Graph Image with Python"
FONT_PATH = "./fonts/DejaVuSans-Bold.ttf"
OUTPUT_PATH = "./static/images/open-graph-python.png"
WIDTH, HEIGHT = 1200, 630
PADDING_X = 100
PADDING_Y = 72
MAX_FONT_SIZE = 76
MIN_FONT_SIZE = 38
LINE_SPACING = 16


def wrap_text(draw, text, font, max_width):
    """Wrap whitespace-separated words without splitting individual words."""
    words = text.split()
    if not words:
        return [""]

    lines = []
    current = words[0]
    for word in words[1:]:
        candidate = f"{current} {word}"
        if draw.textlength(candidate, font=font) <= max_width:
            current = candidate
        else:
            lines.append(current)
            current = word
    lines.append(current)
    return lines


def line_height(draw, font):
    box = draw.textbbox((0, 0), "Ag", font=font)
    return box[3] - box[1]


def render_title(title, font_path, output_path):
    image = Image.new("RGB", (WIDTH, HEIGHT), "#10243A")
    draw = ImageDraw.Draw(image)
    max_text_width = WIDTH - 2 * PADDING_X
    max_text_height = HEIGHT - 2 * PADDING_Y

    chosen = None
    for size in range(MAX_FONT_SIZE, MIN_FONT_SIZE - 1, -2):
        font = ImageFont.truetype(font_path, size)
        lines = wrap_text(draw, title, font, max_text_width)
        height = len(lines) * line_height(draw, font) + (len(lines) - 1) * LINE_SPACING
        # Check both measured advance and ink bounds for each line.
        fits_width = all(
            draw.textlength(line, font=font) <= max_text_width
            and (lambda b: b[2] - b[0] <= max_text_width)(draw.textbbox((0, 0), line, font=font))
            for line in lines
        )
        if fits_width and height <= max_text_height:
            chosen = (font, lines, height)
            break

    if chosen is None:
        raise ValueError("Title does not fit. Increase the canvas or padding area, or lower MIN_FONT_SIZE.")

    font, lines, total_height = chosen
    y = (HEIGHT - total_height) // 2
    for line in lines:
        bounds = draw.textbbox((0, 0), line, font=font)
        ink_width = bounds[2] - bounds[0]
        x = (WIDTH - ink_width) // 2 - bounds[0]
        # Offset by the bbox top so the visible ink, rather than its origin, is centered.
        draw.text((x, y - bounds[1]), line, font=font, fill="#FFFFFF")
        y += line_height(draw, font) + LINE_SPACING

    output = Path(output_path)
    output.parent.mkdir(parents=True, exist_ok=True)
    image.save(output, format="PNG", optimize=True)
    print(f"Wrote {output} ({WIDTH}x{HEIGHT})")


if __name__ == "__main__":
    render_title(TITLE, FONT_PATH, OUTPUT_PATH)

Run it from the project directory:

python create_og_image.py

For reproducible output, keep the font file and design values under version control or otherwise make them part of the deployment configuration. If you need an image with a photograph, gradient, brand mark, or additional visual elements, draw those onto the canvas before drawing the title.

3. Understand text measurement and layout

ImageDraw.textlength returns the rendered text advance in pixels. A text bounding box measures the visible extents; the two can differ, for example with italic glyphs or accents. The code checks both width measures for each line and uses the bounding box to center the visible ink. Font kerning can also mean that the measured widths of separate strings do not sum to the width of their concatenation, so measure complete candidate lines rather than adding individual word widths. See Pillow’s ImageDraw documentation for measurement and drawing methods, and its ImageFont documentation for font loading.

The sample wrapper breaks only at whitespace. A very long word or URL may exceed the canvas even at the minimum size; the script reports a fit error rather than silently clipping. For those titles, choose a smaller minimum font, increase the canvas, or add explicit break opportunities. For multilingual text, confirm that the selected font contains the needed glyphs and check the rendered result in the target environment.

4. Save the image in the required format

The example saves optimized PNG. Pillow can infer the format from a filename extension, or you can pass a format explicitly. For JPEG output, convert to RGB and choose a quality setting; JPEG does not preserve transparency. For WebP, confirm that the Pillow build you deploy supports it.

# JPEG example
image.convert("RGB").save("./static/images/open-graph-python.jpg", format="JPEG", quality=88, optimize=True)

# WebP example
image.save("./static/images/open-graph-python.webp", format="WEBP", quality=88, method=6)

These snippets assume image is the generated Pillow image. If saving to a file-like object instead of a named path, pass format="PNG" (or the desired format) explicitly because there may be no extension from which Pillow can infer it. See Pillow’s Image.save reference.

5. Publish the file and add Open Graph metadata

Make the generated image available at a stable, publicly retrievable URL, then include that URL in the page head. Replace the sample page URL and image URL with the real canonical page and deployed image addresses.

<head>
  <meta property="og:title" content="How to Create an Open Graph Image with Python">
  <meta property="og:type" content="article">
  <meta property="og:url" content="https://example.com/guides/open-graph-python/">
  <meta property="og:image" content="https://example.com/static/images/open-graph-python.png">
  <meta property="og:image:alt" content="A page title rendered as a shareable graphic on a dark blue background">
  <meta property="og:image:type" content="image/png">
  <meta property="og:image:width" content="1200">
  <meta property="og:image:height" content="630">
</head>

The Open Graph Protocol defines og:title, og:type, og:image, and og:url as its basic properties. When specifying an image, provide og:image:alt; image MIME type, width, and height are optional structured properties. The protocol does not set one universal pixel dimension. Consult the Open Graph Protocol basics and its structured image properties, and separately check current requirements for the particular platform where the page will be shared.

6. Generate images for many pages

For a site with title-driven pages, move the rendering logic into a function and pass each page’s title and output path. Keep dimensions, padding, font, colors, and line spacing centralized so images share a consistent layout. Escape or validate title input according to your data source; Pillow draws text rather than interpreting it as HTML, but extremely long or empty titles still need a design policy. You can use a fallback label for empty titles, and cap title length or select an alternate layout for titles that do not fit.

For a build step, write each image to a predictable location and use the matching public URL in that page’s metadata. If titles change, regenerate the corresponding asset and ensure the deployed page points to the updated file. A content hash in the filename can help avoid stale cached assets, provided the HTML metadata is updated to the same URL.

7. Performance, reliability, and cost

For a single static title card, rendering is local work and requires no screenshot service. The main operational considerations are font availability, repeatable design settings, output encoding, and serving the image from a URL that the intended platform can fetch. No performance measurements are assumed here; if generating many images in a build, measure the actual workload and avoid regenerating unchanged assets when your build system can identify them.

PNG is a straightforward lossless output for text and flat colors, while JPEG and WebP may reduce file size depending on content and encoding settings. Compare the actual output quality and size for your artwork. Costs depend on where and how you run the script and host the image; the Pillow workflow itself does not require a paid image API.

Or skip the browser setup

If the image you need is a screenshot of a rendered page rather than a title card drawn from scratch, ScreenshotNeo can return an image from one API request. See the API documentation for options and response details.

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)

ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing, and response headers identify the page verdict and billing status. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots.

Sign up for 1,000 free screenshots a month, with no card required.

Troubleshooting

Symptom Likely cause Fix
OSError: cannot open resource The font path is wrong or the font is not deployed. Use an absolute or project-relative path that exists in the running environment; package the font with the application.
Text is clipped or nearly touches the edge Advance width and visible glyph bounds differ, or the available area is too small. Measure with both textlength and textbbox, increase padding, reduce font size, or enlarge the canvas; inspect the rendered PNG.
Unsupported character appears as a box The chosen font lacks that glyph. Use a font with the required character coverage and check the exact deployed font file.
Text does not fit even at the minimum size A long word, title, or narrow canvas exceeds the wrapper’s limits. Increase dimensions, decrease the minimum size, provide deliberate line breaks, or use a layout intended for long titles.
The image looks stale after changing a title A cached asset or page metadata still refers to an old image URL. Regenerate and deploy the file, then update og:image; use a versioned filename when appropriate.
A sharing preview does not show the expected image The URL may be incorrect or unavailable to the platform, metadata may be missing or stale, or the platform may apply its own rules. Check that the metadata contains the deployed absolute image URL, verify the file can be fetched in the target environment, and consult that platform’s current official documentation. The Open Graph Protocol alone does not guarantee a preview’s rendering behavior.
JPEG save fails for an image with transparency JPEG does not support an alpha channel. Convert to RGB, choosing a background color first if transparent pixels need a particular appearance.

FAQ

Does Open Graph store the image in the HTML?

No. The page metadata names the image URL in og:image; the image file is served separately.

Is 1200 × 630 required?

No universal size is established by the cited Open Graph Protocol. Pick dimensions for the target context and check that platform’s current guidance.

Can I use a different Python imaging library?

Yes, but this guide uses Pillow because its documented font loading, drawing, text measurement, and saving APIs cover the full title-card workflow.

Can I use ScreenshotNeo to generate this exact title card?

ScreenshotNeo captures rendered web pages; for a graphic composed from a title, shapes, and custom artwork, the Pillow canvas workflow gives direct control over those elements.