Make TTF fonts download-on-demand instead of bundled
- Fonts downloaded from int10h.org on first use - Cached in platform-appropriate directory (~/.cache/mcdosbox-x/fonts) - Add fonts_download() MCP tool for explicit pre-download - Wheel size reduced from 473KB to 56KB (88% smaller) - 48 tools now registered
This commit is contained in:
parent
51b0863ba4
commit
0f94edc48d
@ -39,9 +39,11 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
packages = ["src/mcdosbox_x"]
|
packages = ["src/mcdosbox_x"]
|
||||||
|
exclude = ["src/mcdosbox_x/fonts/*.ttf", "src/mcdosbox_x/fonts/LICENSE.TXT"]
|
||||||
|
|
||||||
[tool.hatch.build.targets.sdist]
|
[tool.hatch.build.targets.sdist]
|
||||||
include = ["src/mcdosbox_x", "src/mcdosbox_x/fonts"]
|
include = ["src/mcdosbox_x"]
|
||||||
|
exclude = ["src/mcdosbox_x/fonts/*.ttf"]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
|
|||||||
@ -1,35 +1,147 @@
|
|||||||
"""Font utilities for DOSBox-X TTF output.
|
"""Font utilities for DOSBox-X TTF output.
|
||||||
|
|
||||||
This module provides access to bundled IBM PC fonts from
|
Fonts are downloaded on-demand from The Ultimate Oldschool PC Font Pack
|
||||||
The Ultimate Oldschool PC Font Pack by VileR (int10h.org).
|
by VileR (int10h.org) and cached locally.
|
||||||
|
|
||||||
Fonts are licensed under CC BY-SA 4.0.
|
Fonts are licensed under CC BY-SA 4.0.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import platform
|
||||||
|
import zipfile
|
||||||
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
# Font directory location
|
logger = logging.getLogger(__name__)
|
||||||
FONTS_DIR = Path(__file__).parent / "fonts"
|
|
||||||
|
|
||||||
# Available bundled fonts (name -> filename mapping)
|
# Font pack download URL (official source)
|
||||||
BUNDLED_FONTS = {
|
FONT_PACK_URL = (
|
||||||
|
"https://int10h.org/oldschool-pc-fonts/download/oldschool_pc_font_pack_v2.2_linux.zip"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Available fonts (name -> path within zip)
|
||||||
|
AVAILABLE_FONTS = {
|
||||||
# Strict CP437 encoding
|
# Strict CP437 encoding
|
||||||
"Px437_IBM_VGA_8x16": "Px437_IBM_VGA_8x16.ttf",
|
"Px437_IBM_VGA_8x16": "Px437/Px437_IBM_VGA_8x16.ttf",
|
||||||
"Px437_IBM_VGA_9x16": "Px437_IBM_VGA_9x16.ttf",
|
"Px437_IBM_VGA_9x16": "Px437/Px437_IBM_VGA_9x16.ttf",
|
||||||
"Px437_IBM_EGA_8x14": "Px437_IBM_EGA_8x14.ttf",
|
"Px437_IBM_EGA_8x14": "Px437/Px437_IBM_EGA_8x14.ttf",
|
||||||
"Px437_IBM_CGA": "Px437_IBM_CGA.ttf",
|
"Px437_IBM_CGA": "Px437/Px437_IBM_CGA.ttf",
|
||||||
"Px437_IBM_MDA": "Px437_IBM_MDA.ttf",
|
"Px437_IBM_MDA": "Px437/Px437_IBM_MDA.ttf",
|
||||||
# Extended Unicode (CP437 + additional characters)
|
# Extended Unicode (CP437 + additional characters)
|
||||||
"PxPlus_IBM_VGA_8x16": "PxPlus_IBM_VGA_8x16.ttf",
|
"PxPlus_IBM_VGA_8x16": "PxPlus/PxPlus_IBM_VGA_8x16.ttf",
|
||||||
"PxPlus_IBM_VGA_9x16": "PxPlus_IBM_VGA_9x16.ttf",
|
"PxPlus_IBM_VGA_9x16": "PxPlus/PxPlus_IBM_VGA_9x16.ttf",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Default font for general use
|
# Default font for general use
|
||||||
DEFAULT_FONT = "Px437_IBM_VGA_9x16"
|
DEFAULT_FONT = "Px437_IBM_VGA_9x16"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_cache_dir() -> Path:
|
||||||
|
"""Get platform-appropriate cache directory for fonts."""
|
||||||
|
system = platform.system()
|
||||||
|
|
||||||
|
if system == "Linux":
|
||||||
|
# XDG Base Directory spec
|
||||||
|
xdg_cache = Path.home() / ".cache"
|
||||||
|
return xdg_cache / "mcdosbox-x" / "fonts"
|
||||||
|
elif system == "Darwin":
|
||||||
|
return Path.home() / "Library" / "Caches" / "mcdosbox-x" / "fonts"
|
||||||
|
elif system == "Windows":
|
||||||
|
local_app_data = Path.home() / "AppData" / "Local"
|
||||||
|
return local_app_data / "mcdosbox-x" / "fonts"
|
||||||
|
else:
|
||||||
|
# Fallback
|
||||||
|
return Path.home() / ".mcdosbox-x" / "fonts"
|
||||||
|
|
||||||
|
|
||||||
|
# Font cache directory
|
||||||
|
FONTS_DIR = _get_cache_dir()
|
||||||
|
|
||||||
|
# Docker container path for mounted fonts
|
||||||
|
DOCKER_FONTS_PATH = "/fonts"
|
||||||
|
|
||||||
|
|
||||||
|
def fonts_installed() -> bool:
|
||||||
|
"""Check if fonts have been downloaded."""
|
||||||
|
if not FONTS_DIR.exists():
|
||||||
|
return False
|
||||||
|
# Check if at least one font exists
|
||||||
|
return any((FONTS_DIR / f"{font_name}.ttf").exists() for font_name in AVAILABLE_FONTS)
|
||||||
|
|
||||||
|
|
||||||
|
def download_fonts(force: bool = False) -> dict:
|
||||||
|
"""Download IBM PC fonts from int10h.org.
|
||||||
|
|
||||||
|
Downloads the Ultimate Oldschool PC Font Pack and extracts
|
||||||
|
the subset of fonts used by mcdosbox-x.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
force: Re-download even if fonts exist
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Status dict with downloaded fonts and cache location
|
||||||
|
"""
|
||||||
|
if fonts_installed() and not force:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Fonts already installed",
|
||||||
|
"cache_dir": str(FONTS_DIR),
|
||||||
|
"fonts": list(AVAILABLE_FONTS.keys()),
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"Downloading fonts from {FONT_PACK_URL}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Download the font pack
|
||||||
|
with urlopen(FONT_PACK_URL, timeout=30) as response:
|
||||||
|
zip_data = BytesIO(response.read())
|
||||||
|
|
||||||
|
# Create cache directory
|
||||||
|
FONTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Extract selected fonts
|
||||||
|
extracted = []
|
||||||
|
with zipfile.ZipFile(zip_data) as zf:
|
||||||
|
for font_name, zip_path in AVAILABLE_FONTS.items():
|
||||||
|
# Find the file in the zip (may have prefix)
|
||||||
|
for name in zf.namelist():
|
||||||
|
if name.endswith(zip_path):
|
||||||
|
# Extract to cache with simple name
|
||||||
|
dest = FONTS_DIR / f"{font_name}.ttf"
|
||||||
|
dest.write_bytes(zf.read(name))
|
||||||
|
extracted.append(font_name)
|
||||||
|
logger.info(f"Extracted {font_name}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Write license file
|
||||||
|
license_text = """The Ultimate Oldschool PC Font Pack
|
||||||
|
by VileR (https://int10h.org/oldschool-pc-fonts/)
|
||||||
|
|
||||||
|
Licensed under Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
|
||||||
|
https://creativecommons.org/licenses/by-sa/4.0/
|
||||||
|
"""
|
||||||
|
(FONTS_DIR / "LICENSE.txt").write_text(license_text)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Downloaded {len(extracted)} fonts",
|
||||||
|
"cache_dir": str(FONTS_DIR),
|
||||||
|
"fonts": extracted,
|
||||||
|
"license": "CC BY-SA 4.0",
|
||||||
|
"source": "https://int10h.org/oldschool-pc-fonts/",
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"hint": "Check network connection. Fonts can be manually placed in: " + str(FONTS_DIR),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_font_path(font_name: str) -> Path | None:
|
def get_font_path(font_name: str) -> Path | None:
|
||||||
"""Get the full path to a bundled font.
|
"""Get the full path to a cached font.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
font_name: Font name without .ttf extension
|
font_name: Font name without .ttf extension
|
||||||
@ -37,50 +149,42 @@ def get_font_path(font_name: str) -> Path | None:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Path to font file, or None if not found
|
Path to font file, or None if not found
|
||||||
|
|
||||||
Example:
|
|
||||||
>>> path = get_font_path("Px437_IBM_VGA_9x16")
|
|
||||||
>>> str(path)
|
|
||||||
'/path/to/mcdosbox_x/fonts/Px437_IBM_VGA_9x16.ttf'
|
|
||||||
"""
|
"""
|
||||||
filename = BUNDLED_FONTS.get(font_name)
|
if font_name not in AVAILABLE_FONTS:
|
||||||
if filename:
|
return None
|
||||||
path = FONTS_DIR / filename
|
|
||||||
if path.exists():
|
path = FONTS_DIR / f"{font_name}.ttf"
|
||||||
return path
|
if path.exists():
|
||||||
|
return path
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def list_fonts() -> list[str]:
|
def list_fonts() -> list[str]:
|
||||||
"""List all available bundled font names.
|
"""List all available font names.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of font names that can be passed to get_font_path()
|
List of font names that can be used with DOSBox-X TTF output
|
||||||
|
|
||||||
Example:
|
|
||||||
>>> list_fonts()
|
|
||||||
['Px437_IBM_CGA', 'Px437_IBM_EGA_8x14', ...]
|
|
||||||
"""
|
"""
|
||||||
return sorted(BUNDLED_FONTS.keys())
|
return sorted(AVAILABLE_FONTS.keys())
|
||||||
|
|
||||||
|
|
||||||
def is_bundled_font(font_name: str) -> bool:
|
def is_available_font(font_name: str) -> bool:
|
||||||
"""Check if a font name refers to a bundled font.
|
"""Check if a font name is a known mcdosbox-x font.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
font_name: Font name to check
|
font_name: Font name to check
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if font is bundled with dosbox-mcp
|
True if font is available (may need download)
|
||||||
"""
|
"""
|
||||||
return font_name in BUNDLED_FONTS
|
return font_name in AVAILABLE_FONTS
|
||||||
|
|
||||||
|
|
||||||
def resolve_font(font_name: str | None, container_path: str | None = None) -> str | None:
|
def resolve_font(font_name: str | None, container_path: str | None = None) -> str | None:
|
||||||
"""Resolve a font name to a full path if bundled, or return as-is.
|
"""Resolve a font name to a full path, downloading if needed.
|
||||||
|
|
||||||
This allows users to specify either:
|
This allows users to specify either:
|
||||||
- A bundled font name (e.g., "Px437_IBM_VGA_9x16")
|
- A known font name (e.g., "Px437_IBM_VGA_9x16") - auto-downloads if missing
|
||||||
- A system font name (e.g., "Consolas")
|
- A system font name (e.g., "Consolas")
|
||||||
- A full path to a TTF file
|
- A full path to a TTF file
|
||||||
|
|
||||||
@ -90,25 +194,29 @@ def resolve_font(font_name: str | None, container_path: str | None = None) -> st
|
|||||||
(e.g., "/fonts" for Docker)
|
(e.g., "/fonts" for Docker)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Full path to bundled font, or original value if not bundled
|
Full path to font, or original value if not a known font
|
||||||
"""
|
"""
|
||||||
if font_name is None:
|
if font_name is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Check if it's a bundled font
|
# Check if it's a known font
|
||||||
filename = BUNDLED_FONTS.get(font_name)
|
if font_name in AVAILABLE_FONTS:
|
||||||
if filename:
|
# Auto-download if not installed
|
||||||
|
if not fonts_installed():
|
||||||
|
logger.info("Fonts not installed, downloading...")
|
||||||
|
result = download_fonts()
|
||||||
|
if not result.get("success"):
|
||||||
|
logger.warning(f"Font download failed: {result.get('error')}")
|
||||||
|
return font_name # Fall back to system font
|
||||||
|
|
||||||
if container_path:
|
if container_path:
|
||||||
# Return container-relative path
|
# Return container-relative path
|
||||||
return f"{container_path}/{filename}"
|
return f"{container_path}/{font_name}.ttf"
|
||||||
|
|
||||||
# Return host path
|
# Return host path
|
||||||
path = FONTS_DIR / filename
|
path = FONTS_DIR / f"{font_name}.ttf"
|
||||||
if path.exists():
|
if path.exists():
|
||||||
return str(path)
|
return str(path)
|
||||||
|
|
||||||
# Return as-is (system font or path)
|
# Return as-is (system font or path)
|
||||||
return font_name
|
return font_name
|
||||||
|
|
||||||
|
|
||||||
# Docker container path for mounted fonts
|
|
||||||
DOCKER_FONTS_PATH = "/fonts"
|
|
||||||
|
|||||||
@ -106,6 +106,7 @@ _TOOLS = [
|
|||||||
tools.query_status,
|
tools.query_status,
|
||||||
# Fonts
|
# Fonts
|
||||||
tools.fonts_list,
|
tools.fonts_list,
|
||||||
|
tools.fonts_download,
|
||||||
# Logging
|
# Logging
|
||||||
tools.logging_status,
|
tools.logging_status,
|
||||||
tools.logging_enable,
|
tools.logging_enable,
|
||||||
|
|||||||
@ -12,7 +12,16 @@ Tools are organized by function:
|
|||||||
|
|
||||||
from .breakpoints import breakpoint_delete, breakpoint_list, breakpoint_set
|
from .breakpoints import breakpoint_delete, breakpoint_list, breakpoint_set
|
||||||
from .control import loadstate, memdump, pause, query_status, reset, resume, savestate
|
from .control import loadstate, memdump, pause, query_status, reset, resume, savestate
|
||||||
from .execution import attach, continue_execution, fonts_list, launch, quit, step, step_over
|
from .execution import (
|
||||||
|
attach,
|
||||||
|
continue_execution,
|
||||||
|
fonts_download,
|
||||||
|
fonts_list,
|
||||||
|
launch,
|
||||||
|
quit,
|
||||||
|
step,
|
||||||
|
step_over,
|
||||||
|
)
|
||||||
from .inspection import (
|
from .inspection import (
|
||||||
disassemble,
|
disassemble,
|
||||||
memory_read,
|
memory_read,
|
||||||
@ -56,6 +65,7 @@ __all__ = [
|
|||||||
"step_over",
|
"step_over",
|
||||||
"quit",
|
"quit",
|
||||||
"fonts_list",
|
"fonts_list",
|
||||||
|
"fonts_download",
|
||||||
# Breakpoints
|
# Breakpoints
|
||||||
"breakpoint_set",
|
"breakpoint_set",
|
||||||
"breakpoint_list",
|
"breakpoint_list",
|
||||||
|
|||||||
@ -3,7 +3,15 @@
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from ..dosbox import DOSBoxConfig
|
from ..dosbox import DOSBoxConfig
|
||||||
from ..fonts import BUNDLED_FONTS, FONTS_DIR, list_fonts
|
from ..fonts import (
|
||||||
|
FONTS_DIR,
|
||||||
|
fonts_installed,
|
||||||
|
get_font_path,
|
||||||
|
list_fonts,
|
||||||
|
)
|
||||||
|
from ..fonts import (
|
||||||
|
download_fonts as _download_fonts,
|
||||||
|
)
|
||||||
from ..gdb_client import GDBError
|
from ..gdb_client import GDBError
|
||||||
from ..state import client, manager
|
from ..state import client, manager
|
||||||
from ..utils import format_address
|
from ..utils import format_address
|
||||||
@ -290,36 +298,61 @@ def quit() -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def fonts_list() -> dict:
|
def fonts_list() -> dict:
|
||||||
"""List available bundled TrueType fonts for DOSBox-X TTF output.
|
"""List available TrueType fonts for DOSBox-X TTF output.
|
||||||
|
|
||||||
These fonts from The Ultimate Oldschool PC Font Pack provide
|
These fonts from The Ultimate Oldschool PC Font Pack provide
|
||||||
authentic DOS-era rendering. Pass the font name to launch()
|
authentic DOS-era rendering. Fonts are downloaded on first use.
|
||||||
with output="ttf".
|
|
||||||
|
Use fonts_download() to pre-download fonts, or they will be
|
||||||
|
auto-downloaded when you specify a font in launch().
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary with available fonts and usage hints
|
Dictionary with available fonts and installation status
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
fonts_list()
|
fonts_list()
|
||||||
|
fonts_download() # Optional: pre-download
|
||||||
launch(binary_path="/dos/RIPTERM.EXE", ttf_font="Px437_IBM_VGA_9x16")
|
launch(binary_path="/dos/RIPTERM.EXE", ttf_font="Px437_IBM_VGA_9x16")
|
||||||
"""
|
"""
|
||||||
|
installed = fonts_installed()
|
||||||
fonts = []
|
fonts = []
|
||||||
for name in list_fonts():
|
for name in list_fonts():
|
||||||
info = _FONT_INFO.get(name, {})
|
info = _FONT_INFO.get(name, {})
|
||||||
font_file = BUNDLED_FONTS.get(name)
|
path = get_font_path(name)
|
||||||
available = (FONTS_DIR / font_file).exists() if font_file else False
|
|
||||||
fonts.append(
|
fonts.append(
|
||||||
{
|
{
|
||||||
"name": name,
|
"name": name,
|
||||||
"description": info.get("description", ""),
|
"description": info.get("description", ""),
|
||||||
"best_for": info.get("best_for", ""),
|
"best_for": info.get("best_for", ""),
|
||||||
"available": available,
|
"installed": path is not None,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"fonts": fonts,
|
"fonts": fonts,
|
||||||
|
"fonts_installed": installed,
|
||||||
|
"cache_dir": str(FONTS_DIR),
|
||||||
"recommended": "Px437_IBM_VGA_9x16",
|
"recommended": "Px437_IBM_VGA_9x16",
|
||||||
"usage_hint": 'Pass font name to launch(ttf_font="Px437_IBM_VGA_9x16")',
|
"usage_hint": "Fonts auto-download on use. Use fonts_download() to pre-install.",
|
||||||
"license": "CC BY-SA 4.0 - The Ultimate Oldschool PC Font Pack by VileR",
|
"license": "CC BY-SA 4.0 - The Ultimate Oldschool PC Font Pack by VileR",
|
||||||
|
"source": "https://int10h.org/oldschool-pc-fonts/",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def fonts_download(force: bool = False) -> dict:
|
||||||
|
"""Download IBM PC TrueType fonts for DOSBox-X TTF output.
|
||||||
|
|
||||||
|
Downloads fonts from The Ultimate Oldschool PC Font Pack (int10h.org)
|
||||||
|
and caches them locally. Fonts are also auto-downloaded when needed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
force: Re-download even if fonts already exist
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Status dict with download result and cache location
|
||||||
|
|
||||||
|
Example:
|
||||||
|
fonts_download() # Download fonts
|
||||||
|
fonts_list() # See installed fonts
|
||||||
|
"""
|
||||||
|
return _download_fonts(force=force)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user