Files
Crosspoint/scripts/sd_font_preview_generation_new.ipynb
2026-05-04 15:00:44 +02:00

16 KiB

SD Font Preview Generation

This notebook scans ./assets/sd-fonts, generates preview images for each available SD font family, and creates a markdown listing page that embeds those images.

In [ ]:
import urllib.request
import urllib.parse
from pathlib import Path
from typing import Optional
from PIL import Image, ImageDraw, ImageFont
from IPython.display import display, Markdown
import yaml

repo_root = Path.cwd().resolve().parent
root_dir = repo_root / 'assets' / 'sd-fonts'
output_image_dir = repo_root / 'docs' / 'images' / 'sd-font-previews'
output_image_dir.mkdir(parents=True, exist_ok=True)
output_markdown_path = repo_root / 'docs' / 'sd-fonts-preview.md'
manifest_path = repo_root / 'lib' / 'EpdFont' / 'scripts' / 'sd-fonts.yaml'
cache_dir = repo_root / 'scripts' / 'font-cache'
cache_dir.mkdir(parents=True, exist_ok=True)

with manifest_path.open('r', encoding='utf-8') as manifest_file:
    manifest = yaml.safe_load(manifest_file)

family_map = {family['name']: family for family in manifest['families']}

print(f'Repo root: {repo_root}')
print(f'Root font directory: {root_dir.resolve()}')
print(f'Preview image output directory: {output_image_dir.resolve()}')
print(f'Markdown output path: {output_markdown_path.resolve()}')
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[9], line 15
     13 output_markdown_path = repo_root / 'docs' / 'sd-fonts-preview.md'
     14 manifest_path = repo_root / 'lib' / 'EpdFont' / 'scripts' / 'sd-fonts.yaml'
---> 15 cache_dir = Path(__file__).resolve().parent / 'font-cache'
     16 cache_dir.mkdir(parents=True, exist_ok=True)
     18 with manifest_path.open('r', encoding='utf-8') as manifest_file:

NameError: name '__file__' is not defined
In [8]:
# Discover available font families
font_families = [p.name for p in sorted(root_dir.iterdir()) if p.is_dir()]
print(f'Found {len(font_families)} font families:')
for family in font_families:
    print(f' - {family}')


def download_font(url: str, cache_dir: Path) -> Path:
    parsed = urllib.parse.urlparse(url)
    filename = Path(parsed.path).name
    local_path = cache_dir / filename
    if not local_path.exists():
        print(f'Downloading {url}...')
        urllib.request.urlretrieve(url, local_path)
    return local_path


def resolve_instanced_font_path(family_name: str, style_name: str) -> Optional[Path]:
    instanced_dir = repo_root / 'lib' / 'EpdFont' / 'scripts' / 'instanced_fonts' / family_name
    if not instanced_dir.exists():
        return None
    matches = sorted(instanced_dir.glob(f'{style_name}*.ttf'))
    return matches[0] if matches else None


def resolve_font_path(family_name: str) -> Optional[Path]:
    family = family_map.get(family_name)
    if family is None:
        return None
    styles = family.get('styles', {})
    style = styles.get('regular') or next(iter(styles.values()), None)
    if style is None:
        return None

    if 'path' in style:
        local_path = repo_root / 'lib' / 'EpdFont' / style['path']
        if local_path.exists():
            return local_path

    instanced = resolve_instanced_font_path(family_name, 'regular')
    if instanced is not None:
        return instanced

    if 'url' in style:
        return download_font(style['url'], cache_dir)
    return None
Found 23 font families:
 - Alegreya
 - Arimo
 - AtkinsonHyperlegibleNext
 - Bitter
 - Gelasio
 - GentiumBookPlus
 - IBMPlexMono
 - IBMPlexSans
 - IBMPlexSerif
 - Inter
 - LexicaUltralegible
 - Literata
 - Lora
 - Merriweather
 - NotoSansExtended
 - NotoSerifExtended
 - OpenDyslexic
 - Readerly
 - SourceCodePro
 - SourceSans3
 - SourceSerif4
 - Spectral
 - Vollkorn
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[8], line 18
     14         urllib.request.urlretrieve(url, local_path)
     15     return local_path
---> 18 def resolve_instanced_font_path(family_name: str, style_name: str) -> Optional[Path]:
     19     instanced_dir = repo_root / 'lib' / 'EpdFont' / 'scripts' / 'instanced_fonts' / family_name
     20     if not instanced_dir.exists():

NameError: name 'Optional' is not defined
In [4]:
sample_text = 'Everyone has the right to freedom of thought...'
preview_texts = {family: f'{family} - {sample_text}' for family in font_families}

for family, text in preview_texts.items():
    print(f'{family}: {text}')
