import gradio as gr import os import tempfile import zipfile import io import struct import zlib from pathlib import Path # ========================================== # Pure-Python Font Conversion (no fonttools) # ========================================== def detect_font_format(file_path): with open(file_path, 'rb') as f: header = f.read(4) if header == b'wOFF': return 'woff' elif header == b'wOF2': return 'woff2' elif header in (b'\x00\x01\x00\x00', b'true', b'typ1'): return 'ttf' elif header == b'OTTO': return 'otf' return 'unknown' def woff_to_sfnt(data: bytes) -> bytes: (signature, flavor, length, num_tables, reserved, total_sfnt_size, major, minor, meta_offset, meta_length, meta_orig_length, priv_offset, priv_length) = struct.unpack_from('>4sIIHHIHHIIIII', data, 0) woff_entries = [] offset = 44 for _ in range(num_tables): tag, woff_off, comp_len, orig_len, orig_checksum = struct.unpack_from('>4sIIII', data, offset) woff_entries.append((tag, woff_off, comp_len, orig_len, orig_checksum)) offset += 20 woff_entries.sort(key=lambda e: e[0]) sfnt_tables = [] current_offset = 12 + num_tables * 16 for tag, woff_off, comp_len, orig_len, orig_checksum in woff_entries: table_data = data[woff_off: woff_off + comp_len] if comp_len < orig_len: table_data = zlib.decompress(table_data) padded = table_data + b'\x00' * ((4 - len(table_data) % 4) % 4) sfnt_tables.append((tag, orig_checksum, current_offset, orig_len, padded)) current_offset += len(padded) n = num_tables search_range = 1 entry_selector = 0 while search_range * 2 <= n: search_range *= 2 entry_selector += 1 search_range *= 16 range_shift = n * 16 - search_range out = io.BytesIO() out.write(struct.pack('>4sHHHH', flavor, n, search_range, entry_selector, range_shift)) for tag, checksum, tbl_offset, length, _ in sfnt_tables: out.write(struct.pack('>4sIII', tag, checksum, tbl_offset, length)) for tag, checksum, tbl_offset, length, padded in sfnt_tables: out.write(padded) return out.getvalue() def sfnt_to_woff(data: bytes) -> bytes: flavor = data[:4] num_tables = struct.unpack_from('>H', data, 4)[0] sfnt_tables = [] offset = 12 for _ in range(num_tables): tag, checksum, tbl_offset, length = struct.unpack_from('>4sIII', data, offset) raw = data[tbl_offset: tbl_offset + length] sfnt_tables.append((tag, checksum, length, raw)) offset += 16 woff_entries = [] woff_data = io.BytesIO() current_offset = 44 + num_tables * 20 for tag, checksum, orig_len, raw in sfnt_tables: compressed = zlib.compress(raw, level=9) if len(compressed) >= orig_len: compressed = raw comp_len = len(compressed) woff_entries.append((tag, current_offset, comp_len, orig_len, checksum)) woff_data.write(compressed) pad = (4 - comp_len % 4) % 4 woff_data.write(b'\x00' * pad) current_offset += comp_len + pad total_length = current_offset total_sfnt_size = (12 + num_tables * 16 + sum(((len(r) + 3) & ~3) for _, _, _, r in sfnt_tables)) out = io.BytesIO() out.write(struct.pack('>4sIIHHIHHIIIII', b'wOFF', flavor if len(flavor) == 4 else b'\x00\x01\x00\x00', total_length, num_tables, 0, total_sfnt_size, 1, 0, 0, 0, 0, 0, 0)) for tag, woff_off, comp_len, orig_len, checksum in woff_entries: out.write(struct.pack('>4sIIII', tag, woff_off, comp_len, orig_len, checksum)) out.write(woff_data.getvalue()) return out.getvalue() def convert_font_bytes(src_data, src_fmt, tgt_fmt): src, tgt = src_fmt.lower(), tgt_fmt.lower() if src == tgt: return src_data, None if src == 'woff2' or tgt == 'woff2': return None, "WOFF2 requires Brotli codec (unavailable). Use TTF, OTF, or WOFF." if src == 'woff' and tgt in ('ttf', 'otf'): return woff_to_sfnt(src_data), None if src in ('ttf', 'otf') and tgt == 'woff': return sfnt_to_woff(src_data), None if src in ('ttf', 'otf') and tgt in ('ttf', 'otf'): return src_data, None return None, f"Unsupported: {src.upper()} → {tgt.upper()}" def get_font_info(input_file): if input_file is None: return "*Upload a font to see its details here...*" try: with open(input_file, 'rb') as f: data = f.read() fmt = detect_font_format(input_file) info = [f"**Format:** {fmt.upper()}", f"**File size:** {len(data):,} bytes"] if fmt in ('ttf', 'otf'): num_tables = struct.unpack_from('>H', data, 4)[0] info.append(f"**Tables:** {num_tables}") for i in range(num_tables): tag, _, offset, length = struct.unpack_from('>4sIII', data, 12 + i * 16) if tag == b'name': name_data = data[offset: offset + length] count = struct.unpack_from('>H', name_data, 2)[0] storage_offset = struct.unpack_from('>H', name_data, 4)[0] names = {} for j in range(count): pid, eid, lid, nid, nlen, noff = struct.unpack_from('>HHHHHH', name_data, 6 + j * 12) raw = name_data[storage_offset + noff: storage_offset + noff + nlen] try: val = raw.decode('utf-16-be') if pid == 3 else raw.decode('latin-1') except Exception: continue if nid not in names: names[nid] = val for nid, label in [(0,"Copyright"),(1,"Family"),(2,"Style"),(4,"Full Name"),(5,"Version")]: if nid in names: info.append(f"**{label}:** {names[nid]}") break elif fmt == 'woff': flavor = struct.unpack_from('>4s', data, 4)[0] info.append(f"**SFNT flavor:** {'OTF/CFF' if flavor == b'OTTO' else 'TTF'}") num_tables = struct.unpack_from('>H', data, 12)[0] info.append(f"**Tables:** {num_tables}") return "\n\n".join(info) except Exception as e: return f"❌ Could not read font info: {e}" def convert_font(input_file, output_formats): if input_file is None: return None, "❌ Please upload a font file first." if not output_formats: return None, "❌ Please select at least one output format." with open(input_file, 'rb') as f: src_data = f.read() src_fmt = detect_font_format(input_file) if src_fmt == 'unknown': src_fmt = Path(input_file).suffix.lower().lstrip('.') base_name = Path(input_file).stem results, errors = [], [] zip_buffer = io.BytesIO() converted_count = 0 with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf: for fmt in output_formats: tgt = fmt.lower() if tgt == src_fmt: continue out_bytes, err = convert_font_bytes(src_data, src_fmt, tgt) if err: errors.append(f"❌ {fmt.upper()} — {err}") else: zf.writestr(f"{base_name}.{tgt}", out_bytes) converted_count += 1 results.append(f"✅ {fmt.upper()} — converted successfully") if converted_count == 0: return None, "\n".join(errors) if errors else "⚠️ No conversions performed." pz = tempfile.NamedTemporaryFile(delete=False, suffix='.zip', prefix=f"{base_name}_") pz.write(zip_buffer.getvalue()) pz.close() status = [f"📦 **{src_fmt.upper()}** → {converted_count} format(s) converted"] + results if errors: status += ["", "**Errors:**"] + errors return pz.name, "\n".join(status) def ping(): return {"status": "alive", "service": "FontForge API", "version": "2.0.0", "message": "Font conversion service is running! 🎨"} with gr.Blocks( title="🔤 Font Converter API", theme=gr.themes.Base(primary_hue="violet", neutral_hue="slate"), css=".gradio-container{max-width:900px!important;margin:auto}.title{text-align:center;padding:20px 0}footer{display:none!important}" ) as demo: gr.HTML('
Convert fonts between TTF, OTF and WOFF — no dependencies needed