Install
No framework information available.
README
ImgForge
A .NET CLI tool for generating blog and YouTube thumbnail images from HTML templates.
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:
- 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.
- 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.
- 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 |
| 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:
Copy them from the repository and reference by file path:
imgforge generate --template ./templates/youtube-bold.html --title "My Title" --format youtubeClone 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 looks for a default template starting in the current directory, following the .tools/ directory spec:
.tools/imgforge/template.html(recommended).imgforge/template.html(legacy location, still supported)
If neither exists in the current directory and you are inside a git repository, ImgForge checks each parent directory in turn, up to and including the repository root (the nearest directory containing .git), so you can run it from any subdirectory. It never searches above the repository root, and outside a repository only the current directory is checked. The nearest match wins; within a single directory, .tools/imgforge/template.html takes precedence over .imgforge/template.html.
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
Publishing a Release
Releases are published to NuGet.org automatically by the
Publish to NuGet workflow when a GitHub release is published.
The package version comes from the release tag; the <Version> in src/ImgForge/ImgForge.csproj is a
0.0.0-local placeholder for local builds and does not need to be bumped.
Make sure
maincontains everything for the release and passesdotnet BuildTestFormat.cs(build, tests, and format check).Create a GitHub release with a new tag in the form
vX.Y.Z(orvX.Y.Z-suffixfor a prerelease, e.g.v1.0.0-beta.1), targetingmain. The version must be higher than the latest one on NuGet.org. Write the release notes on the release β the NuGet package's release notes link to it.gh release create v0.1.7 --target main --generate-notesPublishing the release triggers the workflow, which validates the tag, builds and tests with that version, packs, and pushes to NuGet.org. Tags that aren't valid SemVer (e.g.
v0.1.5a,v1.2) fail before anything is pushed.
Running the workflow manually (Actions β Publish to NuGet β Run workflow) performs a dry run: it builds, tests, and
packs a 0.0.0-ci.N package but never pushes it.
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.
Changes from Previous Version
Adoption Assessment
Assess this package against its supported frameworks, license, maintenance signals and dependency graph before making it a project standard.
Related Guides & Workflows
Learn how to evaluate, integrate, or audit this package in your .NET project.