import sys, os, struct
import re, gzip
import xml.etree.ElementTree as ET
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
FONTS_DIR = os.path.join(BASE_DIR, 'fonts')
IMAGES_DIR = os.path.join(BASE_DIR, 'images')
UNICODE_RANGES = [
(0xF200, 0x0F3FF),
(0x1F000, 0x1FFFF),
]
def u8(d, o): return d[o]
def u16(d, o): return struct.unpack_from('>H', d, o)[0]
def u32(d, o): return struct.unpack_from('>I', d, o)[0]
def i16(d, o): return struct.unpack_from('>h', d, o)[0]
def load_ttf(path):
with open(path, 'rb') as f:
return bytearray(f.read())
def get_tables(d):
num = u16(d, 4)
tables = {}
for i in range(num):
rec = 12 + i * 16
try:
tag = d[rec:rec+4].decode('latin-1')
tables[tag] = (u32(d, rec+8), u32(d, rec+12))
except:
pass
return tables
# ---------- CPAL / COLR ----------
def parse_cpal(d, off, ln):
num_colors = u16(d, off+6)
color_off = u32(d, off+8)
colors = []
base = off + color_off
for i in range(num_colors):
b = u8(d, base + i*4)
g = u8(d, base + i*4 + 1)
r = u8(d, base + i*4 + 2)
a = u8(d, base + i*4 + 3)
colors.append((r, g, b, a))
return colors
def parse_colr(d, off, ln):
numBase = u16(d, off+2)
baseOff = u32(d, off+4)
layerOff = u32(d, off+8)
numLayers = u16(d, off+12)
bases = []
ba = off + baseOff
for i in range(numBase):
bases.append((u16(d, ba + i*6),
u16(d, ba + i*6 + 2),
u16(d, ba + i*6 + 4)))
layers = []
la = off + layerOff
for i in range(numLayers):
layers.append((u16(d, la + i*4),
u16(d, la + i*4 + 2)))
return bases, layers
# ---------- cmap ----------
def parse_cmap_full(d, off, ln):
gid_to_cp = {}
cp_to_gid = {}
num_tables = u16(d, off + 2)
for i in range(num_tables):
platform = u16(d, off + 4 + i*8)
encoding = u16(d, off + 6 + i*8)
sub_off = u32(d, off + 8 + i*8)
sub_abs = off + sub_off
if sub_abs >= len(d):
continue
fmt = u16(d, sub_abs)
if fmt == 4:
seg_cnt = u16(d, sub_abs + 6) // 2
end_arr = [u16(d, sub_abs + 14 + i*2) for i in range(seg_cnt)]
start_arr = [u16(d, sub_abs + 16 + seg_cnt*2 + i*2) for i in range(seg_cnt)]
delta_arr = [i16(d, sub_abs + 16 + seg_cnt*4 + i*2) for i in range(seg_cnt)]
range_arr = [u16(d, sub_abs + 16 + seg_cnt*6 + i*2) for i in range(seg_cnt)]
gid_base = sub_abs + 16 + seg_cnt*8
for s in range(seg_cnt):
sc, ec = start_arr[s], end_arr[s]
if sc == 0xFFFF:
continue
for cp in range(sc, ec+1):
ro = range_arr[s]
if ro == 0:
gid = (cp + delta_arr[s]) & 0xFFFF
else:
idx = gid_base + s*2 + (cp - sc)*2 + ro - (seg_cnt - s)*2
gid = u16(d, idx) if idx+2 <= len(d) else 0
if gid:
gid = (gid + delta_arr[s]) & 0xFFFF
if gid:
cp_to_gid[cp] = gid
if gid not in gid_to_cp:
gid_to_cp[gid] = cp
elif fmt == 12:
ng = u32(d, sub_abs + 12)
for g in range(ng):
sc = u32(d, sub_abs + 16 + g*12)
ec = u32(d, sub_abs + 20 + g*12)
sg = u32(d, sub_abs + 24 + g*12)
for j in range(ec - sc + 1):
cp = sc + j
gid = sg + j
cp_to_gid[cp] = gid
if gid not in gid_to_cp:
gid_to_cp[gid] = cp
return gid_to_cp, cp_to_gid
# ---------- loca / glyf ----------
def parse_head(d, off, ln):
return (i16(d, off+50), u16(d, off+18))
def parse_maxp(d, off, ln):
return u16(d, off+4)
def parse_loca(d, off, ln, num_glyphs, fmt):
loca = []
if fmt == 0:
for i in range(num_glyphs+1):
loca.append(u16(d, off + i*2) * 2)
else:
for i in range(num_glyphs+1):
loca.append(u32(d, off + i*4))
return loca
def get_glyph_contours(d, glyf_off, loca, gid, depth=0):
if depth > 8 or gid + 1 >= len(loca):
return []
g_off = glyf_off + loca[gid]
g_end = glyf_off + loca[gid+1]
if g_off >= g_end or g_off + 10 > len(d):
return []
nc = i16(d, g_off)
if nc < 0:
ARG_1_AND_2_ARE_WORDS = 0x0001
ARGS_ARE_XY_VALUES = 0x0002
WE_HAVE_A_SCALE = 0x0008
MORE_COMPONENTS = 0x0020
WE_HAVE_AN_X_AND_Y_SCALE = 0x0040
WE_HAVE_A_TWO_BY_TWO = 0x0080
WE_HAVE_INSTRUCTIONS = 0x0100
contours = []
pos = g_off + 10
while True:
if pos + 4 > len(d):
break
flags = u16(d, pos); pos += 2
cg = u16(d, pos); pos += 2
if flags & ARG_1_AND_2_ARE_WORDS:
arg1 = i16(d, pos); pos += 2
arg2 = i16(d, pos); pos += 2
else:
arg1 = struct.unpack_from('b', d, pos)[0]; pos += 1
arg2 = struct.unpack_from('b', d, pos)[0]; pos += 1
a, b, c, dd = 1.0, 0.0, 0.0, 1.0
if flags & WE_HAVE_A_SCALE:
s = i16(d, pos) / 16384.0; pos += 2
a = dd = s
elif flags & WE_HAVE_AN_X_AND_Y_SCALE:
a = i16(d, pos) / 16384.0; pos += 2
dd = i16(d, pos) / 16384.0; pos += 2
elif flags & WE_HAVE_A_TWO_BY_TWO:
a = i16(d, pos) / 16384.0; pos += 2
b = i16(d, pos) / 16384.0; pos += 2
c = i16(d, pos) / 16384.0; pos += 2
dd = i16(d, pos) / 16384.0; pos += 2
dx, dy = (arg1, arg2) if (flags & ARGS_ARE_XY_VALUES) else (0, 0)
sub = get_glyph_contours(d, glyf_off, loca, cg, depth + 1)
for contour in sub:
contours.append([
(a * x + c * y + dx, b * x + dd * y + dy) for x, y in contour
])
if not (flags & MORE_COMPONENTS):
break
return contours
if nc == 0:
return []
end_pts = [u16(d, g_off + 10 + i*2) for i in range(nc)]
num_pts = end_pts[-1] + 1
il = u16(d, g_off + 10 + nc*2)
pos = g_off + 10 + nc*2 + 2 + il
flags = []
while len(flags) < num_pts and pos < len(d):
fl = u8(d, pos); pos += 1
flags.append(fl)
if fl & 8:
rep = u8(d, pos); pos += 1
for _ in range(rep):
if len(flags) >= num_pts:
break
flags.append(fl)
xs, x = [], 0
for fl in flags:
if fl & 2:
dx = u8(d, pos); pos += 1
x += dx if (fl & 16) else -dx
elif not (fl & 16):
x += i16(d, pos); pos += 2
xs.append(x)
ys, y = [], 0
for fl in flags:
if fl & 4:
dy = u8(d, pos); pos += 1
y += dy if (fl & 32) else -dy
elif not (fl & 32):
y += i16(d, pos); pos += 2
ys.append(y)
contours = []
prev = 0
for ep in end_pts:
contours.append([(xs[j], ys[j]) for j in range(prev, ep + 1)])
prev = ep + 1
return contours
def render_simple_glyph_svg(d, glyf_off, loca, gid, size=256):
contours = get_glyph_contours(d, glyf_off, loca, gid)
if not contours:
return None
all_x = [x for c in contours for x, y in c]
all_y = [y for c in contours for x, y in c]
xmin, xmax = min(all_x), max(all_x)
ymin, ymax = min(all_y), max(all_y)
if xmax == xmin or ymax == ymin:
return None
margin = size * 0.06
scale = min((size - 2*margin) / (xmax - xmin),
(size - 2*margin) / (ymax - ymin))
ox = (size - (xmax - xmin) * scale) / 2
oy = (size - (ymax - ymin) * scale) / 2
def tr(x, y):
return ((x - xmin) * scale + ox, (ymax - y) * scale + oy)
paths = []
for contour in contours:
if len(contour) < 2:
continue
pts = [tr(x, y) for x, y in contour]
pd = f"M{pts[0][0]:.1f},{pts[0][1]:.1f}"
pd += "".join(f"L{px:.1f},{py:.1f}" for px, py in pts[1:])
pd += "Z"
paths.append(f' <path d="{pd}" fill="black" />')
if not paths:
return None
return (f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<svg xmlns="http://www.w3.org/2000/svg" '
f'width="{size}" height="{size}" viewBox="0 0 {size} {size}">\n'
+ "\n".join(paths) + "\n</svg>")
def render_colr_svg(d, tables, loca, colors, layers, first_layer, num_layers, size=256):
glyf_off = tables['glyf'][0]
all_x, all_y = [], []
layer_data = []
for li in range(first_layer, first_layer + num_layers):
if li >= len(layers):
break
gid, pidx = layers[li]
contours = get_glyph_contours(d, glyf_off, loca, gid)
layer_data.append((contours, pidx))
for c in contours:
for x, y in c:
all_x.append(x)
all_y.append(y)
if not all_x:
return None
xmin, xmax = min(all_x), max(all_x)
ymin, ymax = min(all_y), max(all_y)
if xmax == xmin or ymax == ymin:
return None
margin = size * 0.06
scale = min((size - 2*margin) / (xmax - xmin),
(size - 2*margin) / (ymax - ymin))
ox = (size - (xmax - xmin) * scale) / 2
oy = (size - (ymax - ymin) * scale) / 2
def tr(x, y):
return ((x - xmin) * scale + ox, (ymax - y) * scale + oy)
paths = []
for contours, pidx in layer_data:
r, g, b, a = colors[pidx] if pidx < len(colors) else (128,128,128,255)
fill = f"#{r:02X}{g:02X}{b:02X}"
op = round(a / 255, 3)
for contour in contours:
if len(contour) < 2:
continue
pts = [tr(x, y) for x, y in contour]
pd = f"M{pts[0][0]:.1f},{pts[0][1]:.1f}"
pd += "".join(f"L{px:.1f},{py:.1f}" for px, py in pts[1:])
pd += "Z"
paths.append(f' <path d="{pd}" fill="{fill}" fill-opacity="{op}"/>')
if not paths:
return None
return (f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<svg xmlns="http://www.w3.org/2000/svg" '
f'width="{size}" height="{size}" viewBox="0 0 {size} {size}">\n'
+ "\n".join(paths) + "\n</svg>")
def _mat_mul(M, T):
return [
M[0]*T[0] + M[2]*T[1],
M[1]*T[0] + M[3]*T[1],
M[0]*T[2] + M[2]*T[3],
M[1]*T[2] + M[3]*T[3],
M[0]*T[4] + M[2]*T[5] + M[4],
M[1]*T[4] + M[3]*T[5] + M[5],
]
def _parse_transform(tstr):
M = [1, 0, 0, 1, 0, 0]
for func, args in re.findall(r'(\w+)\s*\(([^)]*)\)', tstr):
n = [float(x) for x in re.findall(r'-?\d+\.?\d*(?:[eE][-+]?\d+)?', args)]
if func == 'matrix' and len(n) == 6: T = n
elif func == 'translate':
tx = n[0] if n else 0
ty = n[1] if len(n) > 1 else 0
T = [1, 0, 0, 1, tx, ty]
elif func == 'scale':
sx = n[0] if n else 1
sy = n[1] if len(n) > 1 else sx
T = [sx, 0, 0, sy, 0, 0]
elif func == 'rotate' and n:
import math
a = math.radians(n[0])
ca, sa = math.cos(a), math.sin(a)
T = [ca, sa, -sa, ca, 0, 0]
if len(n) >= 3:
cx, cy = n[1], n[2]
T = _mat_mul([1,0,0,1,cx,cy], _mat_mul(T, [1,0,0,1,-cx,-cy]))
else:
continue
M = _mat_mul(M, T)
return M
def _apply(M, x, y):
return (M[0]*x + M[2]*y + M[4], M[1]*x + M[3]*y + M[5])
def _walk_bbox(elem, M, xs, ys):
t = elem.get('transform')
if t:
M = _mat_mul(M, _parse_transform(t))
tag = elem.tag.rsplit('}', 1)[-1]
if tag == 'path':
d = elem.get('d', '')
nums = [float(n) for n in re.findall(r'-?\d+\.?\d*(?:[eE][-+]?\d+)?', d)]
for i in range(0, len(nums) - 1, 2):
px, py = _apply(M, nums[i], nums[i+1])
xs.append(px); ys.append(py)
elif tag == 'rect':
try:
x = float(elem.get('x', 0)); y = float(elem.get('y', 0))
w = float(elem.get('width', 0)); h = float(elem.get('height', 0))
for cx, cy in [(x, y), (x+w, y), (x+w, y+h), (x, y+h)]:
px, py = _apply(M, cx, cy)
xs.append(px); ys.append(py)
except Exception:
pass
elif tag == 'circle':
try:
cx = float(elem.get('cx', 0)); cy = float(elem.get('cy', 0))
r = float(elem.get('r', 0))
for ax, ay in [(cx-r, cy-r), (cx+r, cy+r)]:
px, py = _apply(M, ax, ay)
xs.append(px); ys.append(py)
except Exception:
pass
elif tag in ('polyline', 'polygon'):
pts = elem.get('points', '')
nums = [float(n) for n in re.findall(r'-?\d+\.?\d*(?:[eE][-+]?\d+)?', pts)]
for i in range(0, len(nums) - 1, 2):
px, py = _apply(M, nums[i], nums[i+1])
xs.append(px); ys.append(py)
elif tag == 'line':
try:
for ax, ay in [(float(elem.get('x1',0)), float(elem.get('y1',0))),
(float(elem.get('x2',0)), float(elem.get('y2',0)))]:
px, py = _apply(M, ax, ay)
xs.append(px); ys.append(py)
except Exception:
pass
for child in elem:
_walk_bbox(child, M, xs, ys)
def _compute_rendered_bbox(svg_text):
body = re.sub(r'<\?xml[^?]*\?>', '', svg_text, count=1)
body = re.sub(r'\sxmlns(?::\w+)?\s*=\s*"[^"]*"', '', body)
try:
root = ET.fromstring(body)
except ET.ParseError:
return None
xs, ys = [], []
_walk_bbox(root, [1, 0, 0, 1, 0, 0], xs, ys)
if not xs or not ys:
return None
return min(xs), min(ys), max(xs), max(ys)
def fix_ot_svg(raw):
if raw[:2] == b'\x1f\x8b':
try:
raw = gzip.decompress(raw)
except Exception:
return raw
text = raw.decode('utf-8', errors='replace')
bbox = _compute_rendered_bbox(text)
if bbox is None:
return text.encode('utf-8')
xmin, ymin, xmax, ymax = bbox
w = xmax - xmin
h = ymax - ymin
if w <= 0 or h <= 0:
return text.encode('utf-8')
pad = max(w, h) * 0.05
vb = f"{xmin - pad:.2f} {ymin - pad:.2f} {w + 2*pad:.2f} {h + 2*pad:.2f}"
m = re.search(r'<svg\b[^>]*>', text)
if not m:
return text.encode('utf-8')
old_tag = m.group(0)
new_tag = re.sub(r'\s(?:viewBox|width|height)\s*=\s*"[^"]*"', '', old_tag)
new_tag = new_tag[:-1] + f' viewBox="{vb}" width="256" height="256">'
return text.replace(old_tag, new_tag, 1).encode('utf-8')
def extract_svg_table(d, tables, out_dir, gid_to_cps, target_codes=None):
if 'SVG ' not in tables:
return 0, set()
svg_off = tables['SVG '][0]
dl_off = u32(d, svg_off + 2)
dl_base = svg_off + dl_off
num_e = u16(d, dl_base)
saved = 0
svg_gids = set()
for e in range(num_e):
eb = dl_base + 2 + e*12
sg = u16(d, eb)
eg = u16(d, eb+2)
doff = u32(d, eb+4)
dlen = u32(d, eb+8)
dbase = dl_base + doff
if dbase + dlen > len(d) or dlen == 0:
continue
raw = bytes(d[dbase:dbase+dlen])
fixed = fix_ot_svg(raw)
for g in range(sg, eg + 1):
svg_gids.add(g)
cps = list(gid_to_cps.get(g, []))
if target_codes:
wanted = [c for c in cps if c in target_codes]
if wanted:
cps = wanted
if cps:
for cp in cps:
fn = os.path.join(out_dir, f"U{cp:05X}_gid{g:04d}.svg")
with open(fn, 'wb') as f:
f.write(fixed)
saved += 1
else:
fn = os.path.join(out_dir, f"gid{g:04d}.svg")
with open(fn, 'wb') as f:
f.write(fixed)
saved += 1
return saved, svg_gids
def make_html_gallery(out_dir, gid_to_cp, font_name):
all_svgs = []
for root, dirs, files in os.walk(out_dir):
for fn in sorted(files):
if fn.endswith('.svg'):
rel = os.path.relpath(os.path.join(root, fn), out_dir)
cp = 0
if fn.startswith('U') and '_' in fn:
try:
cp = int(fn[1:6], 16)
except:
pass
all_svgs.append((rel, cp, fn))
if not all_svgs:
return
html = f'''<!DOCTYPE html>
<html><head>
<meta charset="UTF-8">
<title>{font_name} - Emoji Gallery</title>
<style>
body{{background:#1a1a2e;color:#eee;font-family:monospace;padding:20px}}
h1{{color:#FFD632}}
.grid{{display:flex;flex-wrap:wrap;gap:10px}}
.item{{
background:#16213e;border:1px solid #0f3460;border-radius:8px;
padding:8px;text-align:center;width:120px;cursor:pointer;
}}
.item:hover{{background:#0f3460}}
.item img{{width:80px;height:80px}}
.item .code{{font-size:10px;color:#888;margin-top:4px}}
.item .name{{font-size:11px;color:#FFD632}}
input{{
background:#16213e;color:#eee;border:1px solid #0f3460;
padding:8px;width:300px;margin-bottom:20px;border-radius:4px
}}
</style>
</head><body>
<h1>{font_name} - {len(all_svgs)} glyphs</h1>
<input type="text" id="search" placeholder="Search by U+code..." oninput="filterGlyphs()">
<div class="grid" id="grid">
'''
for rel, cp, fn in all_svgs:
code_str = f"U+{cp:05X}" if cp else fn
html += f''' <div class="item" title="{code_str}">
<img src="{rel.replace(chr(92),'/')}" alt="{code_str}">
<div class="name">{code_str}</div>
<div class="code">{fn[:20]}</div>
</div>\n'''
html += '''</div>
<script>
function filterGlyphs(){
var q = document.getElementById('search').value.toLowerCase();
document.querySelectorAll('.item').forEach(function(el){
el.style.display = el.title.toLowerCase().includes(q) ? '' : 'none';
});
}
</script>
</body></html>'''
html_path = os.path.join(out_dir, 'gallery.html')
with open(html_path, 'w', encoding='utf-8') as f:
f.write(html)
print(f"Gallery: {html_path}")
def load_target_codes(valid_emojis_path):
"""Читает valid_emojis.txt и добавляет UNICODE_RANGES."""
codes = set()
if os.path.exists(valid_emojis_path):
with open(valid_emojis_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line.startswith('U+'):
try:
codes.add(int(line[2:], 16))
except:
pass
for start, end in UNICODE_RANGES:
for cp in range(start, end + 1):
codes.add(cp)
return codes if codes else None
def get_system_emoji_font():
"""Возвращает путь к Segoe UI Emoji, если он есть."""
possible = [
os.path.join(os.environ.get('WINDIR', 'C:\\Windows'), 'Fonts', 'seguiemj.ttf'),
'/System/Library/Fonts/Apple Color Emoji.ttc',
]
for p in possible:
if os.path.exists(p):
return p
return None
def process_font(ttf_path, out_dir, target_codes, rendered_set):
"""Обрабатывает один шрифт, возвращает множество отрендеренных кодов."""
name = os.path.basename(ttf_path).replace('.ttf', '')
print(f"\n{'='*60}")
print(f"Font: {name}")
os.makedirs(out_dir, exist_ok=True)
d = load_ttf(ttf_path)
tables = get_tables(d)
print(f"Tables: {list(tables.keys())}")
index_fmt, units = 0, 1000
if 'head' in tables:
index_fmt, units = parse_head(d, *tables['head'])
num_glyphs = 0
if 'maxp' in tables:
num_glyphs = parse_maxp(d, *tables['maxp'])
print(f"numGlyphs={num_glyphs} locaFmt={index_fmt} unitsPerEm={units}")
loca = []
if 'loca' in tables and num_glyphs > 0:
loca = parse_loca(d, tables['loca'][0], tables['loca'][1], num_glyphs, index_fmt)
gid_to_cp, cp_to_gid = {}, {}
if 'cmap' in tables:
gid_to_cp, cp_to_gid = parse_cmap_full(d, *tables['cmap'])
print(f"cmap: {len(cp_to_gid)} cp->gid mappings")
DEBUG_CODES = [0xF230, 0xF231, 0xF232, 0xF233, 0xF234, 0xF23C]
for cp in DEBUG_CODES:
gid = cp_to_gid.get(cp)
if gid is None:
continue
print(f" >>> U+{cp:05X} -> gid={gid}")
if loca and gid + 1 < len(loca) and 'glyf' in tables:
g_off_abs = tables['glyf'][0] + loca[gid]
g_end_abs = tables['glyf'][0] + loca[gid+1]
sz = g_end_abs - g_off_abs
print(f" glyf size: {sz} bytes")
if sz > 0 and g_off_abs + 10 <= len(d):
nc = i16(d, g_off_abs)
kind = 'simple' if nc > 0 else ('composite' if nc < 0 else 'empty')
print(f" numContours: {nc} ({kind})")
try:
svg = render_simple_glyph_svg(d, tables['glyf'][0], loca, gid)
print(f" render: {'OK' if svg else 'EMPTY/None'}")
except Exception as e:
print(f" render ERROR: {type(e).__name__}: {e}")
gid_to_cps = {}
for cp_, gid_ in cp_to_gid.items():
gid_to_cps.setdefault(gid_, []).append(cp_)
colors = []
if 'CPAL' in tables:
colors = parse_cpal(d, *tables['CPAL'])
print(f"CPAL: {len(colors)} colors")
svg_dir = os.path.join(out_dir, 'svg_table')
os.makedirs(svg_dir, exist_ok=True)
svg_rendered = set()
if 'SVG ' in tables:
n, svg_rendered = extract_svg_table(d, tables, svg_dir, gid_to_cps, target_codes)
print(f"SVG table: {n} files saved")
for gid in svg_rendered:
cp = gid_to_cp.get(gid)
if cp:
rendered_set.add(cp)
colr_rendered = set()
if 'COLR' in tables and loca and 'glyf' in tables:
bases, layers = parse_colr(d, *tables['COLR'])
print(f"\nRendering {len(bases)} COLR glyphs as SVG...")
colr_dir = os.path.join(out_dir, 'colr_svg')
os.makedirs(colr_dir, exist_ok=True)
rendered = 0
for gid, first, num in bases:
colr_rendered.add(gid)
try:
svg = render_colr_svg(d, tables, loca, colors, layers, first, num, size=256)
if not svg:
continue
cps = gid_to_cps.get(gid, [])
if target_codes:
wanted = [c for c in cps if c in target_codes]
if wanted:
cps = wanted
if cps:
for cp in cps:
rendered_set.add(cp)
fn = os.path.join(colr_dir, f"U{cp:05X}_gid{gid:04d}.svg")
with open(fn, 'w', encoding='utf-8') as f:
f.write(svg)
rendered += 1
else:
fn = os.path.join(colr_dir, f"gid{gid:04d}.svg")
with open(fn, 'w', encoding='utf-8') as f:
f.write(svg)
rendered += 1
except Exception as e:
print(f" ERR gid={gid}: {e}")
print(f"COLR rendered {rendered}")
unicode_dir = os.path.join(out_dir, 'unicode_glyphs')
os.makedirs(unicode_dir, exist_ok=True)
total_rendered = 0
glyf_off = tables['glyf'][0] if 'glyf' in tables else None
if glyf_off and loca and target_codes:
for cp in target_codes:
if cp in rendered_set:
continue
gid = cp_to_gid.get(cp)
if gid is None:
continue
if gid in colr_rendered or gid in svg_rendered:
continue
svg = render_simple_glyph_svg(d, glyf_off, loca, gid, size=256)
if svg:
fn = os.path.join(unicode_dir, f"U{cp:05X}_gid{gid:04d}.svg")
with open(fn, 'w', encoding='utf-8') as f:
f.write(svg)
rendered_set.add(cp)
total_rendered += 1
print(f"Simple glyphs rendered: {total_rendered}")
make_html_gallery(out_dir, gid_to_cp, name)
def main():
print(f"Working dir: {BASE_DIR}")
print(f"Fonts dir : {FONTS_DIR}")
print(f"Output dir: {IMAGES_DIR}")
valid_emojis_path = os.path.join(BASE_DIR, 'valid_emojis.txt')
target_codes = load_target_codes(valid_emojis_path)
if target_codes:
print(f"Target codes: {len(target_codes)} (shortcodes + ranges)")
else:
print("valid_emojis.txt not found, will use built-in ranges")
if not os.path.isdir(FONTS_DIR):
print(f"ERROR: Folder '{FONTS_DIR}' not found!")
sys.exit(1)
ttf_files = [f for f in os.listdir(FONTS_DIR) if f.lower().endswith('.ttf')]
if not ttf_files:
print("No .ttf files in fonts folder.")
return
os.makedirs(IMAGES_DIR, exist_ok=True)
all_rendered = set()
for fname in sorted(ttf_files):
ttf_path = os.path.join(FONTS_DIR, fname)
font_name = os.path.splitext(fname)[0]
out_subdir = os.path.join(IMAGES_DIR, font_name)
process_font(ttf_path, out_subdir, target_codes, all_rendered)
if target_codes:
missing = target_codes - all_rendered
if missing:
print(f"\n{'='*60}")
print(f"Missing emojis: {len(missing)} codes not found in local fonts.")
sys_font = get_system_emoji_font()
if sys_font:
print(f"Using system emoji font: {sys_font}")
sys_out = os.path.join(IMAGES_DIR, 'system_emoji')
os.makedirs(sys_out, exist_ok=True)
try:
sys_d = load_ttf(sys_font)
sys_tables = get_tables(sys_d)
sys_gid2cp, sys_cp2gid = {}, {}
if 'cmap' in sys_tables:
sys_gid2cp, sys_cp2gid = parse_cmap_full(sys_d, *sys_tables['cmap'])
sys_loca = []
sys_glyf_off = None
sys_n_glyphs = 0
if 'loca' in sys_tables and 'maxp' in sys_tables and 'glyf' in sys_tables:
sys_n_glyphs = parse_maxp(sys_d, *sys_tables['maxp'])
if 'head' in sys_tables:
idx_fmt, _ = parse_head(sys_d, *sys_tables['head'])
else:
idx_fmt = 1
sys_loca = parse_loca(sys_d, sys_tables['loca'][0], sys_tables['loca'][1],
sys_n_glyphs, idx_fmt)
sys_glyf_off = sys_tables['glyf'][0]
sys_colors = []
sys_colr_bases, sys_colr_layers = [], []
if 'COLR' in sys_tables and 'CPAL' in sys_tables and sys_loca and sys_glyf_off:
sys_colors = parse_cpal(sys_d, *sys_tables['CPAL'])
sys_colr_bases, sys_colr_layers = parse_colr(sys_d, *sys_tables['COLR'])
colr_gid_map = {}
for gid, first, num in sys_colr_bases:
colr_gid_map[gid] = (first, num)
rendered_sys = 0
for cp in missing:
gid = sys_cp2gid.get(cp)
if gid is None:
continue
svg = None
if gid in colr_gid_map:
first, num = colr_gid_map[gid]
try:
svg = render_colr_svg(sys_d, sys_tables, sys_loca, sys_colors,
sys_colr_layers, first, num, size=256)
except Exception as e:
print(f" COLr error U+{cp:05X}: {e}")
if svg is None and sys_glyf_off and sys_loca:
svg = render_simple_glyph_svg(sys_d, sys_glyf_off, sys_loca, gid, size=256)
if svg:
fn = os.path.join(sys_out, f"U{cp:05X}_gid{gid:04d}.svg")
with open(fn, 'w', encoding='utf-8') as f:
f.write(svg)
all_rendered.add(cp)
rendered_sys += 1
print(f"System font rendered {rendered_sys} additional emojis.")
make_html_gallery(sys_out, sys_gid2cp, "System Emoji")
except Exception as e:
print(f"Failed to process system font: {e}")
else:
print("System emoji font not found, some emojis remain missing.")
else:
print("\nAll target emojis rendered successfully!")
print("\nCreating overall gallery...")
make_html_gallery(IMAGES_DIR, {}, "All Emojis")
print("\n=== DONE ===")
print(f"All results are in: {IMAGES_DIR}")
print("Open any gallery.html to view the glyphs!")
os.system("pause")
if __name__ == "__main__":
main()