decor-6-expe/font_pixellizer/font_pixelizer.py

506 lines
17 KiB
Python
Raw Normal View History

2026-02-10 21:30:40 +01:00
#!/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
2026-02-10 21:35:00 +01:00
#pixel_size = randrange(30, 60, 1)
2026-02-10 21:30:40 +01:00
# 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")