Skip to content

ardalis/ImgForge

Repository files navigation

ImgForge

A .NET CLI tool for generating blog and YouTube thumbnail images from HTML templates.

Read the full docs

Getting Started

Install as a global tool:

dotnet tool install --global ImgForge

Or run directly from NuGet via DNX (requires -- before the arguments that should be sent to imgforge):

dnx -y imgforge -- generate --help

Quick Examples

Generate a blog post Open Graph image with a random background:

dnx -y imgforge -- generate --template blog --title "Getting Started with ImgForge" --bg random --format blog

Create a YouTube thumbnail:

dnx -y imgforge -- generate --template youtube --title "How to Build Better Software" --bg random --format youtube

Generate a blog card with subtitle and headshot:

dnx -y imgforge -- generate --template blog-subtitle \
  --title "Advanced .NET Patterns" \
  --subtitle "A practical guide" \
  --bg random \
  --headshot ./photo.jpg \
  --format blog

All three built-in templates (blog, youtube, blog-subtitle) work immediately without any additional setup. Chromium is downloaded automatically on first run.

How It Works

ImgForge follows a three-stage pipeline:

  1. Template rendering — An HTML template (built-in or custom) is loaded and variable placeholders are replaced with the supplied values (title, background, dimensions, overlays) using Scriban.
  2. Browser rendering — The resulting HTML is loaded into a headless Chromium browser via PuppeteerSharp, which applies all CSS layout and styling exactly as a real browser would.
  3. Screenshot capture — PuppeteerSharp screenshots the rendered page at the specified viewport size and writes a PNG to the output path.
GenerateOptions
      │
      ▼
TemplateRenderer  (Scriban: injects title, bg, width, height, overlays into HTML)
      │
      ▼
    HTML string
      │
      ▼
ImageGenerator    (PuppeteerSharp: loads HTML in headless Chromium, screenshots to PNG)
      │
      ▼
    output.png

Dependencies

Package Role
Scriban Liquid-syntax template engine — substitutes {{ title }}, {{ bg }}, {{ width }}, {{ height }}, and {% for img in overlays %} in HTML templates
PuppeteerSharp Headless Chromium driver — renders the HTML with full CSS support and captures a pixel-perfect screenshot. Chromium is downloaded automatically on first run.
System.CommandLine Parses CLI arguments and subcommands
SixLabors.ImageSharp Used in tests to verify output PNG dimensions
xunit Test framework

Usage

imgforge generate \
  --template blog \             # built-in name ("blog", "youtube") or path to a .html file or folder (with a template.html file in it)
  --title "Modular Monoliths Done Right" \
  --subtitle "A practical guide" \  # optional subtitle rendered below the title in templates that support it
  --bg ./images/cover.jpg \     # local path, HTTP(S) URL, or "random" (fetches a random image from picsum.photos); optional
  --overlay ./logo.png \        # optional; repeat for multiple overlays
  --headshot ./guest.jpg \      # optional guest headshot; placed in a styled circle
  --headshot-filter blue-mono \ # built-in: blue-mono (default), mono, none — or a raw CSS filter string
  --var season=3 \              # arbitrary key=value pairs injected as {{ vars.season }} in templates; repeat for multiple
  --format podcast-episode \    # sets width/height from a named preset (see formats below)
  --out og.png \                # output file path; defaults to title slug in current directory
  --out-dir ./output \          # output directory; filename derived from --title; ignored if --out is provided
  --width 1200 \                # explicit width — overrides --format; prompted if neither is given
  --height 630                  # explicit height — overrides --format; prompted if neither is given

All flags

Flag Required Default Description
--template Yes Built-in template name (blog, youtube, blog-subtitle) or path to a .html file or a directory containing template.html
--title Yes Main heading text injected into the template
--subtitle No Optional subtitle rendered below the title in templates that support it (e.g. blog-subtitle)
--bg No Background image: local file path, HTTP(S) URL, or random (fetches a random image from picsum.photos)
--overlay No Overlay image path. Repeatable for multiple overlays
--headshot No Guest headshot image path placed in the template's headshot slot
--headshot-filter No blue-mono Filter applied to the headshot. Built-in: blue-mono, mono, none. Or supply a raw CSS filter string
--var No Arbitrary template variable as key=value (e.g. --var episode=42). Accessible in templates as {{ vars.episode }}. Repeatable
--format No Output format preset that sets width and height. Choices: youtube, blog, github, podcast-show, podcast-episode. Explicit --width/--height override the preset
--out No title slug .png Output PNG file path
--out-dir No . Output directory. Filename is derived from --title. Ignored if --out is provided
--width No 1200 Viewport width in pixels. Overrides --format
--height No 630 Viewport height in pixels. Overrides --format