Alegreya: Alegreya - Everyone has the right to freedom of thought...
Arimo: Arimo - Everyone has the right to freedom of thought...
AtkinsonHyperlegibleNext: AtkinsonHyperlegibleNext - Everyone has the right to freedom of thought...
Bitter: Bitter - Everyone has the right to freedom of thought...
Gelasio: Gelasio - Everyone has the right to freedom of thought...
GentiumBookPlus: GentiumBookPlus - Everyone has the right to freedom of thought...
IBMPlexMono: IBMPlexMono - Everyone has the right to freedom of thought...
IBMPlexSans: IBMPlexSans - Everyone has the right to freedom of thought...
IBMPlexSerif: IBMPlexSerif - Everyone has the right to freedom of thought...
Inter: Inter - Everyone has the right to freedom of thought...
LexicaUltralegible: LexicaUltralegible - Everyone has the right to freedom of thought...
Literata: Literata - Everyone has the right to freedom of thought...
Lora: Lora - Everyone has the right to freedom of thought...
Merriweather: Merriweather - Everyone has the right to freedom of thought...
NotoSansExtended: NotoSansExtended - Everyone has the right to freedom of thought...
NotoSerifExtended: NotoSerifExtended - Everyone has the right to freedom of thought...
OpenDyslexic: OpenDyslexic - Everyone has the right to freedom of thought...
Readerly: Readerly - Everyone has the right to freedom of thought...
SourceCodePro: SourceCodePro - Everyone has the right to freedom of thought...
SourceSans3: SourceSans3 - Everyone has the right to freedom of thought...
SourceSerif4: SourceSerif4 - Everyone has the right to freedom of thought...
Spectral: Spectral - Everyone has the right to freedom of thought...
Vollkorn: Vollkorn - Everyone has the right to freedom of thought...
In [6]:
# Render and save preview images
font_size = 28
image_width = 1080
image_height = 220

for family, text in preview_texts.items():
    font_path = resolve_font_path(family)
    try:
        if font_path is None:
            raise FileNotFoundError('No source font path available')
        font = ImageFont.truetype(str(font_path), font_size)
    except Exception as exc:
        print(f'Warning: could not load {family} from {font_path}: {exc}')
        font = ImageFont.load_default()

    image = Image.new('RGB', (image_width, image_height), 'white')
    draw = ImageDraw.Draw(image)
    bbox = draw.textbbox((0, 0), text, font=font)
    text_width = bbox[2] - bbox[0]
    text_height = bbox[3] - bbox[1]
    x = 20
    y = (image_height - text_height) // 2
    draw.text((x, y), text, fill='black', font=font)
    output_path = output_image_dir / f'{family}.png'
    image.save(output_path)
    print(f'Saved {output_path}')

print('All preview images generated.')
Saved C:\_development\crosspoint-reader\docs\images\sd-font-previews\Alegreya.png
Saved C:\_development\crosspoint-reader\docs\images\sd-font-previews\Arimo.png
Saved C:\_development\crosspoint-reader\docs\images\sd-font-previews\AtkinsonHyperlegibleNext.png
Saved C:\_development\crosspoint-reader\docs\images\sd-font-previews\Bitter.png
Saved C:\_development\crosspoint-reader\docs\images\sd-font-previews\Gelasio.png
Saved C:\_development\crosspoint-reader\docs\images\sd-font-previews\GentiumBookPlus.png
Saved C:\_development\crosspoint-reader\docs\images\sd-font-previews\IBMPlexMono.png
Saved C:\_development\crosspoint-reader\docs\images\sd-font-previews\IBMPlexSans.png
Saved C:\_development\crosspoint-reader\docs\images\sd-font-previews\IBMPlexSerif.png
Saved C:\_development\crosspoint-reader\docs\images\sd-font-previews\Inter.png
Saved C:\_development\crosspoint-reader\docs\images\sd-font-previews\LexicaUltralegible.png
Saved C:\_development\crosspoint-reader\docs\images\sd-font-previews\Literata.png
Saved C:\_development\crosspoint-reader\docs\images\sd-font-previews\Lora.png
Saved C:\_development\crosspoint-reader\docs\images\sd-font-previews\Merriweather.png
Saved C:\_development\crosspoint-reader\docs\images\sd-font-previews\NotoSansExtended.png
Saved C:\_development\crosspoint-reader\docs\images\sd-font-previews\NotoSerifExtended.png
Saved C:\_development\crosspoint-reader\docs\images\sd-font-previews\OpenDyslexic.png
Saved C:\_development\crosspoint-reader\docs\images\sd-font-previews\Readerly.png
Saved C:\_development\crosspoint-reader\docs\images\sd-font-previews\SourceCodePro.png
Saved C:\_development\crosspoint-reader\docs\images\sd-font-previews\SourceSans3.png
Saved C:\_development\crosspoint-reader\docs\images\sd-font-previews\SourceSerif4.png
Saved C:\_development\crosspoint-reader\docs\images\sd-font-previews\Spectral.png
Saved C:\_development\crosspoint-reader\docs\images\sd-font-previews\Vollkorn.png
All preview images generated.
In [7]:
# Generate markdown listing with embedded preview images
lines = [
    '# SD Font Preview',
    '',
    'This page lists the available SD font families under `assets/sd-fonts` and embeds rendered preview images for each family.',
    '',
    '| Font Family | Preview |',
    '|---|---|',
]

for family in font_families:
    image_rel_path = f'images/sd-font-previews/{family}.png'
    lines.append(f'| {family} | ![Preview of {family}]({image_rel_path}) |')

output_markdown_path.write_text('\n'.join(lines) + '\n', encoding='utf-8')
print(f'Generated markdown page at {output_markdown_path}')

# Display a preview of the generated markdown in the notebook
display(Markdown('\n'.join(lines[:8]) + '\n...'))
Generated markdown page at C:\_development\crosspoint-reader\docs\sd-fonts-preview.md
<IPython.core.display.Markdown object>