SA:MP Arizona [Arizona RP] Как/откуда сейчас вытащить актуальный шрифт с иконками чата?

La3ert

Новичок
Автор темы
8
0
Собственно, вопрос в шапке. Аризона снова выкатила новые кастомные иконки в чате.

Ранее я уже немного ковырял их систему: разобрался, как работают шорткоды (типа :uf280:) и лигатуры, через которые движок рендерит картинки прямо в тексте. Но с каждым их апдейтом вырезать новые иконки вручную или искать, где они теперь лежат — тот еще геморрой.

Подскажите, кто потрошил лаунчер:
  • Из каких файлов сейчас можно вытянуть актуальный кастомный шрифт (или атлас/спрайты), который юзается для чата?
  • В каких архивах или по какому пути в сборке они сейчас это прячут?
  • Может, у кого-то есть тул/скрипт или просто рабочий метод, чтобы быстро дампить актуальные ассеты чата после их обновлений?
Хочу автоматизировать этот момент, чтобы не возиться руками при каждой обнове.

Буду благодарен за любую инфу и наводки!
 

La3ert

Новичок
Автор темы
8
0
А можешь пожалуйста дать название всех 4 шрифтов?
1777885900084.png

У них свои шрифты, так что нужно будет использовать файлы
 

La3ert

Новичок
Автор темы
8
0
А можешь сам текст скрипта сюда скинуть?
Там есть два скрипта. Луа, который достаёт шрифты, и пайтоновский, который достаёт картинки из шрифтов.
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()