Format presets

Use --format <name> instead of --width/--height. Explicit dimensions always override the preset. If neither --format nor explicit dimensions are provided, the tool prompts you interactively.

--format value Width Height Use case
youtube 1280 720 YouTube video thumbnail
blog / og 1200 630 Blog post / Open Graph social card
github 1280 640 GitHub repository social preview
podcast-show 3000 3000 Podcast show art
podcast-episode 3000 3000 Podcast episode art

Image Dimension Reference

Ideal dimensions vary by platform and use case. Use --width and --height to match:

Use Case Width Height Aspect Ratio Built-in Template Repository Template Notes
YouTube Video Thumbnail 1280 720 16:9 youtube youtube-bold.html Minimum 640×360; displayed at up to 1280×720
Blog / Open Graph (og:image) 1200 630 ~1.91:1 blog, blog-subtitle blog-gradient.html Recommended by Facebook, LinkedIn, Slack, and most social crawlers. Twitter also accepts this size.
GitHub Repository Social Preview 1280 640 2:1 github-social.html Displayed at 640×320 on repository pages
Podcast Show Art 3000 3000 1:1 podcast-show.html Apple Podcasts minimum 1400×1400; Spotify and most platforms prefer 3000×3000, max 512 KB
Podcast Episode Art 3000 3000 1:1 podcast-episode.html Same requirements as show art; some hosts fall back to show art if omitted

Quick reference commands

# YouTube thumbnail (using --format preset)
imgforge generate --template youtube --title "My Video Title" --bg ./bg.jpg --out thumb.png --format youtube

# YouTube thumbnail with a random background
imgforge generate --template youtube --title "My Video Title" --bg random --out thumb.png --format youtube

# YouTube thumbnail with guest headshot (blue monochrome filter)
imgforge generate --template youtube --title "Building Better APIs" \
  --headshot ./guest.jpg --out thumb.png --format youtube

# Blog / Open Graph card
imgforge generate --template blog --title "My Post Title" --bg ./cover.jpg --out og.png --format blog

# NimblePros blog card with subtitle and headshot
imgforge generate --template blog-subtitle --title "Next Level AI Agents" \
  --subtitle "MCP Servers" --bg ./cover.jpg \
  --headshot ./logo.png --format blog --out og.png

# GitHub social preview
imgforge generate --template templates/github-social.html --title "my-repo" --bg ./cover.jpg --out social-preview.png --format github

# Podcast show art
imgforge generate --template templates/podcast-show.html --title "My Podcast" --out podcast-show.png --format podcast-show

# Podcast episode art with guest headshot, season/episode labels, and named show in footer
imgforge generate --template templates/podcast-episode.html \
  --title "Build vs Buy with guest Ardalis" \
  --headshot ./guest.jpg --headshot-filter blue-mono \
  --var season=3 --var episode=42 --var show="It Depends by NimblePros" \
  --out podcast-episode.png --format podcast-episode

# Podcast episode art — greyscale headshot instead of blue tint
imgforge generate --template templates/podcast-episode.html --title "My Episode Title" \
  --headshot ./guest.jpg --headshot-filter mono --out podcast-episode.png --format podcast-episode

# Write output to a specific directory (filename derived from title)
imgforge generate --template blog --title "My Post Title" --bg ./cover.jpg --out-dir ./output --format blog

# Omit --format and --width/--height to be prompted interactively
imgforge generate --template templates/podcast-episode.html --title "My Episode Title" --out out.png

# Omit --out and --out-dir to use title as the output filename in the current directory
imgforge generate --template youtube --title "This is the title of the show" --format youtube

Built-in Templates

ImgForge includes 3 embedded templates that are always available when you install the tool:

Template Name Default Dimensions Description Usage
blog 1200×630 Open Graph / social preview card with centered title --template blog
youtube 1280×720 YouTube thumbnail with bold title styling --template youtube
blog-subtitle 1200×630 Open Graph card with faded background, optional subtitle, and headshot support --template blog-subtitle

