#!/usr/bin/env python3 import fontforge, os, re, glob, shutil, sys from svgpathtools import * import lxml.etree as ET import math from random import randrange # ============================================================================ # CONFIGURATION # ============================================================================ fontname = sys.argv[1] # font source à pixeliser # Paramètres de pixelisation pixel_size = 30 # Taille des pixels en unités fonte (plus grand = moins de détails) x_height_scale = 1 # Échelle de la hauteur d'x (0.5 = écrasé, 2.0 = étiré) width_scale = 1 # Échelle de largeur (0.5 = condensé, 2.0 = étendu) monospace = True # True pour forcer l'espacement fixe monospace_width = None # Largeur fixe si monospace (None = auto-calculé) # Style des pixels pixel_shape = "square" # Options: "square", "round", "diamond" pixel_gap = 0 # Espace entre les pixels (0 = pixels collés) round_corners = 0 # Rayon d'arrondi des coins pour pixels carrés (0 = coins droits) # Paramètres d'export new_family_name = "Decor" # Nom de famille de la nouvelle typo font_weight = "Regular" # Options: "Thin", "Light", "Regular", "Medium", "Bold", "Black" font_style = "normal" # Options: "normal", "italic", "oblique" export_formats = ["ttf"] # Formats d'export souhaités # ============================================================================ # INITIALISATION # ============================================================================ try: os.mkdir("svg") os.mkdir("svg2") except: pass print("=" * 70) print("FONT PIXELIZER") print("=" * 70) print(f"Fonte source: {fontname}") print(f"Taille pixel: {pixel_size}") print(f"Échelle hauteur x: {x_height_scale}") print(f"Échelle largeur: {width_scale}") print(f"Monospace: {monospace}") print(f"Forme pixel: {pixel_shape}") print("=" * 70) print("\nOuverture de la fonte source...") font = fontforge.open(fontname) font_ascent, font_descent = font.ascent, font.descent # Stocker les infos des glyphes glyph_info = {} composite_glyphs = {} print("Extraction des glyphes...") for gly in font.glyphs(): glyph_name = gly.glyphname is_composite = len(gly.references) > 0 if is_composite: composite_glyphs[glyph_name] = { 'width': gly.width, 'unicode': gly.unicode, 'encoding': gly.encoding, 'references': [] } for ref in gly.references: composite_glyphs[glyph_name]['references'].append({ 'glyph': ref[0], 'transform': ref[1] }) print(f" Composite: {glyph_name} (unicode: {gly.unicode})") else: glyph_info[glyph_name] = { 'width': gly.width, 'unicode': gly.unicode, 'encoding': gly.encoding } try: gly.export("svg/" + glyph_name + ".svg") print(f" Exporté: {glyph_name} (unicode: {gly.unicode})") except Exception as e: print(f" Erreur export {glyph_name}: {e}") # ============================================================================ # FONCTIONS DE PIXELISATION # ============================================================================ def create_pixel_path(x, y, size, shape="square", gap=0, round_radius=0): """Crée un path SVG pour un pixel unique""" actual_size = size - gap half = actual_size / 2 if shape == "square": if round_radius > 0: # Carré avec coins arrondis r = min(round_radius, half) path_data = f""" M {x - half + r},{y - half} L {x + half - r},{y - half} Q {x + half},{y - half} {x + half},{y - half + r} L {x + half},{y + half - r} Q {x + half},{y + half} {x + half - r},{y + half} L {x - half + r},{y + half} Q {x - half},{y + half} {x - half},{y + half - r} L {x - half},{y - half + r} Q {x - half},{y - half} {x - half + r},{y - half} Z """ else: # Carré simple path_data = f""" M {x - half},{y - half} L {x + half},{y - half} L {x + half},{y + half} L {x - half},{y + half} Z """ elif shape == "round": # Cercle path_data = f""" M {x - half},{y} A {half},{half} 0 1,0 {x + half},{y} A {half},{half} 0 1,0 {x - half},{y} Z """ elif shape == "diamond": # Losange path_data = f""" M {x},{y - half} L {x + half},{y} L {x},{y + half} L {x - half},{y} Z """ return parse_path(path_data.replace("\n", " ").strip()) def pixelize_glyph(svg_path): # Pixelise un glyphe en analysant sa forme et en plaçant des pixels try: # Lire le SVG original paths, attributes = svg2paths(svg_path) if len(paths) == 0: return False # Calculer la bounding box xmin, xmax, ymin, ymax = paths[0].bbox() for path in paths[1:]: bbox = path.bbox() xmin = min(xmin, bbox[0]) xmax = max(xmax, bbox[1]) ymin = min(ymin, bbox[2]) ymax = max(ymax, bbox[3]) width = xmax - xmin height = ymax - ymin if width == 0 or height == 0: return False # Appliquer les échelles scaled_height = height * x_height_scale scaled_width = width * width_scale # Décommenter ici pour avoir des tailles de pixels aléatoires #pixel_size = randrange(30, 60, 1) # Calculer la grille de pixels cols = max(1, int(scaled_width / pixel_size)) rows = max(1, int(scaled_height / pixel_size)) # Ajuster les positions pour centrer start_x = xmin + (width - cols * pixel_size) / 2 start_y = ymin + (height - rows * pixel_size) / 2 pixel_paths = [] # Pour chaque position de pixel potentielle for row in range(rows): for col in range(cols): # Centre du pixel px = start_x + col * pixel_size + pixel_size / 2 py = start_y + row * pixel_size + pixel_size / 2 # Transformer le point selon les échelles test_x = xmin + (px - xmin) / width_scale test_y = ymin + (py - ymin) / x_height_scale # Tester si ce point est à l'intérieur des contours originaux point = complex(test_x, test_y) is_inside = False for path in paths: try: # Utiliser la méthode de raycasting # Compter combien de fois un rayon horizontal croise le contour crossings = 0 for seg in path: if hasattr(seg, 'start') and hasattr(seg, 'end'): y1 = seg.start.imag y2 = seg.end.imag x1 = seg.start.real x2 = seg.end.real if (y1 <= test_y < y2) or (y2 <= test_y < y1): if x1 + (test_y - y1) / (y2 - y1) * (x2 - x1) > test_x: crossings += 1 if crossings % 2 == 1: is_inside = True break except: pass # Si le point est à l'intérieur, créer un pixel if is_inside: pixel_path = create_pixel_path( px, py, pixel_size, shape=pixel_shape, gap=pixel_gap, round_radius=round_corners ) # Exemple de configuration avec de l'aléatoire '''pixel_path = create_pixel_path( px, py, randrange(30, 60, 1), shape=pixel_shape, gap=randrange(0, 10, 1), round_radius=randrange(0, 10, 1) )''' pixel_paths.append(pixel_path) if len(pixel_paths) == 0: return False # Créer le SVG de sortie attr = { "fill": "black", "stroke": "none" } attrs = [attr] * len(pixel_paths) # Calculer les nouvelles dimensions output_width = cols * pixel_size output_height = rows * pixel_size wsvg( pixel_paths, attributes=attrs, svg_attributes={ "width": output_width, "height": output_height, "viewBox": f"{start_x} {start_y} {output_width} {output_height}" }, filename=svg_path.replace("svg/", "svg2/"), ) return True except Exception as e: print(f"Erreur pixelisation: {e}") return False def makeFont(family_name, weight, style): # Génère la fonte finale à partir des SVG pixelisés svgDir = glob.glob("svg2/*.svg") print("\nCréation d'une nouvelle fonte vide…") newfont = fontforge.font() newfont.encoding = "UnicodeFull" newfont.ascent = int(font_ascent * x_height_scale) newfont.descent = int(font_descent * x_height_scale) newfont.em = newfont.ascent + newfont.descent # Configuration dy nom de la font et de ses paramètres newfont.familyname = family_name newfont.fontname = family_name.replace(" ", "") + "-" + weight newfont.fullname = family_name + " " + weight newfont.weight = weight newfont.appendSFNTName("English (US)", "Family", family_name) newfont.appendSFNTName("English (US)", "SubFamily", weight) newfont.appendSFNTName("English (US)", "Fullname", family_name + " " + weight) newfont.appendSFNTName("English (US)", "PostScriptName", family_name.replace(" ", "") + "-" + weight) if style == "italic": newfont.italicangle = -12 newfont.fontname += "Italic" newfont.fullname += " Italic" elif style == "oblique": newfont.italicangle = -12 newfont.fontname += "Oblique" newfont.fullname += " Oblique" weight_map = { "Thin": 100, "ExtraLight": 200, "Light": 300, "Regular": 400, "Medium": 500, "SemiBold": 600, "Bold": 700, "ExtraBold": 800, "Black": 900 } newfont.os2_weight = weight_map.get(weight, 400) # Calculer la largeur monospace si nécessaire calculated_monospace_width = monospace_width if monospace and calculated_monospace_width is None: # Calculer la largeur moyenne des glyphes widths = [info['width'] for info in glyph_info.values()] calculated_monospace_width = int(sum(widths) / len(widths) * width_scale) if widths else 500 print(f"Largeur monospace calculée: {calculated_monospace_width}") # Import des glyphes depuis SVG print(f"\nImport des glyphes pixelisés...") imported_count = 0 for glyph_path in svgDir: try: glyph_name = glyph_path.split("/")[-1].replace(".svg", "") if glyph_name not in glyph_info: continue info = glyph_info[glyph_name] # Créer le glyphe if info['unicode'] != -1: char = newfont.createChar(info['unicode'], glyph_name) else: char = newfont.createChar(-1, glyph_name) # Appliquer la largeur if monospace: char.width = calculated_monospace_width else: char.width = int(info['width'] * width_scale) char.importOutlines(glyph_path, scale=False) # Centrer le glyphe si monospace if monospace: try: char.left_side_bearing = int((calculated_monospace_width - char.width) / 2) char.width = calculated_monospace_width except: pass imported_count += 1 if imported_count % 50 == 0: print(f" ... {imported_count} glyphes importés") except Exception as e: print(f" ✗ Erreur avec {glyph_name}: {e}") print(f"✓ {imported_count} glyphes pixelisés importés") # Copie des glyphes spéciaux print("\nCopie des glyphes spéciaux...") special_count = 0 for glyph_name, info in glyph_info.items(): unicode_val = info['unicode'] if unicode_val != -1 and unicode_val not in newfont: try: special_glyph = newfont.createChar(unicode_val, glyph_name) if monospace: special_glyph.width = calculated_monospace_width else: special_glyph.width = int(info['width'] * width_scale) special_count += 1 except: pass print(f"✓ {special_count} glyphes spéciaux copiés") # Reconstruction des composites print("\nReconstruction des glyphes composites...") composite_success = 0 for glyph_name, info in composite_glyphs.items(): try: unicode_val = info['unicode'] if unicode_val == -1: continue composite_glyph = newfont.createChar(unicode_val, glyph_name) if monospace: composite_glyph.width = calculated_monospace_width else: composite_glyph.width = int(info['width'] * width_scale) all_refs_exist = True for ref in info['references']: if ref['glyph'] not in newfont: all_refs_exist = False break if not all_refs_exist: continue for ref in info['references']: # Adapter la transformation avec les échelles transform = list(ref['transform']) transform[0] *= width_scale # xx transform[3] *= x_height_scale # yy transform[4] *= width_scale # dx transform[5] *= x_height_scale # dy composite_glyph.addReference(ref['glyph'], tuple(transform)) composite_glyph.unlinkRef() composite_success += 1 except Exception as e: print(f" ✗ Erreur composite {glyph_name}: {e}") print(f"✓ {composite_success} glyphes composites créés") # Export output_name = family_name.replace(" ", "") + "-" + weight if style != "normal": output_name += style.capitalize() print(f"\nExport de la fonte...") for fmt in export_formats: try: output_file = output_name + "." + fmt if fmt == "otf": newfont.generate(output_file, flags=("opentype",)) else: newfont.generate(output_file) print(f" ✓ Exporté: {output_file}") except Exception as e: print(f" ✗ Erreur export {fmt}: {e}") newfont.close() # ============================================================================ # TRAITEMENT PRINCIPAL # ============================================================================ print("\n" + "=" * 70) print("PIXELISATION DES GLYPHES") print("=" * 70) processed = 0 failed = 0 empty = 0 for lettre in glob.glob("svg/*.svg"): try: result = pixelize_glyph(lettre) if result: processed += 1 if processed % 10 == 0: print(f" ... {processed} glyphes traités") else: empty += 1 except Exception as e: print(f" Erreur: {e}") failed += 1 print(f"\n✓ Traités: {processed}") print(f"⚠ Vides: {empty}") print(f"✗ Échecs: {failed}") print("\n" + "=" * 70) print("GÉNÉRATION DE LA FONTE") print("=" * 70) makeFont(new_family_name, font_weight, font_style) print("\n" + "=" * 70) print("TERMINÉ!") print("=" * 70) print(f"\nRésumé:") print(f" - Famille: {new_family_name}") print(f" - Poids: {font_weight}") print(f" - Taille pixel: {pixel_size}") print(f" - Échelle hauteur: {x_height_scale}") print(f" - Échelle largeur: {width_scale}") print(f" - Monospace: {monospace}") print(f" - Formats: {', '.join(export_formats)}") # Nettoyage print("\nNettoyage…") try: shutil.rmtree("svg") shutil.rmtree("svg2") print("✓ Nettoyage terminé") except: print("⚠ Nettoyage impossible")