presenton/servers/fastapi/templates/font_utils.py
sudipnext c7860127f2 feat: add support for optional embedded Ollama and enhance database migration handling
- Updated docker-compose.yml to allow disabling embedded Ollama via environment variable.
- Refactored Dockerfile and Dockerfile.dev for improved dependency management and installation process.
- Enhanced FastAPI migration scripts to handle orphaned Alembic revisions and added new database migration logic.
- Improved error handling in background tasks and Codex authentication endpoints.
- Added support for font file uploads with better validation and extraction of font names.
- Introduced new image search functionality with support for Pexels and Pixabay APIs.
2026-04-15 15:39:35 +05:45

167 lines
4.9 KiB
Python

import asyncio
import re
import xml.etree.ElementTree as ET
from typing import Iterable
import aiohttp
_STYLE_TOKENS = {
"italic",
"italics",
"ital",
"oblique",
"roman",
"bolditalic",
"bolditalics",
"thin",
"hairline",
"extralight",
"ultralight",
"light",
"demilight",
"semilight",
"book",
"regular",
"normal",
"medium",
"semibold",
"demibold",
"bold",
"extrabold",
"ultrabold",
"black",
"extrablack",
"ultrablack",
"heavy",
"narrow",
"condensed",
"semicondensed",
"extracondensed",
"ultracondensed",
"expanded",
"semiexpanded",
"extraexpanded",
"ultraexpanded",
}
_STYLE_MODIFIERS = {"semi", "demi", "extra", "ultra"}
def _insert_spaces_in_camel_case(value: str) -> str:
value = re.sub(r"(?<=[a-z0-9])([A-Z])", r" \1", value)
value = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1 \2", value)
return value
def normalize_font_family_name(raw_name: str) -> str:
if not raw_name:
return raw_name
name = raw_name.replace("_", " ").replace("-", " ")
name = _insert_spaces_in_camel_case(name)
name = re.sub(r"\s+", " ", name).strip()
lower_name = name.lower()
for style in sorted(_STYLE_TOKENS, key=len, reverse=True):
suffix = " " + style
if lower_name.endswith(suffix):
name = name[: -len(suffix)]
lower_name = lower_name[: -len(suffix)]
break
tokens_original = name.split(" ")
tokens_filtered: list[str] = []
for index, token in enumerate(tokens_original):
lower_token = token.lower()
if index == 0:
tokens_filtered.append(token)
continue
if lower_token in _STYLE_TOKENS or lower_token in _STYLE_MODIFIERS:
continue
tokens_filtered.append(token)
if not tokens_filtered:
tokens_filtered = tokens_original
return re.sub(r"\s+", " ", " ".join(tokens_filtered).strip())
def extract_fonts_from_oxml(xml_content: str) -> list[str]:
fonts = set()
try:
root = ET.fromstring(xml_content)
namespaces = {
"a": "http://schemas.openxmlformats.org/drawingml/2006/main",
"p": "http://schemas.openxmlformats.org/presentationml/2006/main",
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
}
for xpath in (".//a:latin", ".//a:ea", ".//a:cs", ".//a:font"):
for font_elem in root.findall(xpath, namespaces):
typeface = font_elem.attrib.get("typeface")
if typeface:
fonts.add(typeface)
for rpr_elem in root.findall(".//a:rPr", namespaces):
for font_elem in rpr_elem.findall(".//a:latin", namespaces):
typeface = font_elem.attrib.get("typeface")
if typeface:
fonts.add(typeface)
for font_elem in root.findall(".//latin"):
typeface = font_elem.attrib.get("typeface")
if typeface:
fonts.add(typeface)
fonts.update(re.findall(r'typeface="([^"]+)"', xml_content))
system_fonts = {"+mn-lt", "+mj-lt", "+mn-ea", "+mj-ea", "+mn-cs", "+mj-cs", ""}
return sorted(font for font in fonts if font not in system_fonts and font.strip())
except Exception:
return []
def get_google_font_css_url(font_name: str) -> str:
return f"https://fonts.googleapis.com/css2?family={font_name.replace(' ', '+')}&display=swap"
async def check_google_font_availability(font_name: str) -> bool:
try:
async with aiohttp.ClientSession() as session:
async with session.head(
get_google_font_css_url(font_name),
timeout=aiohttp.ClientTimeout(total=10),
) as response:
return response.status == 200
except Exception:
return False
def collect_normalized_fonts_from_xmls(slide_xmls: Iterable[str]) -> list[str]:
raw_fonts = set()
for xml_content in slide_xmls:
raw_fonts.update(extract_fonts_from_oxml(xml_content))
normalized_fonts = {normalize_font_family_name(font) for font in raw_fonts}
return sorted(font for font in normalized_fonts if font)
async def get_available_and_unavailable_fonts(
font_names: Iterable[str],
) -> tuple[list[tuple[str, str]], list[tuple[str, None]]]:
normalized_fonts = sorted({font for font in font_names if font})
if not normalized_fonts:
return [], []
results = await asyncio.gather(
*[check_google_font_availability(font) for font in normalized_fonts]
)
available_fonts: list[tuple[str, str]] = []
unavailable_fonts: list[tuple[str, None]] = []
for font_name, is_available in zip(normalized_fonts, results):
if is_available:
available_fonts.append((font_name, get_google_font_css_url(font_name)))
else:
unavailable_fonts.append((font_name, None))
return available_fonts, unavailable_fonts