Cat Feeder  1.0.0
The Cat feeder project
Loading...
Searching...
No Matches
font_to_font.py
Go to the documentation of this file.
1r"""
2# +==== BEGIN CatFeeder =================+
3# LOGO:
4# ..............(..../\
5# ...............)..(.')
6# ..............(../..)
7# ...............\‍(__)|
8# Inspired by Joan Stark
9# source https://www.asciiart.eu/
10# animals/cats
11# /STOP
12# PROJECT: CatFeeder
13# FILE: image_to_image.py
14# CREATION DATE: 15-01-2026
15# LAST Modified: 6:14:23 15-01-2026
16# DESCRIPTION:
17# This is the backend server in charge of making the actual website work.
18# /STOP
19# COPYRIGHT: (c) Cat Feeder
20# PURPOSE: The file containing the code for converting fonts to other font formats.
21# // AR
22# +==== END CatFeeder =================+
23"""
24from __future__ import annotations
25from io import BytesIO
26from typing import Optional, Dict, Union
27from fontTools.ttLib import TTFont, TTLibError
28
29from display_tty import Disp, initialise_logger
30from ..http_constants import DataTypes, MEDIA_TYPES
31from . import converters_constants as CONV_CONST
32from ...core import FinalClass
33from ...utils import CONST
34
35
36class FontToFont(metaclass=FinalClass):
37 """Convert fonts between web-compatible formats (TTF, OTF, WOFF, WOFF2)."""
38
39 disp: Disp = initialise_logger(__qualname__, CONST.DEBUG)
40 _instance: Optional["FontToFont"] = None
41
42 def __new__(cls) -> "FontToFont":
43 if cls._instance is None:
44 cls._instance = super().__new__(cls)
45 return cls._instance
46
47 def __init__(self) -> None:
48 self.disp.log_debug("Initialising...")
49 self.disp.log_debug("Initialised.")
50
52 self, data: bytes, source_format: DataTypes, generate_css: bool = True
53 ) -> CONV_CONST.ConversionResult:
54 return self.font_to_font(data, source_format, generate_css)
55
56 # ------------------------- Main Conversion ------------------------- #
57 def font_to_font(
58 self, data: bytes, source_format: DataTypes, generate_css: bool = True
59 ) -> CONV_CONST.ConversionResult:
60 """_summary_
61
62 Args:
63 data (bytes): _description_
64 source_format (DataTypes): _description_
65 generate_css (bool, optional): _description_. Defaults to True.
66
67 Returns:
68 CONV_CONST.ConversionResult: _description_
69 """
70
71 # Unsupported formats
72 if not MEDIA_TYPES.is_font(source_format):
73 self.disp.log_warning(f"Unsupported font format: {source_format}")
74 return self._build_result(data, False, source_format, source_format, None)
75
76 # EOT is read-only
77 if source_format == DataTypes.EOT:
78 self.disp.log_warning("EOT is read-only; returning original data")
79 return self._build_result(data, False, source_format, source_format, data)
80
81 # Load font
82 font = self._load_font(data)
83 if font is None:
84 return self._build_result(data, False, source_format, source_format, None)
85
86 # Determine target formats
87 targets = self._determine_targets(source_format)
88
89 # Convert each target
90 converted_files = self._convert_all_targets(font, data, targets)
91
92 # Generate CSS if requested
93 css_text = self._generate_css(converted_files) if generate_css else ""
94
95 font.close()
96
97 # Wrap all converted files in FontResult
98 font_result = CONV_CONST.FontResult(
99 ttf=converted_files.get(DataTypes.TTF, b""),
100 otf=converted_files.get(DataTypes.OTF, b""),
101 woff=converted_files.get(DataTypes.WOFF, b""),
102 woff2=converted_files.get(DataTypes.WOFF2, b""),
103 css=css_text
104 )
105
106 return self._build_result(
107 data=data,
108 converted=True,
109 from_type=source_format,
110 to_type=None,
111 result=font_result
112 )
113
114 # ------------------------- Helper Functions ------------------------- #
115 def _load_font(self, data: bytes) -> Optional[TTFont]:
116 try:
117 return TTFont(BytesIO(data))
118 except TTLibError as e:
119 self.disp.log_error(f"Cannot load font: {e}")
120 return None
121
122 def _determine_targets(self, source_format: DataTypes) -> list[DataTypes]:
123 targets = [DataTypes.WOFF, DataTypes.WOFF2]
124 if source_format in (DataTypes.TTF, DataTypes.OTF):
125 targets.append(source_format)
126 return targets
127
129 self, font: TTFont, data: bytes, targets: list[DataTypes]
130 ) -> Dict[DataTypes, bytes]:
131 converted_files: Dict[DataTypes, bytes] = {}
132 for target in targets:
133 converted = self._convert_target(data, target)
134 if converted:
135 converted_files[target] = converted
136 return converted_files
137
138 def _convert_target(self, data: bytes, target: DataTypes) -> Optional[bytes]:
139 try:
140 font_copy = TTFont(BytesIO(data))
141 out_buffer = BytesIO()
142
143 if target == DataTypes.WOFF:
144 font_copy.flavor = "woff"
145 font_copy.save(out_buffer)
146 elif target == DataTypes.WOFF2:
147 font_copy.flavor = "woff2"
148 font_copy.save(out_buffer)
149 else: # TTF/OTF passthrough
150 font_copy.save(out_buffer)
151
152 return out_buffer.getvalue()
153 except Exception as e:
154 self.disp.log_error(f"Failed to convert to {target}: {e}")
155 return None
156
157 def _generate_css(self, converted_files: Dict[DataTypes, bytes]) -> str:
158 family = self._get_family_name(
159 next(iter(converted_files.values()))) or "Unknown"
160
161 css_srcs = []
162 for fmt, _ in converted_files.items():
163 ext = fmt.name.lower()
164 css_fmt = "opentype" if fmt == DataTypes.OTF else fmt.name.lower()
165 css_srcs.append(f"url('{family}.{ext}') format('{css_fmt}')")
166
167 return (
168 f"@font-face {{\n"
169 f" font-family: '{family}';\n"
170 f" font-style: normal;\n"
171 f" font-weight: 400;\n"
172 f" src: {', '.join(css_srcs)};\n"
173 f" font-display: swap;\n"
174 f"}}\n"
175 )
176
177 def _get_family_name(self, font_bytes: bytes) -> Optional[str]:
178 try:
179 tt = TTFont(BytesIO(font_bytes))
180 name_table = tt["name"]
181 for rec in name_table.names:
182 if rec.nameID == 1: # Font Family Name
183 try:
184 return str(rec.toUnicode())
185 except Exception:
186 return str(rec)
187 return None
188 except TTLibError:
189 return None
190
192 self,
193 data: bytes,
194 converted: bool,
195 from_type: DataTypes,
196 to_type: Optional[DataTypes],
197 result: Optional[Union[bytes, str, CONV_CONST.FontResult]] = None
198 ) -> CONV_CONST.ConversionResult:
199 if not to_type:
200 return CONV_CONST.ConversionResult(
201 data=data,
202 converted=False,
203 from_type=from_type,
204 to_type=from_type,
205 result=data
206 )
207 return CONV_CONST.ConversionResult(
208 data=data,
209 converted=converted,
210 from_type=from_type,
211 to_type=to_type,
212 result=result
213 )
Dict[DataTypes, bytes] _convert_all_targets(self, TTFont font, bytes data, list[DataTypes] targets)
CONV_CONST.ConversionResult __call__(self, bytes data, DataTypes source_format, bool generate_css=True)
str _generate_css(self, Dict[DataTypes, bytes] converted_files)
Optional[bytes] _convert_target(self, bytes data, DataTypes target)
CONV_CONST.ConversionResult font_to_font(self, bytes data, DataTypes source_format, bool generate_css=True)
CONV_CONST.ConversionResult _build_result(self, bytes data, bool converted, DataTypes from_type, Optional[DataTypes] to_type, Optional[Union[bytes, str, CONV_CONST.FontResult]] result=None)
list[DataTypes] _determine_targets(self, DataTypes source_format)