mirror of
https://github.com/nicoverbruggen/kobo-font-fix.git
synced 2026-03-29 21:40:09 +02:00
Update overall structure and comments
This commit is contained in:
615
kobofix.py
615
kobofix.py
@@ -32,19 +32,31 @@ from fontTools.ttLib.tables._k_e_r_n import KernTable_format_0
|
|||||||
# Constants
|
# Constants
|
||||||
DEFAULT_PREFIX = "KF"
|
DEFAULT_PREFIX = "KF"
|
||||||
DEFAULT_LINE_PERCENT = 20
|
DEFAULT_LINE_PERCENT = 20
|
||||||
DEFAULT_KOBO_KERN = True
|
|
||||||
|
|
||||||
VALID_SUFFIXES = ("-Regular", "-Bold", "-Italic", "-BoldItalic")
|
# Style mapping for filenames and internal font data.
|
||||||
|
# This centralized map is used as a single source of truth for all style-related
|
||||||
|
# properties based on the font's filename.
|
||||||
|
# The keys are substrings to check in the filename.
|
||||||
|
# The values are a tuple of (human-readable_style_name, usWeightClass).
|
||||||
|
STYLE_MAP = {
|
||||||
|
"BoldItalic": ("Bold Italic", 700),
|
||||||
|
"Bold": ("Bold", 700),
|
||||||
|
"Italic": ("Italic", 400),
|
||||||
|
"Regular": ("Regular", 400),
|
||||||
|
}
|
||||||
SUPPORTED_EXTENSIONS = (".ttf")
|
SUPPORTED_EXTENSIONS = (".ttf")
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging for clear output
|
||||||
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FontMetadata:
|
class FontMetadata:
|
||||||
"""A container for consistent font naming and metadata."""
|
"""
|
||||||
|
A simple data class to hold consistent font naming and metadata.
|
||||||
|
This prevents passing multiple, potentially inconsistent strings between functions.
|
||||||
|
"""
|
||||||
family_name: str
|
family_name: str
|
||||||
style_name: str
|
style_name: str
|
||||||
full_name: str
|
full_name: str
|
||||||
@@ -52,55 +64,94 @@ class FontMetadata:
|
|||||||
|
|
||||||
|
|
||||||
class FontProcessor:
|
class FontProcessor:
|
||||||
"""Main font processing class."""
|
"""
|
||||||
|
Main font processing class. All core logic is encapsulated here to improve
|
||||||
|
readability and testability.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, prefix: str = DEFAULT_PREFIX, line_percent: int = DEFAULT_LINE_PERCENT, kobo_kern_fix: bool = DEFAULT_KOBO_KERN):
|
def __init__(self, prefix: str = DEFAULT_PREFIX, line_percent: int = DEFAULT_LINE_PERCENT):
|
||||||
"""
|
"""
|
||||||
Initialize the font processor.
|
Initialize the font processor.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
prefix: Prefix to add to font names
|
prefix: Prefix to add to font names
|
||||||
line_percent: Percentage for baseline adjustment
|
line_percent: Percentage for baseline adjustment
|
||||||
kobo_kern_fix: Apply `kern` table fix for Kobo devices
|
|
||||||
"""
|
"""
|
||||||
self.prefix = prefix
|
self.prefix = prefix
|
||||||
self.line_percent = line_percent
|
self.line_percent = line_percent
|
||||||
self.kobo_kern_fix = kobo_kern_fix
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Helper methods
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_style_from_filename(filename: str) -> Tuple[str, int]:
|
||||||
|
"""
|
||||||
|
Determine font style and weight from filename.
|
||||||
|
This function centralizes a critical piece of logic that is used in
|
||||||
|
multiple places to ensure consistency across the script.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: The font file name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A tuple of (style_name, usWeightClass).
|
||||||
|
"""
|
||||||
|
base_filename = os.path.basename(filename)
|
||||||
|
for key, (style_name, weight) in STYLE_MAP.items():
|
||||||
|
if key.lower() in base_filename.lower():
|
||||||
|
return style_name, weight
|
||||||
|
return "Regular", 400 # Default if no style found
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_name_records(font: TTFont, name_id: int, new_name: str) -> None:
|
||||||
|
"""
|
||||||
|
Update a font's name table record using a consistent method.
|
||||||
|
This helper function abstracts the complexity of working with name IDs,
|
||||||
|
platform IDs (3 for Microsoft), encoding IDs (1 for Unicode), and
|
||||||
|
language IDs (0x0409 for English-US). This avoids repetitive code.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
font: The TTFont object.
|
||||||
|
name_id: The ID of the name record to update.
|
||||||
|
new_name: The new string for the name record.
|
||||||
|
"""
|
||||||
|
name_table = font["name"]
|
||||||
|
|
||||||
|
# Check if the name already exists and is correct to avoid redundant updates
|
||||||
|
current_name = name_table.getName(name_id, 3, 1, 0x0409)
|
||||||
|
if current_name and current_name.toUnicode() == new_name:
|
||||||
|
logger.info(f" Name ID {name_id} is already correct.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
name_table.setName(new_name, name_id, 3, 1, 0x0409) # Windows, Unicode
|
||||||
|
logger.info(f" Name ID {name_id} updated to '{new_name}'.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f" Failed to update name ID {name_id}: {e}")
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Metadata extraction
|
# Metadata extraction
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
def _get_font_metadata(self, font: TTFont, font_path: str, new_family_name: Optional[str]) -> Optional[FontMetadata]:
|
def _get_font_metadata(self, font: TTFont, font_path: str, new_family_name: Optional[str]) -> Optional[FontMetadata]:
|
||||||
"""Extract or infer font metadata from the font and arguments, prioritizing filename suffix."""
|
"""
|
||||||
if "name" not in font:
|
Extract or infer font metadata from the font and arguments.
|
||||||
logger.warning(" No 'name' table found; cannot determine metadata.")
|
This function acts as a single point of truth for font metadata,
|
||||||
return None
|
ensuring consistency throughout the processing pipeline.
|
||||||
|
"""
|
||||||
|
if "name" in font:
|
||||||
|
# Determine family name from user input or best available name from font.
|
||||||
|
family_name = new_family_name if new_family_name else font["name"].getBestFamilyName()
|
||||||
|
else:
|
||||||
|
family_name = new_family_name
|
||||||
|
|
||||||
name_table = font["name"]
|
|
||||||
|
|
||||||
# Determine family name
|
|
||||||
family_name = new_family_name if new_family_name else name_table.getBestFamilyName()
|
|
||||||
if not family_name:
|
if not family_name:
|
||||||
logger.warning(" Could not determine font family name.")
|
logger.warning(" Could not determine font family name.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Determine style name from filename suffix
|
# Centralized logic: Determine style name from filename.
|
||||||
base_filename = os.path.basename(font_path)
|
style_name, _ = self._get_style_from_filename(font_path)
|
||||||
style_map = {
|
|
||||||
"BoldItalic": "Bold Italic",
|
|
||||||
"Bold": "Bold",
|
|
||||||
"Italic": "Italic",
|
|
||||||
"Regular": "Regular",
|
|
||||||
}
|
|
||||||
|
|
||||||
style_name = "Regular" # Default to regular if no suffix found
|
|
||||||
|
|
||||||
# Iterate through styles and check if filename contains the style string
|
|
||||||
for style_key, style_val in style_map.items():
|
|
||||||
if style_key.lower() in base_filename.lower():
|
|
||||||
style_name = style_val
|
|
||||||
break
|
|
||||||
|
|
||||||
# Construct the full name and PS name based on style name logic
|
# Construct the full name and PS name based on style name logic
|
||||||
full_name = f"{family_name}"
|
full_name = f"{family_name}"
|
||||||
@@ -128,23 +179,15 @@ class FontProcessor:
|
|||||||
def _pair_value_to_kern(value1, value2) -> int:
|
def _pair_value_to_kern(value1, value2) -> int:
|
||||||
"""
|
"""
|
||||||
Compute a legacy kerning value from GPOS PairValue records.
|
Compute a legacy kerning value from GPOS PairValue records.
|
||||||
|
This logic is specific to converting GPOS (OpenType) kerning to
|
||||||
Args:
|
the older 'kern' (TrueType) table format.
|
||||||
value1: First value record
|
|
||||||
value2: Second value record
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Integer kerning value (may be negative)
|
|
||||||
"""
|
"""
|
||||||
kern_value = 0
|
kern_value = 0
|
||||||
|
|
||||||
# Prefer XAdvance adjustments
|
|
||||||
if value1 is not None:
|
if value1 is not None:
|
||||||
kern_value += getattr(value1, "XAdvance", 0) or 0
|
kern_value += getattr(value1, "XAdvance", 0) or 0
|
||||||
if value2 is not None:
|
if value2 is not None:
|
||||||
kern_value += getattr(value2, "XAdvance", 0) or 0
|
kern_value += getattr(value2, "XAdvance", 0) or 0
|
||||||
|
|
||||||
# Fall back to XPlacement if no XAdvance
|
|
||||||
if kern_value == 0:
|
if kern_value == 0:
|
||||||
if value1 is not None:
|
if value1 is not None:
|
||||||
kern_value += getattr(value1, "XPlacement", 0) or 0
|
kern_value += getattr(value1, "XPlacement", 0) or 0
|
||||||
@@ -156,7 +199,6 @@ class FontProcessor:
|
|||||||
def _extract_format1_pairs(self, subtable) -> Dict[Tuple[str, str], int]:
|
def _extract_format1_pairs(self, subtable) -> Dict[Tuple[str, str], int]:
|
||||||
"""Extract kerning pairs from PairPos Format 1 (per-glyph PairSets)."""
|
"""Extract kerning pairs from PairPos Format 1 (per-glyph PairSets)."""
|
||||||
pairs = defaultdict(int)
|
pairs = defaultdict(int)
|
||||||
|
|
||||||
coverage = getattr(subtable, "Coverage", None)
|
coverage = getattr(subtable, "Coverage", None)
|
||||||
pair_sets = getattr(subtable, "PairSet", [])
|
pair_sets = getattr(subtable, "PairSet", [])
|
||||||
|
|
||||||
@@ -172,13 +214,11 @@ class FontProcessor:
|
|||||||
kern_value = self._pair_value_to_kern(record.Value1, record.Value2)
|
kern_value = self._pair_value_to_kern(record.Value1, record.Value2)
|
||||||
if kern_value:
|
if kern_value:
|
||||||
pairs[(left_glyph, right_glyph)] += kern_value
|
pairs[(left_glyph, right_glyph)] += kern_value
|
||||||
|
|
||||||
return pairs
|
return pairs
|
||||||
|
|
||||||
def _extract_format2_pairs(self, subtable) -> Dict[Tuple[str, str], int]:
|
def _extract_format2_pairs(self, subtable) -> Dict[Tuple[str, str], int]:
|
||||||
"""Extract kerning pairs from PairPos Format 2 (class-based)."""
|
"""Extract kerning pairs from PairPos Format 2 (class-based)."""
|
||||||
pairs = defaultdict(int)
|
pairs = defaultdict(int)
|
||||||
|
|
||||||
coverage = getattr(subtable, "Coverage", None)
|
coverage = getattr(subtable, "Coverage", None)
|
||||||
class_def1 = getattr(subtable, "ClassDef1", None)
|
class_def1 = getattr(subtable, "ClassDef1", None)
|
||||||
class_def2 = getattr(subtable, "ClassDef2", None)
|
class_def2 = getattr(subtable, "ClassDef2", None)
|
||||||
@@ -187,20 +227,17 @@ class FontProcessor:
|
|||||||
if not coverage or not hasattr(coverage, "glyphs"):
|
if not coverage or not hasattr(coverage, "glyphs"):
|
||||||
return pairs
|
return pairs
|
||||||
|
|
||||||
# Build left-side glyph lists per class
|
|
||||||
class1_map = getattr(class_def1, "classDefs", {}) if class_def1 else {}
|
class1_map = getattr(class_def1, "classDefs", {}) if class_def1 else {}
|
||||||
left_by_class = defaultdict(list)
|
left_by_class = defaultdict(list)
|
||||||
for glyph in coverage.glyphs:
|
for glyph in coverage.glyphs:
|
||||||
class_idx = class1_map.get(glyph, 0)
|
class_idx = class1_map.get(glyph, 0)
|
||||||
left_by_class[class_idx].append(glyph)
|
left_by_class[class_idx].append(glyph)
|
||||||
|
|
||||||
# Build right-side glyph lists per class
|
|
||||||
class2_map = getattr(class_def2, "classDefs", {}) if class_def2 else {}
|
class2_map = getattr(class_def2, "classDefs", {}) if class_def2 else {}
|
||||||
right_by_class = defaultdict(list)
|
right_by_class = defaultdict(list)
|
||||||
for glyph, class_idx in class2_map.items():
|
for glyph, class_idx in class2_map.items():
|
||||||
right_by_class[class_idx].append(glyph)
|
right_by_class[class_idx].append(glyph)
|
||||||
|
|
||||||
# Extract kerning values
|
|
||||||
for class1_idx, class1_record in enumerate(class1_records):
|
for class1_idx, class1_record in enumerate(class1_records):
|
||||||
left_glyphs = left_by_class.get(class1_idx, [])
|
left_glyphs = left_by_class.get(class1_idx, [])
|
||||||
if not left_glyphs:
|
if not left_glyphs:
|
||||||
@@ -211,42 +248,32 @@ class FontProcessor:
|
|||||||
if not right_glyphs:
|
if not right_glyphs:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
kern_value = self._pair_value_to_kern(
|
kern_value = self._pair_value_to_kern(class2_record.Value1, class2_record.Value2)
|
||||||
class2_record.Value1,
|
|
||||||
class2_record.Value2
|
|
||||||
)
|
|
||||||
if not kern_value:
|
if not kern_value:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for left in left_glyphs:
|
for left in left_glyphs:
|
||||||
for right in right_glyphs:
|
for right in right_glyphs:
|
||||||
pairs[(left, right)] += kern_value
|
pairs[(left, right)] += kern_value
|
||||||
|
|
||||||
return pairs
|
return pairs
|
||||||
|
|
||||||
def extract_kern_pairs(self, font: TTFont) -> Dict[Tuple[str, str], int]:
|
def extract_kern_pairs(self, font: TTFont) -> Dict[Tuple[str, str], int]:
|
||||||
"""
|
"""
|
||||||
Extract all kerning pairs from GPOS PairPos lookups.
|
Extract all kerning pairs from GPOS PairPos lookups.
|
||||||
|
GPOS (Glyph Positioning) is the modern standard for kerning in OpenType fonts.
|
||||||
Args:
|
This function iterates through the GPOS tables to find all kerning pairs
|
||||||
font: Font object to extract kerning from
|
before we convert them to the legacy 'kern' table format.
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary mapping glyph pairs to kerning values
|
|
||||||
"""
|
"""
|
||||||
pairs = defaultdict(int)
|
pairs = defaultdict(int)
|
||||||
|
|
||||||
if "GPOS" in font:
|
if "GPOS" in font:
|
||||||
gpos = font["GPOS"].table
|
gpos = font["GPOS"].table
|
||||||
lookup_list = getattr(gpos, "LookupList", None)
|
lookup_list = getattr(gpos, "LookupList", None)
|
||||||
|
|
||||||
if lookup_list and lookup_list.Lookup:
|
if lookup_list and lookup_list.Lookup:
|
||||||
for lookup in lookup_list.Lookup:
|
for lookup in lookup_list.Lookup:
|
||||||
# Only process Pair Adjustment lookups (type 2)
|
# Only process Pair Adjustment lookups (type 2)
|
||||||
if getattr(lookup, "LookupType", None) == 2:
|
if getattr(lookup, "LookupType", None) == 2:
|
||||||
for subtable in getattr(lookup, "SubTable", []):
|
for subtable in getattr(lookup, "SubTable", []):
|
||||||
fmt = getattr(subtable, "Format", None)
|
fmt = getattr(subtable, "Format", None)
|
||||||
|
|
||||||
if fmt == 1:
|
if fmt == 1:
|
||||||
format1_pairs = self._extract_format1_pairs(subtable)
|
format1_pairs = self._extract_format1_pairs(subtable)
|
||||||
for key, value in format1_pairs.items():
|
for key, value in format1_pairs.items():
|
||||||
@@ -255,24 +282,14 @@ class FontProcessor:
|
|||||||
format2_pairs = self._extract_format2_pairs(subtable)
|
format2_pairs = self._extract_format2_pairs(subtable)
|
||||||
for key, value in format2_pairs.items():
|
for key, value in format2_pairs.items():
|
||||||
pairs[key] += value
|
pairs[key] += value
|
||||||
|
|
||||||
return dict(pairs)
|
return dict(pairs)
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Legacy kern table methods
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_legacy_kern(font: TTFont, kern_pairs: Dict[Tuple[str, str], int]) -> int:
|
def add_legacy_kern(font: TTFont, kern_pairs: Dict[Tuple[str, str], int]) -> int:
|
||||||
"""
|
"""
|
||||||
Create or replace a legacy 'kern' table with the supplied pairs.
|
Create or replace a legacy 'kern' table with the supplied pairs.
|
||||||
|
Older devices like some Kobo models only recognize the 'kern' table.
|
||||||
Args:
|
This function creates a new `kern` table from the extracted GPOS pairs.
|
||||||
font: Font object to modify
|
|
||||||
kern_pairs: Dictionary of kerning pairs
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Number of kern pairs written
|
|
||||||
"""
|
"""
|
||||||
if not kern_pairs:
|
if not kern_pairs:
|
||||||
return 0
|
return 0
|
||||||
@@ -283,16 +300,13 @@ class FontProcessor:
|
|||||||
|
|
||||||
subtable = KernTable_format_0()
|
subtable = KernTable_format_0()
|
||||||
subtable.version = 0
|
subtable.version = 0
|
||||||
subtable.length = None # Recalculated by fontTools
|
subtable.length = None
|
||||||
subtable.coverage = 1 # Horizontal kerning, format 0
|
subtable.coverage = 1
|
||||||
|
|
||||||
# Ensure proper types for kern table
|
|
||||||
subtable.kernTable = {
|
subtable.kernTable = {
|
||||||
tuple(k): int(v)
|
tuple(k): int(v)
|
||||||
for k, v in kern_pairs.items()
|
for k, v in kern_pairs.items()
|
||||||
if v # Only include non-zero values
|
if v
|
||||||
}
|
}
|
||||||
|
|
||||||
kern_table.kernTables.append(subtable)
|
kern_table.kernTables.append(subtable)
|
||||||
font["kern"] = kern_table
|
font["kern"] = kern_table
|
||||||
|
|
||||||
@@ -305,134 +319,44 @@ class FontProcessor:
|
|||||||
def rename_font(self, font: TTFont, metadata: FontMetadata) -> None:
|
def rename_font(self, font: TTFont, metadata: FontMetadata) -> None:
|
||||||
"""
|
"""
|
||||||
Update the font's name-related metadata.
|
Update the font's name-related metadata.
|
||||||
|
This method uses the centralized `_set_name_records` helper to update
|
||||||
This method prefixes family, full, and unique names, and updates
|
all relevant name fields.
|
||||||
the PostScript font name.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
font: Font object to modify
|
|
||||||
metadata: The FontMetadata object containing the new names
|
|
||||||
"""
|
"""
|
||||||
if "name" not in font:
|
if "name" not in font:
|
||||||
logger.warning(" No 'name' table found; skipping all name changes")
|
logger.warning(" No 'name' table found; skipping all name changes")
|
||||||
return
|
return
|
||||||
|
|
||||||
name_table = font["name"]
|
|
||||||
|
|
||||||
# Update Name ID 1 (Family Name) and 16 (Typographic Family)
|
# Update Name ID 1 (Family Name) and 16 (Typographic Family)
|
||||||
family_name_str = f"{self.prefix} {metadata.family_name}"
|
self._set_name_records(font, 1, f"{self.prefix} {metadata.family_name}")
|
||||||
for record in name_table.names:
|
self._set_name_records(font, 16, f"{self.prefix} {metadata.family_name}")
|
||||||
if record.nameID in {1, 16}:
|
|
||||||
try:
|
|
||||||
current_name = record.toUnicode()
|
|
||||||
if current_name != family_name_str:
|
|
||||||
record.string = family_name_str.encode(record.getEncoding())
|
|
||||||
logger.info(f" Name ID {record.nameID} updated: '{current_name}'->'{family_name_str}'")
|
|
||||||
else:
|
|
||||||
logger.info(f" Name ID {record.nameID} is already correct")
|
|
||||||
except Exception:
|
|
||||||
try:
|
|
||||||
record.string = family_name_str.encode("utf_16_be")
|
|
||||||
logger.info(f" Name ID {record.nameID} updated with UTF-16 BE encoding")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f" Failed to update name ID {record.nameID}: {e}")
|
|
||||||
|
|
||||||
# Update Name ID 4 (Full Name)
|
# Update Name ID 2 (Subfamily Name) and 17 (Preferred Subfamily)
|
||||||
full_name_str = f"{self.prefix} {metadata.full_name}"
|
# These are crucial for font menu display on macOS and Windows,
|
||||||
for record in name_table.names:
|
# ensuring the font correctly groups with its family.
|
||||||
if record.nameID == 4:
|
self._set_name_records(font, 2, metadata.style_name)
|
||||||
try:
|
self._set_name_records(font, 17, metadata.style_name)
|
||||||
current_name = record.toUnicode()
|
|
||||||
if current_name != full_name_str:
|
# Update Full Name (Name ID 4)
|
||||||
record.string = full_name_str.encode(record.getEncoding())
|
self._set_name_records(font, 4, f"{self.prefix} {metadata.full_name}")
|
||||||
logger.info(f" Name ID 4 updated: '{current_name}'->'{full_name_str}'")
|
|
||||||
else:
|
|
||||||
logger.info(" Name ID 4 is already correct")
|
|
||||||
except Exception:
|
|
||||||
try:
|
|
||||||
record.string = full_name_str.encode("utf_16_be")
|
|
||||||
logger.info(" Name ID 4 updated with UTF-16 BE encoding")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f" Failed to update name ID 4: {e}")
|
|
||||||
|
|
||||||
# --- Update Subfamily Name (nameID 2) and Preferred Subfamily (nameID 17) ---
|
# Update Unique ID (nameID 3)
|
||||||
for record in name_table.names:
|
try:
|
||||||
if record.nameID in {2, 17}:
|
current_unique = font["name"].getName(3, 3, 1).toUnicode()
|
||||||
try:
|
parts = current_unique.split("Version")
|
||||||
current_name = record.toUnicode()
|
version_info = f"Version{parts[1]}" if len(parts) == 2 else "Version 1.000"
|
||||||
if current_name != metadata.style_name:
|
new_unique_id = f"{self.prefix} {metadata.family_name.strip()}:{version_info}"
|
||||||
record.string = metadata.style_name.encode(record.getEncoding())
|
if current_unique != new_unique_id:
|
||||||
logger.info(f" Name ID {record.nameID} updated: '{current_name}'->'{metadata.style_name}'")
|
self._set_name_records(font, 3, new_unique_id)
|
||||||
else:
|
except Exception as e:
|
||||||
logger.info(f" Name ID {record.nameID} is already correct")
|
logger.warning(f" Failed to update Unique ID: {e}")
|
||||||
except Exception:
|
|
||||||
try:
|
|
||||||
record.string = metadata.style_name.encode("utf_16_be")
|
|
||||||
logger.info(f" Name ID {record.nameID} updated with UTF-16 BE encoding")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f" Failed to update name ID {record.nameID}: {e}")
|
|
||||||
|
|
||||||
# --- Update Unique ID (nameID 3) ---
|
|
||||||
for record in name_table.names:
|
|
||||||
if record.nameID == 3: # Unique ID
|
|
||||||
try:
|
|
||||||
current_unique = record.toUnicode()
|
|
||||||
parts = current_unique.split("Version")
|
|
||||||
version_info = f"Version{parts[1]}" if len(parts) == 2 else "Version 1.000"
|
|
||||||
|
|
||||||
new_unique_id = f"{self.prefix} {metadata.family_name.strip()}:{version_info}"
|
# Update PostScript Name (nameID 6)
|
||||||
if current_unique != new_unique_id:
|
|
||||||
record.string = new_unique_id.encode(record.getEncoding())
|
|
||||||
logger.info(f" Unique ID updated: '{current_unique}'->'{new_unique_id}'")
|
|
||||||
else:
|
|
||||||
logger.info(" Unique ID is already correct")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f" Failed to update Unique ID: {e}")
|
|
||||||
|
|
||||||
# --- Update PostScript Name (nameID 6) and other tables ---
|
|
||||||
new_ps_name = metadata.ps_name
|
new_ps_name = metadata.ps_name
|
||||||
|
self._set_name_records(font, 6, new_ps_name)
|
||||||
name_updated = False
|
|
||||||
|
if "CFF " in font and font["CFF "].cff.topDictIndex[0].fontName != new_ps_name:
|
||||||
# 1. Try to update the name table (nameID 6)
|
font["CFF "].cff.topDictIndex[0].fontName = new_ps_name
|
||||||
for record in name_table.names:
|
logger.info(f" PostScript CFF fontName updated to '{new_ps_name}'.")
|
||||||
if record.nameID == 6: # PostScript Name
|
|
||||||
try:
|
|
||||||
current_name = record.toUnicode()
|
|
||||||
if current_name != new_ps_name:
|
|
||||||
record.string = new_ps_name.encode(record.getEncoding())
|
|
||||||
logger.info(f" PostScript name table (nameID 6) updated: '{current_name}'->'{new_ps_name}'")
|
|
||||||
else:
|
|
||||||
logger.info(" PostScript name table (nameID 6) is already correct")
|
|
||||||
name_updated = True
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f" Failed to update PostScript name in name table: {e}")
|
|
||||||
|
|
||||||
if name_updated:
|
|
||||||
return
|
|
||||||
|
|
||||||
# 2. Fallback to CFF or post table if nameID 6 wasn't found or updated
|
|
||||||
if "CFF " in font:
|
|
||||||
top_dict = font["CFF "].cff.topDictIndex[0]
|
|
||||||
current_name = getattr(top_dict, "fontName", "")
|
|
||||||
|
|
||||||
if current_name != new_ps_name:
|
|
||||||
top_dict.fontName = new_ps_name
|
|
||||||
logger.info(f" PostScript CFF fontName updated: '{current_name}'->'{new_ps_name}'")
|
|
||||||
else:
|
|
||||||
logger.info(" PostScript CFF fontName is already correct")
|
|
||||||
elif "post" in font:
|
|
||||||
post_table = font["post"]
|
|
||||||
current_name = getattr(post_table, "postscriptName", "")
|
|
||||||
|
|
||||||
if current_name != new_ps_name:
|
|
||||||
post_table.postscriptName = new_ps_name
|
|
||||||
logger.info(f" PostScript 'post' fontName updated: '{current_name}'->'{new_ps_name}'")
|
|
||||||
else:
|
|
||||||
logger.info(" PostScript 'post' fontName is already correct")
|
|
||||||
else:
|
|
||||||
logger.warning(" No PostScript name found in `name`, `CFF` or `post` tables.")
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Weight metadata methods
|
# Weight metadata methods
|
||||||
@@ -441,132 +365,71 @@ class FontProcessor:
|
|||||||
def update_weight_metadata(self, font: TTFont, filename: str) -> None:
|
def update_weight_metadata(self, font: TTFont, filename: str) -> None:
|
||||||
"""
|
"""
|
||||||
Update font weight metadata based on filename suffix.
|
Update font weight metadata based on filename suffix.
|
||||||
|
This function uses the centralized style lookup, which simplifies
|
||||||
Args:
|
the logic significantly.
|
||||||
font: Font object to modify
|
|
||||||
filename: Font filename to check suffix
|
|
||||||
"""
|
"""
|
||||||
weight_map = {
|
style_name, os2_weight = self._get_style_from_filename(filename)
|
||||||
"-Regular": ("Regular", 400),
|
ps_weight = style_name.replace(" ", "")
|
||||||
"-Italic": ("Italic", 400),
|
|
||||||
"-Bold": ("Bold", 700),
|
|
||||||
"-BoldItalic": ("Bold Italic", 700),
|
|
||||||
}
|
|
||||||
|
|
||||||
base_filename = os.path.basename(filename)
|
|
||||||
|
|
||||||
# Find matching style and corresponding weight data
|
|
||||||
matched_style = None
|
|
||||||
for suffix, (ps_weight, os2_weight) in weight_map.items():
|
|
||||||
if suffix in base_filename:
|
|
||||||
matched_style = suffix
|
|
||||||
self._update_os2_weight(font, os2_weight)
|
|
||||||
self._update_postscript_weight(font, ps_weight)
|
|
||||||
break
|
|
||||||
|
|
||||||
if not matched_style:
|
|
||||||
logger.warning(
|
|
||||||
f" Filename doesn't match expected patterns {list(weight_map.keys())}. "
|
|
||||||
"Weight metadata skipped"
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _update_os2_weight(font: TTFont, weight: int) -> None:
|
|
||||||
"""Update the OS/2 usWeightClass."""
|
|
||||||
if "OS/2" in font and hasattr(font["OS/2"], "usWeightClass"):
|
if "OS/2" in font and hasattr(font["OS/2"], "usWeightClass"):
|
||||||
current_weight = font["OS/2"].usWeightClass
|
if font["OS/2"].usWeightClass != os2_weight:
|
||||||
if current_weight != weight:
|
font["OS/2"].usWeightClass = os2_weight
|
||||||
font["OS/2"].usWeightClass = weight
|
logger.info(f" OS/2 usWeightClass updated to {os2_weight}.")
|
||||||
logger.info(f" OS/2 usWeightClass updated: {current_weight}->{weight}")
|
|
||||||
else:
|
else:
|
||||||
logger.info(" OS/2 usWeightClass is already correct")
|
logger.info(" OS/2 usWeightClass is already correct.")
|
||||||
else:
|
|
||||||
logger.warning(" No OS/2 usWeightClass table found; skipping")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _update_postscript_weight(font: TTFont, weight: str) -> None:
|
|
||||||
"""Update the PostScript weight string."""
|
|
||||||
if "CFF " in font and hasattr(font["CFF "].cff.topDictIndex[0], "Weight"):
|
if "CFF " in font and hasattr(font["CFF "].cff.topDictIndex[0], "Weight"):
|
||||||
current_weight = getattr(font["CFF "].cff.topDictIndex[0], "Weight", "")
|
if getattr(font["CFF "].cff.topDictIndex[0], "Weight", "") != ps_weight:
|
||||||
if current_weight != weight:
|
font["CFF "].cff.topDictIndex[0].Weight = ps_weight
|
||||||
font["CFF "].cff.topDictIndex[0].Weight = weight
|
logger.info(f" PostScript CFF weight updated to '{ps_weight}'.")
|
||||||
logger.info(f" PostScript CFF weight updated: '{current_weight}'->'{weight}'")
|
|
||||||
else:
|
|
||||||
logger.info(" PostScript CFF weight is already correct")
|
|
||||||
elif "post" in font and hasattr(font["post"], "Weight"):
|
elif "post" in font and hasattr(font["post"], "Weight"):
|
||||||
current_weight = getattr(font["post"], "Weight", "")
|
if getattr(font["post"], "Weight", "") != ps_weight:
|
||||||
if current_weight != weight:
|
font["post"].Weight = ps_weight
|
||||||
font["post"].Weight = weight
|
logger.info(f" PostScript 'post' weight updated to '{ps_weight}'.")
|
||||||
logger.info(f" PostScript 'post' weight updated: '{current_weight}'->'{weight}'")
|
|
||||||
else:
|
|
||||||
logger.info(" PostScript 'post' weight is already correct")
|
|
||||||
else:
|
|
||||||
logger.warning(" No CFF or post table weight found; skipping")
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# PANOSE methods
|
# PANOSE methods
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
@staticmethod
|
def check_and_fix_panose(self, font: TTFont, filename: str) -> None:
|
||||||
def check_and_fix_panose(font: TTFont, filename: str) -> None:
|
|
||||||
"""
|
"""
|
||||||
Check and adjust PANOSE values based on filename suffix.
|
Check and adjust PANOSE values based on filename suffix.
|
||||||
|
PANOSE is an older classification system for fonts. Correcting these
|
||||||
Args:
|
values ensures better compatibility with legacy systems and font menus.
|
||||||
font: Font object to modify
|
|
||||||
filename: Font filename to check suffix
|
|
||||||
"""
|
"""
|
||||||
|
style_name, _ = self._get_style_from_filename(filename)
|
||||||
|
|
||||||
# PANOSE expected values for each style
|
# PANOSE expected values for each style
|
||||||
style_specs = {
|
style_specs = {
|
||||||
"-BoldItalic": {"weight": 8, "letterform": 3},
|
"Bold Italic": {"weight": 8, "letterform": 3},
|
||||||
"-Bold": {"weight": 8, "letterform": 2},
|
"Bold": {"weight": 8, "letterform": 2},
|
||||||
"-Italic": {"weight": 5, "letterform": 3},
|
"Italic": {"weight": 5, "letterform": 3},
|
||||||
"-Regular": {"weight": 5, "letterform": 2},
|
"Regular": {"weight": 5, "letterform": 2},
|
||||||
}
|
}
|
||||||
|
|
||||||
if "OS/2" not in font:
|
if "OS/2" not in font or not hasattr(font["OS/2"], "panose") or font["OS/2"].panose is None:
|
||||||
logger.warning(" No OS/2 table found; skipping PANOSE check")
|
logger.warning(" No OS/2 table or PANOSE information found; skipping check.")
|
||||||
return
|
|
||||||
|
|
||||||
if not hasattr(font["OS/2"], "panose") or font["OS/2"].panose is None:
|
|
||||||
logger.warning(" No PANOSE information; skipping PANOSE check")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
panose = font["OS/2"].panose
|
panose = font["OS/2"].panose
|
||||||
base_filename = os.path.basename(filename)
|
expected = style_specs.get(style_name)
|
||||||
|
if not expected:
|
||||||
# Find matching style
|
logger.warning(f" No PANOSE specification for style '{style_name}'; skipping.")
|
||||||
matched_style = None
|
|
||||||
for style, specs in style_specs.items():
|
|
||||||
if style in base_filename:
|
|
||||||
matched_style = style
|
|
||||||
expected = specs
|
|
||||||
break
|
|
||||||
|
|
||||||
if not matched_style:
|
|
||||||
logger.warning(
|
|
||||||
f" Filename doesn't match expected patterns {list(style_specs.keys())}. "
|
|
||||||
"PANOSE check skipped"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check and fix values
|
|
||||||
changes = []
|
changes = []
|
||||||
current_weight = getattr(panose, "bWeight", None)
|
if panose.bWeight != expected["weight"]:
|
||||||
current_letterform = getattr(panose, "bLetterForm", None)
|
|
||||||
|
|
||||||
if current_weight != expected["weight"]:
|
|
||||||
panose.bWeight = expected["weight"]
|
panose.bWeight = expected["weight"]
|
||||||
changes.append(f"bWeight {current_weight}->{expected['weight']}")
|
changes.append(f"bWeight {panose.bWeight}->{expected['weight']}")
|
||||||
|
|
||||||
if current_letterform != expected["letterform"]:
|
if panose.bLetterForm != expected["letterform"]:
|
||||||
panose.bLetterForm = expected["letterform"]
|
panose.bLetterForm = expected["letterform"]
|
||||||
changes.append(f"bLetterForm {current_letterform}->{expected['letterform']}")
|
changes.append(f"bLetterForm {panose.bLetterForm}->{expected['letterform']}")
|
||||||
|
|
||||||
if changes:
|
if changes:
|
||||||
logger.info(f" PANOSE corrected for {matched_style}: {', '.join(changes)}")
|
logger.info(f" PANOSE corrected: {', '.join(changes)}")
|
||||||
else:
|
else:
|
||||||
logger.info(f" PANOSE check passed for {matched_style}")
|
logger.info(" PANOSE check passed.")
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Line adjustment methods
|
# Line adjustment methods
|
||||||
@@ -575,46 +438,28 @@ class FontProcessor:
|
|||||||
def apply_line_adjustment(self, font_path: str) -> bool:
|
def apply_line_adjustment(self, font_path: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Apply font-line baseline adjustment to the font.
|
Apply font-line baseline adjustment to the font.
|
||||||
|
This external tool fixes an issue with line spacing on some e-readers.
|
||||||
Args:
|
The function handles the necessary file operations (renaming and cleanup)
|
||||||
font_path: Path to the font file
|
after the external utility has run.
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if successful, False otherwise
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Check if font-line is available
|
if subprocess.run(["which", "font-line"], capture_output=True).returncode != 0:
|
||||||
result = subprocess.run(
|
logger.error(" font-line utility not found. Please install it first.")
|
||||||
["which", "font-line"],
|
|
||||||
capture_output=True,
|
|
||||||
text=True
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
|
||||||
logger.error(" font-line utility not found. Please install it first")
|
|
||||||
logger.error(" See: https://github.com/source-foundry/font-line")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Apply font-line adjustment
|
subprocess.run(["font-line", "percent", str(self.line_percent), font_path], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
|
||||||
subprocess.run(
|
|
||||||
["font-line", "percent", str(self.line_percent), font_path],
|
|
||||||
check=True,
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.PIPE
|
|
||||||
)
|
|
||||||
|
|
||||||
# Handle the renamed output file
|
|
||||||
base, ext = os.path.splitext(font_path)
|
base, ext = os.path.splitext(font_path)
|
||||||
linegap_file = f"{base}-linegap{self.line_percent}{ext}"
|
linegap_file = f"{base}-linegap{self.line_percent}{ext}"
|
||||||
|
|
||||||
if os.path.exists(linegap_file):
|
if os.path.exists(linegap_file):
|
||||||
os.remove(font_path)
|
os.remove(font_path)
|
||||||
os.rename(linegap_file, font_path)
|
os.rename(linegap_file, font_path)
|
||||||
logger.info(f" Line spacing adjusted ({self.line_percent}% baseline shift)")
|
logger.info(f" Line spacing adjusted ({self.line_percent}% baseline shift).")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
logger.warning(f" Expected font-line output '{linegap_file}' not found")
|
logger.warning(f" Expected font-line output '{linegap_file}' not found.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
logger.warning(f" font-line failed: {e}")
|
logger.warning(f" font-line failed: {e}")
|
||||||
return False
|
return False
|
||||||
@@ -629,103 +474,79 @@ class FontProcessor:
|
|||||||
def process_font(self, kern: bool, remove_gpos: bool, font_path: str, new_name: Optional[str] = None) -> bool:
|
def process_font(self, kern: bool, remove_gpos: bool, font_path: str, new_name: Optional[str] = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Process a single font file.
|
Process a single font file.
|
||||||
|
This function orchestrates the entire process, calling the various
|
||||||
Args:
|
helper methods in the correct order.
|
||||||
font_path: Path to the font file
|
|
||||||
new_name: Optional new family name
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if successful, False otherwise
|
|
||||||
"""
|
"""
|
||||||
logger.info(f"\nProcessing: {font_path}")
|
logger.info(f"\nProcessing: {font_path}")
|
||||||
|
|
||||||
# Load font
|
|
||||||
try:
|
try:
|
||||||
font = TTFont(font_path)
|
font = TTFont(font_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f" Failed to open font: {e}")
|
logger.error(f" Failed to open font: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Generate metadata
|
|
||||||
metadata = self._get_font_metadata(font, font_path, new_name)
|
metadata = self._get_font_metadata(font, font_path, new_name)
|
||||||
if not metadata:
|
if not metadata:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Process font
|
|
||||||
try:
|
try:
|
||||||
# Update names
|
|
||||||
self.rename_font(font, metadata)
|
self.rename_font(font, metadata)
|
||||||
|
|
||||||
# Fix PANOSE and weight metadata
|
|
||||||
self.check_and_fix_panose(font, font_path)
|
self.check_and_fix_panose(font, font_path)
|
||||||
self.update_weight_metadata(font, font_path)
|
self.update_weight_metadata(font, font_path)
|
||||||
|
|
||||||
if kern:
|
if kern:
|
||||||
# Handle kerning
|
|
||||||
kern_pairs = self.extract_kern_pairs(font)
|
kern_pairs = self.extract_kern_pairs(font)
|
||||||
if kern_pairs:
|
if kern_pairs:
|
||||||
written = self.add_legacy_kern(font, kern_pairs)
|
written = self.add_legacy_kern(font, kern_pairs)
|
||||||
logger.info(
|
logger.info(f" Kerning: extracted {len(kern_pairs)} pairs; wrote {written} to legacy 'kern' table.")
|
||||||
f" Kerning: extracted {len(kern_pairs)} pairs; "
|
|
||||||
f"wrote {written} to legacy 'kern' table"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
logger.info(" Kerning: no GPOS kerning found")
|
logger.info(" Kerning: no GPOS kerning found.")
|
||||||
else:
|
else:
|
||||||
# Skip kerning step
|
logger.info(" Skipping `kern` step.")
|
||||||
logger.info(" Skipping `kern` step")
|
|
||||||
|
|
||||||
# Remove GPOS if requested and kerning was processed
|
# The GPOS table is removed after the kerning data has been extracted
|
||||||
|
# and written to the `kern` table. This ensures the information is not lost.
|
||||||
if remove_gpos and kern and "GPOS" in font:
|
if remove_gpos and kern and "GPOS" in font:
|
||||||
del font["GPOS"]
|
del font["GPOS"]
|
||||||
logger.info(" Removed GPOS table from the font.")
|
logger.info(" Removed GPOS table from the font.")
|
||||||
|
|
||||||
# Generate output filename
|
|
||||||
output_path = self._generate_output_path(font_path, metadata)
|
output_path = self._generate_output_path(font_path, metadata)
|
||||||
|
|
||||||
# Save modified font
|
|
||||||
font.save(output_path)
|
font.save(output_path)
|
||||||
logger.info(f" Saved: {output_path}")
|
logger.info(f" Saved: {output_path}")
|
||||||
|
|
||||||
if self.line_percent != 0:
|
if self.line_percent != 0:
|
||||||
# Apply line adjustments
|
|
||||||
self.apply_line_adjustment(output_path)
|
self.apply_line_adjustment(output_path)
|
||||||
else:
|
else:
|
||||||
logger.info(" Skipping line adjustment step")
|
logger.info(" Skipping line adjustment step.")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f" Processing failed: {e}")
|
logger.error(f" Processing failed: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _generate_output_path(self, original_path: str, metadata: FontMetadata) -> str:
|
def _generate_output_path(self, original_path: str, metadata: FontMetadata) -> str:
|
||||||
"""Generate the output path for the processed font."""
|
"""
|
||||||
|
Generate the output path for the processed font.
|
||||||
|
This function now uses the centralized `STYLE_MAP` to ensure filename
|
||||||
|
suffixes are consistent with the styles found in the font's internal metadata.
|
||||||
|
"""
|
||||||
dirname = os.path.dirname(original_path)
|
dirname = os.path.dirname(original_path)
|
||||||
original_name, ext = os.path.splitext(os.path.basename(original_path))
|
original_name, ext = os.path.splitext(os.path.basename(original_path))
|
||||||
|
|
||||||
# Detect style suffix
|
style_suffix = ""
|
||||||
suffix = ""
|
for key in STYLE_MAP:
|
||||||
for valid_suffix in VALID_SUFFIXES:
|
if key.lower() in original_name.lower():
|
||||||
if original_name.endswith(valid_suffix):
|
style_suffix = key
|
||||||
suffix = valid_suffix
|
|
||||||
break
|
break
|
||||||
|
|
||||||
# Build new filename
|
style_part = f"-{style_suffix}" if style_suffix else ""
|
||||||
base_name = f"{self.prefix}_{metadata.family_name.replace(' ', '_')}{suffix}"
|
|
||||||
|
base_name = f"{self.prefix}_{metadata.family_name.replace(' ', '_')}{style_part}"
|
||||||
|
|
||||||
return os.path.join(dirname, f"{base_name}{ext.lower()}")
|
return os.path.join(dirname, f"{base_name}{ext.lower()}")
|
||||||
|
|
||||||
|
|
||||||
def validate_font_files(font_paths: List[str]) -> Tuple[List[str], List[str]]:
|
def validate_font_files(font_paths: List[str]) -> Tuple[List[str], List[str]]:
|
||||||
"""
|
"""Validate font files for processing."""
|
||||||
Validate font files for processing.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
font_paths: List of font file paths
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple of (valid_files, invalid_files)
|
|
||||||
"""
|
|
||||||
valid_files = []
|
valid_files = []
|
||||||
invalid_files = []
|
invalid_files = []
|
||||||
|
|
||||||
@@ -733,21 +554,18 @@ def validate_font_files(font_paths: List[str]) -> Tuple[List[str], List[str]]:
|
|||||||
if not os.path.isfile(path):
|
if not os.path.isfile(path):
|
||||||
logger.warning(f"File not found: {path}")
|
logger.warning(f"File not found: {path}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not path.lower().endswith(SUPPORTED_EXTENSIONS):
|
if not path.lower().endswith(SUPPORTED_EXTENSIONS):
|
||||||
logger.warning(f"Unsupported file type: {path}")
|
logger.warning(f"Unsupported file type: {path}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check for valid suffix
|
|
||||||
basename = os.path.basename(path)
|
|
||||||
has_valid_suffix = any(
|
has_valid_suffix = any(
|
||||||
suffix in basename for suffix in VALID_SUFFIXES
|
key.lower() in os.path.basename(path).lower() for key in STYLE_MAP
|
||||||
)
|
)
|
||||||
|
|
||||||
if has_valid_suffix:
|
if has_valid_suffix:
|
||||||
valid_files.append(path)
|
valid_files.append(path)
|
||||||
else:
|
else:
|
||||||
invalid_files.append(basename)
|
invalid_files.append(os.path.basename(path))
|
||||||
|
|
||||||
return valid_files, invalid_files
|
return valid_files, invalid_files
|
||||||
|
|
||||||
@@ -765,56 +583,24 @@ Examples:
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument("fonts", nargs="+", help="Font files to process (*.ttf)")
|
||||||
"fonts",
|
parser.add_argument("--name", type=str, help="Optional new family name for all fonts")
|
||||||
nargs="+",
|
parser.add_argument("--prefix", type=str, default=DEFAULT_PREFIX, help=f"Prefix to add to font names (default: {DEFAULT_PREFIX})")
|
||||||
help="Font files to process (*.ttf)"
|
parser.add_argument("--line-percent", type=int, default=DEFAULT_LINE_PERCENT, help=f"Line spacing adjustment percentage (default: {DEFAULT_LINE_PERCENT})")
|
||||||
)
|
parser.add_argument("--skip-kobo-kern", action="store_true", help="Skip the creation of the legacy 'kern' table from GPOS data.")
|
||||||
parser.add_argument(
|
parser.add_argument("--remove-gpos", action="store_true", help="Remove the GPOS table after converting kerning to a 'kern' table.")
|
||||||
"--name",
|
parser.add_argument("--verbose", action="store_true", help="Enable verbose output")
|
||||||
type=str,
|
|
||||||
help="Optional new family name for all fonts"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--prefix",
|
|
||||||
type=str,
|
|
||||||
default=DEFAULT_PREFIX,
|
|
||||||
help=f"Prefix to add to font names (default: {DEFAULT_PREFIX})"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--line-percent",
|
|
||||||
type=int,
|
|
||||||
default=DEFAULT_LINE_PERCENT,
|
|
||||||
help=f"Line spacing adjustment percentage (default: {DEFAULT_LINE_PERCENT})"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--skip-kobo-kern",
|
|
||||||
action="store_true",
|
|
||||||
help="Skip the creation of the legacy 'kern' table from GPOS data."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--remove-gpos",
|
|
||||||
action="store_true",
|
|
||||||
help="Remove the GPOS table after converting kerning to a 'kern' table."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--verbose",
|
|
||||||
action="store_true",
|
|
||||||
help="Enable verbose output"
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Configure logging level
|
|
||||||
if args.verbose:
|
if args.verbose:
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
|
||||||
# Validate files
|
|
||||||
valid_files, invalid_files = validate_font_files(args.fonts)
|
valid_files, invalid_files = validate_font_files(args.fonts)
|
||||||
|
|
||||||
if invalid_files:
|
if invalid_files:
|
||||||
logger.error("\nERROR: The following fonts have invalid filenames:")
|
logger.error("\nERROR: The following fonts have invalid filenames:")
|
||||||
logger.error("(Must end with -Regular, -Bold, -Italic, or -BoldItalic)")
|
logger.error(f"(Must contain one of the following: {', '.join(STYLE_MAP.keys())})")
|
||||||
for filename in invalid_files:
|
for filename in invalid_files:
|
||||||
logger.error(f" {filename}")
|
logger.error(f" {filename}")
|
||||||
|
|
||||||
@@ -826,10 +612,9 @@ Examples:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if not valid_files:
|
if not valid_files:
|
||||||
logger.error("No valid font files to process")
|
logger.error("No valid font files to process.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Process fonts
|
|
||||||
processor = FontProcessor(
|
processor = FontProcessor(
|
||||||
prefix=args.prefix,
|
prefix=args.prefix,
|
||||||
line_percent=args.line_percent,
|
line_percent=args.line_percent,
|
||||||
@@ -845,13 +630,11 @@ Examples:
|
|||||||
):
|
):
|
||||||
success_count += 1
|
success_count += 1
|
||||||
|
|
||||||
# Summary
|
|
||||||
logger.info(f"\n{'='*50}")
|
logger.info(f"\n{'='*50}")
|
||||||
logger.info(f"Processed {success_count}/{len(valid_files)} fonts successfully")
|
logger.info(f"Processed {success_count}/{len(valid_files)} fonts successfully.")
|
||||||
|
|
||||||
if success_count < len(valid_files):
|
if success_count < len(valid_files):
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
Reference in New Issue
Block a user