How to Generate a Hero Image using Python.PIL

guides
tutorials
Author

Sam M

Published

June 6, 2024

Instructions for Placing Hero Images and Updating Blog Posts

images/Python-PIL-Generate-Hero-Image.png

1. Generate the hero image and optionally update the .qmd file:

python makeheroimage.py "Your Blog Post Title" -o "blog/images/Python-PIL-Generate-Hero-Image.png" -q "blog/Python-PIL-Generate-Hero-Image.qmd"

2. Place the Hero Image:

  • The script will automatically place the generated image in the specified output path.
  • If the -q option is used, the script will also update the front matter of the specified .qmd file with the image path.

3. Commit and Push the Changes:

  • Add the new image and updated blog post to git:

    git add blog/images/Python-PIL-Generate-Hero-Image.png blog/Python-PIL-Generate-Hero-Image.qmd
  • Commit the changes with a meaningful message:

    git commit -m "Add hero image and new blog post about [topic]"
  • Push the changes to your GitHub repository:

    git push origin main

4. Publish the Blog Post on the Server:

  • SSH into your server where the blog is hosted.

  • Navigate to the project directory on the server.

  • Pull the latest changes from GitHub:

    git pull origin main
  • Render the site with Quarto:

    quarto render

5. Verify the Blog Post:

  • Open your website in a browser to ensure the new post is live and the hero image is displayed correctly.

Instructions for Using the Script

  1. Save the script below as makeheroimage.py.

  2. Ensure you have a compatible font installed (DejaVu Sans, Arial, Monaco or similar).

  3. Run the script from the command line:

    python makeheroimage.py "Your text here" -o "path/to/hero_image.png" -q "path/to/blog/post.qmd"
    # makeheroimage.py
    """
    This script generates a hero image with complementary colors and centered text.
    It can optionally insert/update the hero image in the .qmd front matter.
    requirements: Pillow, PyYAML
    """
    
    import argparse
    from PIL import Image, ImageDraw, ImageFont
    import random
    import os
    import sys
    import yaml
    
    def generate_complementary_colors():
        # Pantone Orange and complementary colors
        colors = [
            ((255, 165, 0), (0, 0, 255)),  # Orange and Blue
            ((255, 127, 80), (75, 0, 130)),  # Coral and Indigo
            ((255, 69, 0), (0, 128, 128)),  # Red-Orange and Teal
            ((255, 140, 0), (0, 206, 209)),  # Dark Orange and Dark Turquoise
        ]
        return random.choice(colors)
    
    def get_font_path():
        # Check for common font paths in different operating systems
        common_paths = [
            "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",  # Linux
            "/System/Library/Fonts/Monaco.ttf",  # macOS
            "C:\\Windows\\Fonts\\Arial.ttf",  # Windows
        ]
        for path in common_paths:
            if os.path.exists(path):
                return path
        print("Error: Font file not found. Please install DejaVu Sans or Arial font.")
        sys.exit(1)
    
    def create_hero_image(text, output_path="hero_image.png"):
        try:
            # Define image dimensions and background color
            width, height = 1200, 600
            background_color, text_color = generate_complementary_colors()
    
            # Create an image
            image = Image.new("RGB", (width, height), color=background_color)
            draw = ImageDraw.Draw(image)
    
            # Define font
            font_path = get_font_path()
            font = ImageFont.truetype(font_path, size=60)
    
            # Calculate text size and position using textbbox
            text_bbox = draw.textbbox((0, 0), text, font=font)
            text_width, text_height = text_bbox[2] - text_bbox[0], text_bbox[3] - text_bbox[1]
            while text_width > width - 140:  # Adjust font size to fit within image width
                font = ImageFont.truetype(font_path, size=font.size - 2)
                text_bbox = draw.textbbox((0, 0), text, font=font)
                text_width, text_height = text_bbox[2] - text_bbox[0], text_bbox[3] - text_bbox[1]
    
            text_x = (width - text_width) / 2
            text_y = (height - text_height) / 2
    
            # Draw the text on the image
            draw.text((text_x, text_y), text, font=font, fill=text_color)
    
            # Save the image
            image.save(output_path)
            print(f"Image saved at {output_path}")
    
        except Exception as e:
            print(f"An error occurred: {e}")
    
    def update_qmd_front_matter(qmd_path, image_path):
        try:
            with open(qmd_path, 'r') as file:
                content = file.read()
    
            # Find the front matter section
            if content.startswith('---'):
                end_index = content.find('---', 3) + 3
                front_matter = content[3:end_index-3]
                body = content[end_index:]
            else:
                front_matter = ""
                body = content
    
            # Parse the front matter
            fm_data = yaml.safe_load(front_matter) if front_matter else {}
    
            # Update the image path to be relative
            fm_data['image'] = os.path.join("images", os.path.basename(image_path))
    
            # Convert the front matter back to a string
            new_front_matter = yaml.dump(fm_data)
    
            # Write the updated content back to the file
            with open(qmd_path, 'w') as file:
                file.write(f"---\n{new_front_matter}---\n{body}")
    
            print(f"Updated {qmd_path} with image {fm_data['image']}")
    
        except Exception as e:
            print(f"An error occurred while updating the .qmd file: {e}")
    
    if __name__ == "__main__":
        parser = argparse.ArgumentParser(description="Generate a hero image with specified text.")
        parser.add_argument("text", type=str, help="Text to display on the hero image")
        parser.add_argument("-o", "--output", type=str, default="hero_image.png", help="Output path for the hero image")
        parser.add_argument("-q", "--qmd", type=str, help="Path to the .qmd file to update front matter with the image")
        args = parser.parse_args()
    
        create_hero_image(args.text, args.output)
    
        if args.qmd:
            update_qmd_front_matter(args.qmd, args.output)

    The hero image for this post and Create-Publish-Quarto-Blog-Post were generated with the script above.