Hwaro::Content::Seo::OgPngRenderer
Inherits Reference / Object
Renders OG images directly as PNG using stb_truetype + stb_image_write. No external tools required — all rendering is done in-process.
Constants
Bold font variants to search for
Bundled DejaVu Sans Bold font (compiled into the binary as the wide-coverage Latin/Cyrillic/Greek fallback at the end of every font chain).
Bundled brand fonts (OFL 1.1 — license texts live next to the
TTFs). Space Grotesk carries titles and descriptions; JetBrains
Mono carries the terminal style. Static instances, not variable
fonts — stb_truetype ignores variation axes.
CJK-capable system fonts. A title/description containing Hangul, kana, or Han ideographs renders as blank "tofu" boxes in the Latin-only fonts above; these cover CJK and Latin, so swapping the whole font to one of these renders mixed "Noir v1.0 — 한국어" lines correctly. Ordered by coverage breadth, then likelihood of being installed.
System font search paths (platform-dependent)
Class methods
Pre-render the config-only layers (background fill, background image + overlay, style pattern, top accent bar) into a reusable RGBA buffer that can be memcpy'd into each per-page pixel buffer. On large sites these layers account for the bulk of the per-page cost — the "gradient" pattern alone touches every one of the 756,000 pixels in Crystal-level math — so doing them once per build instead of once per page is the largest single win in PNG OG generation.
Z-order is preserved: the original render order is bg → bg image → pattern → top bar → text → logo → bottom bar, and the remaining per-page work (text, logo, bottom bar) is layered on top of this base in the same order.
Measure text at px_size across the chain. tracking adds a
fixed per-character advance (used for eyebrow/brand labels).
Render text at px_size with per-glyph font fallback. y_top
is the top of the text box (same convention as
hwaro_font_render_text: baseline = y_top + scale * ascent).
Returns the final x position.
Chain variant: drop a codepoint only when no font in the chain covers it.
Drop codepoints info has no glyph for. stb draws missing glyphs
as blank "tofu" boxes — emoji are the common case, since color
emoji fonts can't be loaded here — so degrade "🔥 Title" to
"Title" instead of "☐ Title". Whitespace always passes; leftover
double spaces from removed runs are collapsed. Returns the
original string untouched when everything is drawable.
Try to find a system font, returns file path or nil
True if the font defines a glyph for codepoint (delegates to stb's
glyph lookup). Used to verify CJK coverage.
Load fonts once, return a reusable context of per-role fallback chains. Priority within every chain: user font_path → bundled brand font (Space Grotesk / JetBrains Mono) → CJK-capable system font when the content needs it → bundled DejaVu Sans Bold (wide coverage). A user font no longer replaces everything — it merely leads the chain, so glyphs it lacks still fall back to the bundled fonts. Results are memoized so repeated calls (fast-start priority + deferred passes, or watch rebuilds) do not re-scan the filesystem or re-parse TTF data.
Decode an image file and resize it to target dimensions. Returns nil on failure.
Parse "#RRGGBB" to 0xRRGGBB. Also accepts "#rgb" shorthand and "#rrggbbaa" (alpha dropped); falls back to black for invalid input.
Render OG image directly to PNG file. Returns true on success. Accepts optional pre-loaded CachedImage for logo/background to avoid repeated decode+resize when generating many pages.
When base_layer is given, the config-only layers (background,
pattern, top accent bar) are memcpy'd from it instead of being
re-rendered per page. Callers that render many pages should
build the base layer once via build_base_layer and pass it
in here.