local ffi = require 'ffi' ffi.cdef [[ void* GetModuleHandleA(const char* lpModuleName); bool IsBadReadPtr(const void* lp, unsigned int ucb); void* FindResourceA(void* hModule, const char* lpName, const char* lpType); unsigned long SizeofResource(void* hModule, void* hResInfo); void* LoadResource(void* hModule, void* hResInfo); void* LockResource(void* hData); void* malloc(size_t size); void free(void* ptr); ]] ffi.cdef [[ typedef struct _IMAGE_DOS_HEADER { uint16_t e_magic; uint16_t e_cblp; uint16_t e_cp; uint16_t e_crlc; uint16_t e_cparhdr; uint16_t e_minalloc; uint16_t e_maxalloc; uint16_t e_ss; uint16_t e_sp; uint16_t e_csum; uint16_t e_ip; uint16_t e_cs; uint16_t e_lfarlc; uint16_t e_ovno; uint16_t e_res[4]; uint16_t e_oemid; uint16_t e_oeminfo; uint16_t e_res2[10]; int32_t e_lfanew; } IMAGE_DOS_HEADER; typedef struct _IMAGE_FILE_HEADER { uint16_t Machine; uint16_t NumberOfSections; uint32_t TimeDateStamp; uint32_t PointerToSymbolTable; uint32_t NumberOfSymbols; uint16_t SizeOfOptionalHeader; uint16_t Characteristics; } IMAGE_FILE_HEADER; typedef struct _IMAGE_OPTIONAL_HEADER { uint16_t Magic; uint8_t MajorLinkerVersion; uint8_t MinorLinkerVersion; uint32_t SizeOfCode; uint32_t SizeOfInitializedData; uint32_t SizeOfUninitializedData; uint32_t AddressOfEntryPoint; uint32_t BaseOfCode; uint32_t BaseOfData; uint32_t ImageBase; uint32_t SectionAlignment; uint32_t FileAlignment; uint16_t MajorOperatingSystemVersion; uint16_t MinorOperatingSystemVersion; uint16_t MajorImageVersion; uint16_t MinorImageVersion; uint16_t MajorSubsystemVersion; uint16_t MinorSubsystemVersion; uint32_t Win32VersionValue; uint32_t SizeOfImage; uint32_t SizeOfHeaders; uint32_t CheckSum; uint16_t Subsystem; uint16_t DllCharacteristics; uint32_t SizeOfStackReserve; uint32_t SizeOfStackCommit; uint32_t SizeOfHeapReserve; uint32_t SizeOfHeapCommit; uint32_t LoaderFlags; uint32_t NumberOfRvaAndSizes; } IMAGE_OPTIONAL_HEADER; typedef struct _IMAGE_NT_HEADERS { uint32_t Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER OptionalHeader; } IMAGE_NT_HEADERS; typedef struct _IMAGE_SECTION_HEADER { char Name[8]; uint32_t VirtualSize; uint32_t VirtualAddress; uint32_t SizeOfRawData; uint32_t PointerToRawData; uint32_t PointerToRelocations; uint32_t PointerToLinenumbers; uint16_t NumberOfRelocations; uint16_t NumberOfLinenumbers; uint32_t Characteristics; } IMAGE_SECTION_HEADER; ]] ffi.cdef [[ int __cdecl convert(char* outBuf, size_t outSize, const char* input, const char* end); ]] ffi.cdef [[ typedef int (__stdcall *ENUMRESNAMEPROC)(void* hModule, const char* lpType, char* lpName, long lParam); int EnumResourceNamesA(void* hModule, const char* lpType, ENUMRESNAMEPROC lpEnumFunc, long lParam); ]] function findPattern(hModule, pattern, startOffset) local base = tonumber(ffi.cast("uintptr_t", hModule)) local dos = ffi.cast("IMAGE_DOS_HEADER*", base) if dos.e_magic ~= 0x5A4D then return nil end local nt = ffi.cast("IMAGE_NT_HEADERS*", base + dos.e_lfanew) if nt.Signature ~= 0x00004550 then return nil end local sizeOfImage = nt.OptionalHeader.SizeOfImage local patLen = #pattern local start = base + (startOffset or 0) local endAddr = base + sizeOfImage - patLen local p = ffi.cast("uint8_t*", start) while p <= ffi.cast("uint8_t*", endAddr) do if ffi.C.IsBadReadPtr(p, patLen) then break end local found = true for i = 1, patLen do if pattern[i] ~= 0xCC and p[i - 1] ~= pattern[i] then found = false break end end if found then return tonumber(ffi.cast("uintptr_t", p)) end p = p + 1 end return nil end function r16be(buf, off) return buf[off] * 256 + buf[off + 1] end function loadRes(hMod, id, rtype) local hi = ffi.C.FindResourceA(hMod, ffi.cast("const char*", id), ffi.cast("const char*", rtype)) if hi == nil then return nil, 0 end local sz = ffi.C.SizeofResource(hMod, hi) if sz == 0 then return nil, 0 end local hd = ffi.C.LoadResource(hMod, hi) if hd == nil then return nil, 0 end local p = ffi.C.LockResource(hd) if p == nil then return nil, 0 end return ffi.cast("uint8_t*", p), tonumber(sz) end function saveBytes(buf, off, size, path) local f = io.open(path, "wb") if not f then return false end local tmp = {} for i = off, off + size - 1 do tmp[#tmp + 1] = string.char(buf[i]) if #tmp >= 8192 then f:write(table.concat(tmp)); tmp = {} end end if #tmp > 0 then f:write(table.concat(tmp)) end f:close() return true end function decompressUsing(buffer, decompSz, ptr, fnDecomp) local outBuf = ffi.new("unsigned char[?]", decompSz + 16) ffi.fill(outBuf, decompSz + 16, 0) fnDecomp(outBuf, ptr) return outBuf, decompSz end function detectFormat(ptr, sz) if sz < 4 then return "bin" end local b0, b1, b2, b3 = ptr[0], ptr[1], ptr[2], ptr[3] if (b0 == 0x00 and b1 == 0x01 and b2 == 0x00 and b3 == 0x00) or (b0 == 0x4F and b1 == 0x54 and b2 == 0x54 and b3 == 0x4F) or (b0 == 0x74 and b1 == 0x72 and b2 == 0x75 and b3 == 0x65) then return "ttf" end if b0 == 0x77 and b1 == 0x4F and b2 == 0x46 and b3 == 0x46 then return "woff" end if b0 == 0x45 and b1 == 0x4F and b2 == 0x54 and b3 == 0x47 then return "eot" end if b0 == 0x49 and b1 == 0x44 and b2 == 0x33 then return "mp3" end if b0 == 0x52 and b1 == 0x49 and b2 == 0x46 and b3 == 0x46 then return "wav" end if b0 == 0x4F and b1 == 0x67 and b2 == 0x67 and b3 == 0x53 then return "ogg" end if sz >= 8 and b0 == 0x89 and b1 == 0x50 and b2 == 0x4E and b3 == 0x47 and ptr[4] == 0x0D and ptr[5] == 0x0A and ptr[6] == 0x1A and ptr[7] == 0x0A then return "png" end if b0 == 0xFF and b1 == 0xD8 and b2 == 0xFF then return "jpg" end if b0 == 0x44 and b1 == 0x44 and b2 == 0x53 and b3 == 0x20 then return "dds" end if b0 == 0x42 and b1 == 0x4D then return "bmp" end if b0 == 0x00 and b1 == 0x00 and b2 == 0x01 and b3 == 0x00 then return "ico" end if b0 == 0x1F and b1 == 0x8B then return "gz" end if b0 == 0x3C and b1 == 0x3F and b2 == 0x78 and b3 == 0x6D then return "xml" end if b0 == 0x3C and b1 == 0x73 and b2 == 0x76 and b3 == 0x67 then return "svg" end return "bin" end function looksLikeTTF(buf, sz) if sz < 16 then return false end local numTables = r16be(buf, 4) if numTables < 1 or numTables > 40 then return false end local b0, b1, b2, b3 = buf[12], buf[13], buf[14], buf[15] if b0 < 0x20 or b0 > 0x7E or b1 < 0x20 or b1 > 0x7E or b2 < 0x20 or b2 > 0x7E or b3 < 0x20 or b3 > 0x7E then return false end return true end function main() while not isSampAvailable() do wait(100) end wait(2000) sampRegisterChatCommand('poiskShrift', function() lua_thread.create(doWork) end) wait(-1) end function doWork() local hMod = ffi.C.GetModuleHandleA("_chat.asi") if hMod == nil then print("_chat.asi not found") return end local sigGetSize = { 0x55, 0x8B, 0xEC, 0x5D, 0xE9, 0x1E, 0x06, 0x00, 0x00 } local sigDecomp = { 0x55, 0x8B, 0xEC, 0x5D, 0xE9, 0x33, 0x05, 0x00, 0x00 } local addrGetSize = findPattern(hMod, sigGetSize, 0x10000) local addrDecomp = findPattern(hMod, sigDecomp, 0x10000) if not addrGetSize or not addrDecomp then print("ERROR: decompression functions not found by signature!") return end print(string.format("getSize @ 0x%X", addrGetSize)) print(string.format("decomp @ 0x%X", addrDecomp)) local fnGetSize = ffi.cast("unsigned int(__cdecl*)(void*)", addrGetSize) local fnDecomp = ffi.cast("void(__cdecl*)(unsigned char*, void*)", addrDecomp) local sigConvert = { 0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x1C, 0xA1, 0xCC, 0xCC, 0xCC, 0xCC, 0x33, 0xC5, 0x89, 0x45, 0xFC, 0x8B, 0x45, 0x14, 0x56, 0x8B, 0x75, 0x10, 0x57 } local addrConvert = findPattern(hMod, sigConvert, 0x10000) local fnConvert = nil if addrConvert then print(string.format("convert @ 0x%X", addrConvert)) fnConvert = ffi.cast("int(__cdecl*)(char*, size_t, const char*, const char*)", addrConvert) else print("WARNING: convert not found by signature, emoji export will be skipped") end local dir = getWorkingDirectory() local outRoot = dir .. "\\shrifts" local fontsDir = outRoot .. "\\fonts" local audioDir = outRoot .. "\\audio" local imagesDir = outRoot .. "\\images_raw" local otherDir = outRoot .. "\\other" os.execute('mkdir "' .. outRoot .. '" 2>nul') os.execute('mkdir "' .. fontsDir .. '" 2>nul') os.execute('mkdir "' .. audioDir .. '" 2>nul') os.execute('mkdir "' .. imagesDir .. '" 2>nul') os.execute('mkdir "' .. otherDir .. '" 2>nul') local resourceTypes = { { 10, fontsDir }, -- RT_RCDATA { 8, fontsDir }, -- RT_FONT { 2, imagesDir }, -- RT_BITMAP { 14, imagesDir }, -- RT_GROUP_ICON { 6, otherDir }, -- RT_STRING } for _, rt in ipairs(resourceTypes) do local rtype = rt[1] local targetDir = rt[2] local ids = {} local enumCallback = ffi.cast("ENUMRESNAMEPROC", function(hMod, lpType, lpName, lParam) local namePtr = tonumber(ffi.cast("uintptr_t", lpName)) if namePtr < 0x10000 then ids[#ids + 1] = namePtr end return 1 end) ffi.C.EnumResourceNamesA(hMod, ffi.cast("const char*", rtype), enumCallback, 0) print(string.format("\n=== Resource type %d: %d items ===", rtype, #ids)) for _, id in ipairs(ids) do local name = string.format("type%d_id%04X", rtype, id) local ptr, rawSz = loadRes(hMod, id, rtype) if ptr == nil then print(string.format("0x%04X: load failed", id)) else local dataPtr = ptr local dataSz = rawSz local isPacked = false if (rtype == 10 or rtype == 8) and rawSz >= 8 then local magic = ptr[0] * 0x1000000 + ptr[1] * 0x10000 + ptr[2] * 0x100 + ptr[3] local field4 = ptr[4] * 0x1000000 + ptr[5] * 0x10000 + ptr[6] * 0x100 + ptr[7] if magic == 0x57BC0000 and field4 == 0 then isPacked = true local ok, decompSz = pcall(function() return fnGetSize(ptr) end) if ok and decompSz > 0 and decompSz <= 50 * 1024 * 1024 then local outBuf, _ = decompressUsing(nil, decompSz, ptr, fnDecomp) if outBuf then dataPtr = outBuf dataSz = decompSz print(string.format(" decompressed, size=%d bytes", decompSz)) else print(" Decompress failed (null buffer)") goto nextResource end else print(" Decompress failed (invalid size or error)") goto nextResource end end end local fmt = detectFormat(dataPtr, dataSz) local ext = fmt print(string.format("0x%04X: %d bytes, format=%s", id, dataSz, fmt)) if fmt == "bin" and looksLikeTTF(dataPtr, dataSz) then fmt = "ttf" ext = "ttf" end local targetPath if fmt == "ttf" or fmt == "woff" or fmt == "eot" then targetPath = fontsDir .. "\\" .. name .. "." .. ext elseif fmt == "mp3" or fmt == "wav" or fmt == "ogg" then targetPath = audioDir .. "\\" .. name .. "." .. ext elseif fmt == "png" or fmt == "jpg" or fmt == "bmp" or fmt == "dds" or fmt == "ico" then targetPath = imagesDir .. "\\" .. name .. "." .. ext elseif fmt == "xml" or fmt == "svg" or fmt == "gz" then targetPath = otherDir .. "\\" .. name .. "." .. ext else targetPath = otherDir .. "\\" .. name .. "." .. ext end saveBytes(dataPtr, 0, dataSz, targetPath) print(" Saved as " .. targetPath) end ::nextResource:: wait(0) end end print("\n=== Extracting shortcode emojis via signature ===") local base = tonumber(ffi.cast("uintptr_t", hMod)) local dos = ffi.cast("IMAGE_DOS_HEADER*", base) local nt = ffi.cast("IMAGE_NT_HEADERS*", base + dos.e_lfanew) local function extractShortcodes() local joyPattern = { 0x6A, 0x6F, 0x79, 0x00 } local joyAddr = findPattern(hMod, joyPattern, 0) if not joyAddr then print("ERROR: 'joy' string not found!") return end print(string.format("Found 'joy' at 0x%X", joyAddr)) local roflPattern = { 0x72, 0x6F, 0x66, 0x6C, 0x00 } local roflAddr = findPattern(hMod, roflPattern, 0) if not roflAddr then print("ERROR: 'rofl' string not found!") return end print(string.format("Found 'rofl' at 0x%X", roflAddr)) local tableStart = nil local imageSize = nt.OptionalHeader.SizeOfImage local scanStart = base local scanEnd = base + imageSize - 12 local scanPtr = ffi.cast("uint32_t*", scanStart) local iterations = 0 while scanPtr <= ffi.cast("uint32_t*", scanEnd) do if scanPtr[0] == joyAddr and scanPtr[2] == roflAddr then tableStart = tonumber(ffi.cast("uintptr_t", scanPtr)) print(string.format("Table start found at 0x%X", tableStart)) break end scanPtr = scanPtr + 1 iterations = iterations + 1 if iterations % 4096 == 0 then wait(0) end end if not tableStart then print("ERROR: shortcode table signature not found in module!") return end local extracted = {} local seen = {} local ptr = ffi.cast("uint32_t*", tableStart) while true do local strAddr = ptr[0] local codepoint = ptr[1] if not (strAddr >= base and strAddr < base + imageSize and codepoint > 0x7F) then break end local strPtr = ffi.cast("uint8_t*", strAddr) if ffi.C.IsBadReadPtr(strPtr, 1) then break end local chars = {} for i = 0, 30 do if ffi.C.IsBadReadPtr(strPtr + i, 1) then break end local b = strPtr[i] if b == 0 or b < 0x20 or b > 0x7E then break end table.insert(chars, string.char(b)) end local name = table.concat(chars) if not name:match("^[a-zA-Z][a-zA-Z0-9_]*$") then break end if not seen[codepoint] then extracted[#extracted + 1] = { name = name, cp = codepoint } seen[codepoint] = true print(string.format(" :%s: -> U+%05X", name, codepoint)) end ptr = ptr + 2 if tonumber(ffi.cast("uintptr_t", ptr)) >= base + imageSize then break end if tonumber(ffi.cast("uintptr_t", ptr)) % 4096 == 0 then wait(0) end end print(string.format("Extracted %d shortcode emojis", #extracted)) local fname = outRoot .. "\\valid_emojis.txt" local existing = {} local fRead = io.open(fname, "r") if fRead then for line in fRead:lines() do existing[line:match("^(U%+[0-9A-F]+)") or ""] = true end fRead:close() end local fApp = io.open(fname, "a") if fApp then local added = 0 for _, item in ipairs(extracted) do local line = string.format("U+%05X", item.cp) if not existing[line] then fApp:write(line .. "\n") existing[line] = true added = added + 1 end end fApp:close() print("Added " .. added .. " new shortcode codes") end end extractShortcodes() print("Shortcode extraction finished.") local pythonScript = outRoot .. "\\render_colr.py" if io.open(pythonScript, "r") then print("\n=== Launching Python renderer ===") os.execute('python "' .. pythonScript .. '"') else print("\nPython script not found at " .. pythonScript .. ", skipping rendering.") end print("\n=== All resources saved to " .. outRoot .. " ===") end


А можно как-то использовать эти иконки через шрифт по юникоду? Или только заменять на SVGшки?
 
Последнее редактирование: