akurapluz / app.py
mrpoddaa's picture
Upload 3 files
4b73001 verified
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('<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)