These templates are embedded in the tool's DLL and work immediately after installation—no need to download template files.

Example Usage

# Simple blog Open Graph image
imgforge generate --template blog --title "My Blog Post" --bg random --format blog

# YouTube thumbnail
imgforge generate --template youtube --title "My Video Title" --bg random --format youtube

# Blog card with subtitle and headshot
imgforge generate --template blog-subtitle \
  --title "Building Modular Monoliths" \
  --subtitle "A Practical Guide" \
  --bg ./cover.jpg \
  --headshot ./author.jpg \
  --format blog

Additional Templates in Repository

The repository's templates/ folder contains additional example templates (like youtube-bold.html, podcast-episode.html, github-social.html) that are not embedded in the tool. To use these, either:

  1. Copy them from the repository and reference by file path:

    imgforge generate --template ./templates/youtube-bold.html --title "My Title" --format youtube
  2. Clone the repo and run from source to access all example templates

Custom Templates

Any .html file (or a folder with a template.html file in it) can be used as a template. If you omit --template, ImgForge uses /.imgforge/index.html as the default custom template path. Scriban Liquid syntax is supported for variable injection:

<html>
<body style="width:{{ width }}px; height:{{ height }}px; margin:0;
             background-image:url('{{ bg }}'); background-size:cover;
             display:flex; align-items:center; justify-content:center;
             font-family:sans-serif; color:white;">
  <h1 style="font-size:64px; text-align:center;">{{ title }}</h1>
  {% for img in overlays %}
  <img src="{{ img.src }}" style="position:absolute; {{ img.style }}" />
  {% endfor %}
  {% if headshot %}
  <img src="{{ headshot.src }}" style="filter:{{ headshot.filter_css }};" />
  {% endif %}
  <p>Season {{ vars.season }}, Episode {{ vars.episode }}</p>
</body>
</html>

Template variables

Variable Type Description
title string Main heading text
subtitle string Optional subtitle text — empty string when --subtitle is not supplied; guard with {% if subtitle %} in templates
bg string Background image URI — local file:/// paths, HTTP(S) URLs, and random picsum URLs are all resolved before injection
width int Viewport width in pixels
height int Viewport height in pixels
overlays array List of { src, style } overlay image objects
headshot object or null Guest headshot — exposes headshot.src (file URI) and headshot.filter_css (CSS filter string). null when --headshot is not supplied
vars object Arbitrary key/value pairs supplied via --var key=value. Access as {{ vars.key }}

Headshot filters

Pass a name to --headshot-filter or supply any raw CSS filter string.

Name CSS applied Effect
blue-mono (default) grayscale(100%) sepia(100%) hue-rotate(190deg) saturate(300%) brightness(1.15) Light-blue monochrome — "black and white but in shades of blue"
mono grayscale(100%) True greyscale
none none No filter; original colours kept
(custom) (your string) Any valid CSS filter string, e.g. sepia(80%) hue-rotate(270deg)

Custom template directories

When you pass a directory path to --template, ImgForge looks for template.html inside it and injects a <base> tag so that relative image references (e.g. a watermark sitting next to template.html) resolve correctly.

my-template/
  template.html
  watermark.png   ← referenced as just "watermark.png" inside the template
imgforge generate --template ./my-template --title "Hello" --format blog

Build and Test

dotnet build

# Unit tests (no browser required)
dotnet test --filter "Category!=Integration"

# Full test suite including integration tests (Chromium is downloaded automatically on first run)
dotnet test

Install as a Global Tool

dotnet tool install --global ImgForge
imgforge generate --template blog --title "Hello!" --bg random --format blog

Chromium is downloaded automatically on the first run — no separate install step needed.

Run Directly from NuGet via DNX

You can run ImgForge without installing it globally:

# Simple example
dnx -y imgforge -- generate --template blog --title "Hello World" --bg random --format blog

# YouTube thumbnail
dnx -y imgforge -- generate --template youtube --title "My Video" --bg random --format youtube

# Blog post with subtitle
dnx -y imgforge -- generate --template blog-subtitle \
  --title "Advanced Techniques" \
  --subtitle "Learn the secrets" \
  --bg random \
  --format blog

The -y flag automatically accepts any prompts, making it perfect for quick one-off image generation.

About

A tool for producing images from templates with text overlays

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors