| import gradio as gr |
| import os |
| import tempfile |
| import zipfile |
| import io |
| import struct |
| import zlib |
| from pathlib import Path |
|
|
| |
| |
| |
|
|
| 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('<div class="title"><h1>π€ Font Converter API</h1><p>Convert fonts between TTF, OTF and WOFF β no dependencies needed</p></div>') |
| with gr.Tabs(): |
| with gr.TabItem("π Convert Font"): |
| with gr.Row(): |
| with gr.Column(scale=1): |
| font_input = gr.File(label="Upload Font File", file_types=[".ttf",".otf",".woff",".woff2"], type="filepath") |
| format_checkboxes = gr.CheckboxGroup(choices=["TTF","OTF","WOFF"], value=["TTF","WOFF"], label="Convert To") |
| gr.Markdown("*WOFF2 not supported (requires Brotli). TTF / OTF / WOFF only.*") |
| convert_btn = gr.Button("π Convert Font", variant="primary", size="lg") |
| with gr.Column(scale=1): |
| font_info = gr.Markdown(value="*Upload a font to see its details here...*") |
| status_output = gr.Markdown() |
| download_output = gr.File(label="π₯ Download Converted Fonts (ZIP)") |
| font_input.change(fn=get_font_info, inputs=[font_input], outputs=[font_info]) |
| convert_btn.click(fn=convert_font, inputs=[font_input, format_checkboxes], outputs=[download_output, status_output]) |
| with gr.TabItem("π API Usage"): |
| gr.Markdown(""" |
| ## Supported Conversions |
| | From \\ To | TTF | OTF | WOFF | |
| |-----------|-----|-----|------| |
| | TTF | β | β
| β
| |
| | OTF | β
| β | β
| |
| | WOFF | β
| β
| β | |
| | WOFF2 | β | β | β | |
| |
| ### Python Client |
| ```python |
| from gradio_client import Client, handle_file |
| client = Client("YOUR_USERNAME/font-converter") |
| zip_path, status = client.predict( |
| input_file=handle_file("font.ttf"), |
| output_formats=["TTF", "WOFF"], |
| api_name="/convert_font" |
| ) |
| ``` |
| """) |
| with gr.TabItem("π Health Check"): |
| gr.Markdown("**Monitor URL:** `https://YOUR_USERNAME-font-converter.hf.space/` β set UptimeRobot to 5-minute interval.") |
| with gr.Row(): |
| ping_btn = gr.Button("π Test Health Check", variant="secondary") |
| ping_output = gr.JSON(label="Response") |
| ping_btn.click(fn=ping, outputs=[ping_output]) |
|
|
| demo.queue() |
| demo.launch(server_name="0.0.0.0", server_port=7860, show_api=True, show_error=True) |
|
|