diff --git a/Local_FONT_LOADING.md b/Local_FONT_LOADING.md new file mode 100644 index 000000000..4d2588675 --- /dev/null +++ b/Local_FONT_LOADING.md @@ -0,0 +1,176 @@ +# Font Loading Guide + +This document explains how to use custom fonts with the City Map Poster Generator. + +## Overview + +The application supports three methods for loading fonts, in priority order: + +1. **Google Fonts** (`--font-family`) - Highest priority +2. **Local Font Path** (`--font-path`) - Second priority +3. **Default Roboto** - Automatic fallback + +## Using Local Fonts + +### Single Font File + +Load a specific font file for all text weights (bold, regular, light): + +```bash +python create_map_poster.py -c "Paris" -C "France" --font-path "path/to/font.ttf" +``` + +Examples: +```bash +# Windows absolute path +python create_map_poster.py -c "Paris" -C "France" --font-path "D:\Fonts\CustomFont.ttf" + +# Unix absolute path +python create_map_poster.py -c "Tokyo" -C "Japan" --font-path "/usr/share/fonts/truetype/myfont.ttf" + +# Relative path +python create_map_poster.py -c "Berlin" -C "Germany" --font-path "./fonts/custom.ttf" +``` + +### Font Directory + +Load multiple font files from a directory. The function intelligently detects font weights: + +```bash +python create_map_poster.py -c "Barcelona" -C "Spain" --font-path "path/to/fonts/" +``` + +The function looks for the following patterns (case-insensitive): + +**Bold weight patterns:** +- `*bold*` (e.g., `font-bold.ttf`, `Font_Bold.otf`) +- `*700*` (e.g., `font-700.ttf`) + +**Regular weight patterns:** +- `*regular*` (e.g., `font-regular.ttf`) +- `*400*` (e.g., `font-400.ttf`) +- `*normal*` (e.g., `font-normal.ttf`) + +**Light weight patterns:** +- `*light*` (e.g., `font-light.ttf`) +- `*300*` (e.g., `font-300.ttf`) +- `*thin*` (e.g., `font-thin.ttf`) + +**Example directory structure:** +``` +fonts/ +├── myfont-bold.ttf +├── myfont-regular.ttf +├── myfont-light.ttf +└── alternate-300.otf +``` + +Then use: +```bash +python create_map_poster.py -c "Rome" -C "Italy" --font-path "fonts/" +``` + +### Path Handling + +The function handles paths robustly: + +- **Absolute paths**: `/path/to/font.ttf` or `C:\path\to\font.ttf` +- **Relative paths**: `./fonts/myfont.ttf` or `fonts/myfont.ttf` +- **Home directory expansion**: `~/fonts/myfont.ttf` (expands to user home) +- **Windows & Unix paths**: Automatically handled regardless of OS +- **Spaces in paths**: Fully supported with proper escaping + +## Using Google Fonts + +Download fonts directly from Google Fonts: + +```bash +python create_map_poster.py -c "Berlin" -C "Germany" --font-family "Noto Sans JP" +python create_map_poster.py -c "Bangkok" -C "Thailand" --font-family "Noto Sans Thai" +``` + +Supports any family available on [Google Fonts](https://fonts.google.com/) + +## Supported Font Formats + +- `.ttf` - TrueType Font +- `.otf` - OpenType Font +- `.woff` - Web Open Font Format +- `.woff2` - Web Open Font Format 2 (compressed) + +## Examples + +### Using a custom display font + +```bash +python create_map_poster.py \ + -c "Tokyo" \ + -C "Japan" \ + -t japanese_ink \ + --font-path "C:\MyFonts\NotoSansJP-Bold.ttf" \ + -d 15000 +``` + +### Using a font directory with multiple weights + +```bash +python create_map_poster.py \ + -c "Paris" \ + -C "France" \ + -t pastel_dream \ + --font-path "./custom_fonts/" \ + -d 10000 +``` + +### Combining with other options + +```bash +python create_map_poster.py \ + -c "Barcelona" \ + -C "Spain" \ + --font-path "/usr/share/fonts/custom/" \ + -t warm_beige \ + -W 14 \ + -H 18 \ + --display-city "Barcelona" \ + --display-country "España" +``` + +## Fallback Behavior + +If font loading fails at any stage: + +1. If `--font-family` is provided but fails → tries `--font-path` (if provided) +2. If `--font-path` is provided but fails → uses default Roboto +3. If both fail → uses default Roboto fonts + +The console will show appropriate warning messages about what fallback was used. + +## Troubleshooting + +### "Font path does not exist" +- Check that the path is correct and file/directory exists +- Use absolute paths if relative paths don't work +- On Windows, use `\` or `\\` for path separators (or use forward slashes `/`) + +### "Unsupported font format" +- Only `.ttf`, `.otf`, `.woff`, and `.woff2` are supported +- Convert your font to a supported format if needed + +### "No font files found in directory" +- Make sure font files are directly in the specified directory +- Check that files have a supported extension (.ttf, .otf, etc.) + +### Fonts not appearing in output +- The font file may be corrupt or incompatible +- Try a different font or the default fonts +- Check console output for specific error messages + +## Font Policy + +When using custom fonts: +- Ensure you have the right to use the font for your intended purpose +- Respect font licenses and attribution requirements +- For commercial use, verify font licensing terms +- The generated posters include OpenStreetMap attribution by default + diff --git a/create_map_poster.py b/create_map_poster.py index 3eab412b2..649c8cd44 100755 --- a/create_map_poster.py +++ b/create_map_poster.py @@ -809,6 +809,13 @@ def print_examples(): python create_map_poster.py -c "London" -C "UK" -t noir -d 15000 # Thames curves python create_map_poster.py -c "Budapest" -C "Hungary" -t copper_patina -d 8000 # Danube split + # Custom fonts from local path + python create_map_poster.py -c "Paris" -C "France" --font-path "D:\\Fonts\\BrownieStencil-8O8MJ.ttf" + python create_map_poster.py -c "Tokyo" -C "Japan" --font-path "/path/to/fonts/" -t japanese_ink + + # Google Fonts + python create_map_poster.py -c "Berlin" -C "Germany" --font-family "Noto Sans JP" + # List themes python create_map_poster.py --list-themes @@ -819,6 +826,8 @@ def print_examples(): --theme, -t Theme name (default: terracotta) --all-themes Generate posters for all themes --distance, -d Map radius in meters (default: 18000) + --font-family Google Fonts family name (e.g., "Noto Sans JP", "Open Sans") + --font-path Path to local font file or directory (e.g., "fonts/custom.ttf" or "fonts/") --list-themes List all available themes Distance guide: @@ -826,6 +835,11 @@ def print_examples(): 8000-12000m Medium cities, focused downtown (Paris, Barcelona) 15000-20000m Large metros, full city view (Tokyo, Mumbai) +Font loading priority: + 1. --font-family (Google Fonts) - highest priority + 2. --font-path (local file or directory) - second priority + 3. Roboto fonts (default fallback) + Available themes can be found in the 'themes/' directory. Generated posters are saved to 'posters/' directory. """) @@ -948,6 +962,11 @@ def list_themes(): type=str, help='Google Fonts family name (e.g., "Noto Sans JP", "Open Sans"). If not specified, uses local Roboto fonts.', ) + parser.add_argument( + "--font-path", + type=str, + help="Path to local font file or directory containing fonts (e.g., /path/to/font.ttf or /path/to/fonts/)", + ) parser.add_argument( "--format", "-f", @@ -1010,6 +1029,10 @@ def list_themes(): custom_fonts = load_fonts(args.font_family) if not custom_fonts: print(f"⚠ Failed to load '{args.font_family}', falling back to Roboto") + elif args.font_path: + custom_fonts = load_fonts(font_path=args.font_path) + if not custom_fonts: + print(f"⚠ Failed to load fonts from path '{args.font_path}', falling back to Roboto") # Get coordinates and generate poster try: diff --git a/font_management.py b/font_management.py index 1c738d83d..aa5808aa8 100644 --- a/font_management.py +++ b/font_management.py @@ -6,7 +6,7 @@ import os import re from pathlib import Path -from typing import Optional +from typing import Optional, Union import requests @@ -134,27 +134,169 @@ def download_google_font(font_family: str, weights: list = None) -> Optional[dic return None -def load_fonts(font_family: Optional[str] = None) -> Optional[dict]: +def load_fonts_from_path(font_path: Union[str, Path]) -> Optional[dict]: """ - Load fonts from local directory or download from Google Fonts. + Load fonts from a local file path. + Handles both single font files and directories with multiple font weights. + + Supports: + - Single font file: Uses same file for all weights (bold, regular, light) + - Directory with weight-specific files: Looks for patterns like: + * filename-bold.ttf, filename-regular.ttf, filename-light.ttf + * filename_bold.ttf, filename_regular.ttf, filename_light.ttf + * bold.ttf, regular.ttf, light.ttf + * fontname-700.ttf, fontname-400.ttf, fontname-300.ttf (numeric weights) + + Supported formats: .ttf, .otf, .woff, .woff2 + + Args: + font_path: Path to font file or directory containing fonts. + Can be absolute or relative path. + + Returns: + Dict with 'bold', 'regular', 'light' keys mapping to font file paths, + or None if path is invalid or no suitable fonts found. + """ + # Convert to Path object for robust path handling + font_path = Path(font_path).expanduser().resolve() + + # Check if path exists + if not font_path.exists(): + print(f"⚠ Font path does not exist: {font_path}") + return None + + # Supported font extensions + SUPPORTED_EXTS = {".ttf", ".otf", ".woff", ".woff2"} + + # Single file case + if font_path.is_file(): + if font_path.suffix.lower() not in SUPPORTED_EXTS: + print( + f"⚠ Unsupported font format: {font_path.suffix}. " + f"Supported: {', '.join(SUPPORTED_EXTS)}" + ) + return None + + # Use the same file for all weights + font_files = { + "bold": str(font_path), + "regular": str(font_path), + "light": str(font_path), + } + print(f"✓ Loaded font from: {font_path}") + return font_files + + # Directory case + if font_path.is_dir(): + font_files = {} + + # Find all font files in directory + font_candidates = [ + f for f in font_path.iterdir() + if f.is_file() and f.suffix.lower() in SUPPORTED_EXTS + ] + + if not font_candidates: + print(f"⚠ No font files found in: {font_path}") + return None + + # Weight patterns to search for (in priority order) + weight_patterns = { + "bold": [ + r"bold", + r"700", # Numeric weight + r"^b_", # Prefix style + ], + "regular": [ + r"regular", + r"400", # Numeric weight + r"normal", + r"^r_", # Prefix style + ], + "light": [ + r"light", + r"300", # Numeric weight + r"thin", + r"^l_", # Prefix style + ], + } + + # Try to match fonts to weights + for weight_name, patterns in weight_patterns.items(): + for candidate in font_candidates: + candidate_name = candidate.stem.lower() + if any(re.search(pattern, candidate_name) for pattern in patterns): + font_files[weight_name] = str(candidate) + break + + # Fallback: Use first available font for missing weights + if font_candidates and font_files: + first_font = str(font_candidates[0]) + if "regular" not in font_files: + font_files["regular"] = first_font + if "bold" not in font_files: + font_files["bold"] = first_font + if "light" not in font_files: + font_files["light"] = first_font + elif font_candidates and not font_files: + # If no weight patterns matched, use available fonts + first_font = str(font_candidates[0]) + font_files = { + "bold": first_font, + "regular": first_font, + "light": first_font, + } + + if font_files: + print(f"✓ Loaded fonts from directory: {font_path}") + for weight, path in font_files.items(): + print(f" {weight}: {Path(path).name}") + return font_files + else: + print(f"⚠ Could not find suitable fonts in: {font_path}") + return None + + print(f"⚠ Invalid path (not a file or directory): {font_path}") + return None + + +def load_fonts(font_family: Optional[str] = None, font_path: Optional[Union[str, Path]] = None) -> Optional[dict]: + """ + Load fonts from local path, Google Fonts, or default local directory. Returns dict with font paths for different weights. - :param font_family: Google Fonts family name (e.g., 'Noto Sans JP', 'Open Sans'). - If None, uses local Roboto fonts. - :return: Dict with 'bold', 'regular', 'light' keys mapping to font file paths, - or None if all loading methods fail + Priority: + 1. If font_family provided, download from Google Fonts + 2. If font_path provided, load from that local path + 3. Fall back to default local Roboto fonts + + Args: + font_family: Google Fonts family name (e.g., 'Noto Sans JP', 'Open Sans'). + Ignored if font_path is provided. + font_path: Path to local font file or directory containing fonts. + Supports absolute or relative paths. + + Returns: + Dict with 'bold', 'regular', 'light' keys mapping to font file paths, + or None if all loading methods fail. """ - # If custom font family specified, try to download from Google Fonts + # Priority 1: Download from Google Fonts if family specified if font_family and font_family.lower() != "roboto": print(f"Loading Google Font: {font_family}") fonts = download_google_font(font_family) if fonts: print(f"✓ Font '{font_family}' loaded successfully") return fonts + print(f"⚠ Failed to load '{font_family}', trying local font path (if provided)...") - print(f"⚠ Failed to load '{font_family}', falling back to local Roboto") + # Priority 2: Load from local path if provided + if font_path: + fonts = load_fonts_from_path(font_path) + if fonts: + return fonts + print("⚠ Failed to load from font path, falling back to Roboto...") - # Default: Load local Roboto fonts + # Priority 3: Load default local Roboto fonts fonts = { "bold": os.path.join(FONTS_DIR, "Roboto-Bold.ttf"), "regular": os.path.join(FONTS_DIR, "Roboto-Regular.ttf"), diff --git a/posters/paris_noir_20260203_215631.png b/posters/paris_noir_20260203_215631.png new file mode 100644 index 000000000..90650cc0d Binary files /dev/null and b/posters/paris_noir_20260203_215631.png differ diff --git a/posters/paris_noir_20260203_220513.png b/posters/paris_noir_20260203_220513.png new file mode 100644 index 000000000..90650cc0d Binary files /dev/null and b/posters/paris_noir_20260203_220513.png differ diff --git a/posters/paris_terracotta_20260203_213348.png b/posters/paris_terracotta_20260203_213348.png new file mode 100644 index 000000000..402446f85 Binary files /dev/null and b/posters/paris_terracotta_20260203_213348.png differ diff --git a/posters/paris_terracotta_20260203_214835.png b/posters/paris_terracotta_20260203_214835.png new file mode 100644 index 000000000..25a3b9426 Binary files /dev/null and b/posters/paris_terracotta_20260203_214835.png differ