commit 67eb7fa3153fb3b15f5915a4cdd61e18bb11f79f Author: sarahgarcin1 Date: Tue Feb 10 21:30:40 2026 +0100 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4afdc02 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Python +*/venv + +# Logs +logs +*.log + + +# Editor directories and files +.DS_Store diff --git a/ascii_writer/README.md b/ascii_writer/README.md new file mode 100644 index 0000000..cf01461 --- /dev/null +++ b/ascii_writer/README.md @@ -0,0 +1,20 @@ +# ASCII Writer + +Une bibliothèque python pour dessiner avec du texte brut dans le style ASCII (mais avec Unicode). +Par Manetta Berends + +## Système +- Linux +- OSX + +## Dépendance +Ce site peut aider à l'installation: https://avoidsoftware.sarahgarcin.com/dependances.html + +- Python 3 +- Asciiwriter ```pip3 install asciiwriter``` + +## Marche à suivre +- Télécharger les fichiers .py +- Ouvrir le terminal et lancer le script python souhaité: `python3 line.py` +- Le résultat s'affiche dans le terminal +-Vous pouvez ensuite tester les autres exemples présents dans le dossier et modifier les variables et le texte dans les fichiers Python. \ No newline at end of file diff --git a/ascii_writer/line.py b/ascii_writer/line.py new file mode 100644 index 0000000..9f05136 --- /dev/null +++ b/ascii_writer/line.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 + +""" + Draws a single, vertical line +""" + +from asciiWriter.patterns import vertical +from asciiWriter.utils import make_lines, visit, print_lines +from asciiWriter.marks import sentence, space + +# Set the canvas +width = 75 +height = 75 + +# Define the line, most importantly it's position +pattern = vertical(20) +# We're going to fill the line with a text +mark = sentence('AVOID SOFTWARE ') +# Set the character for the 'blank' space +blank = space() + +# Make a canvas +lines = make_lines(width, height) +# Draw the result +result = visit(lines, pattern, mark, blank) + +# Print the result +print_lines(result) diff --git a/ascii_writer/line_random.py b/ascii_writer/line_random.py new file mode 100644 index 0000000..57dc7d9 --- /dev/null +++ b/ascii_writer/line_random.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 + +""" + Draws a single just like line.py but does so + using a 'random' character +""" + +from asciiWriter.patterns import vertical +from asciiWriter.utils import make_lines, visit, print_lines +from asciiWriter.marks import random, space + +# Set the canvas +width = 75 +height = 75 + +# Define the line, most importantly it's position +image_pattern = vertical(20) +# We're going to fill the line with random selections +# from the '%$&@!*' chars +mark = random('%$&@!*') +# Set the character for the 'blank' space +blank = space() + +# Make a canvas +lines = make_lines(width, height) +# Draw the result +result = visit(lines, image_pattern, mark, blank) + +# Print the result +print_lines(result) diff --git a/ascii_writer/repeated_lines.py b/ascii_writer/repeated_lines.py new file mode 100644 index 0000000..902b663 --- /dev/null +++ b/ascii_writer/repeated_lines.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 + +""" + Draws lines like line.py, but draws more than one +""" + +from asciiWriter.patterns import vertical +from asciiWriter.utils import make_lines, visit, print_lines, merge +from asciiWriter.marks import sentence, space + +# Set the canvas +width = 75 +height = 75 + +# We are going to draw multiple lines and collect them +# in a list named 'layers' +layers = [] + +# Set the position of the line, do this in a loop +# from 10 to 75 in steps of then +for x in range(10, 75, 10): + # Define the line, x will start at 10 and grow in steps of 10 + image_pattern = vertical(x) + # Fill the line with the sentence 'OPEN DESIGN COURSE ' + mark = sentence('AVOID SOFTWARE ') + # Set the blank space + blank = space() + + # Make a canvas + lines = make_lines(width, height) + # Make a layer with the line + layer = visit(lines, image_pattern, mark, blank) + # Add the layer to the list of layers + layers.append(layer) + +# Merge the list of layers into a single layer +result = merge(width, height, blank(), layers) +# Print the result +print_lines(result) diff --git a/ascii_writer/repeated_sinus.py b/ascii_writer/repeated_sinus.py new file mode 100644 index 0000000..5abab03 --- /dev/null +++ b/ascii_writer/repeated_sinus.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 + +from asciiWriter.patterns import sinus_vertical +from asciiWriter.utils import make_lines, visit, print_lines, merge +from asciiWriter.marks import sentence, space + +# Define width and height of the output +width = 80 +height = 70 + +# As we draw multiple sinoids we will collect +# them in a list of layers +layers = [] + +# Loop through an offset from -40 to 40 in steps of 10 +for x in range(-40, 40, 10): + # Set the pattern with the changing offset + pattern = sinus_vertical(period=40, amplitude=40, offset=x) + # We use a sentence to draw the text + mark = sentence('convivialité ') + # Define a blank character + blank = space() + + # Make the canvas + lines = make_lines(width, height) + + # Draw the sinoid, but add it to the list + result = visit(lines, pattern, mark, blank) + # Add it the result to the list of layers + layers.append(result) + +# Merge the layers into one layer again +merged = merge(width, height, blank(), layers) + +# Print the result +print_lines(merged) diff --git a/ascii_writer/repeated_sinus_amplitude_variation.py b/ascii_writer/repeated_sinus_amplitude_variation.py new file mode 100644 index 0000000..4c8c25c --- /dev/null +++ b/ascii_writer/repeated_sinus_amplitude_variation.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 + +from asciiWriter.patterns import sinus_vertical +from asciiWriter.utils import make_lines, visit, print_lines, merge +from asciiWriter.marks import sentence, space + +import random + +# Define width and height of the output +width = 75 +height = 75 + +# As we draw multiple sinoids we will collect +# them in a list of layers +layers = [] + +# Loop through an amplitude of -50 to 50 in steps of 10 +for amplitude in range(-50, 50, 10): + # Set the pattern with the changing amplitude + pattern = sinus_vertical(period=40, amplitude=amplitude) + # We use a sentence to draw the text + mark = sentence('convivialité ') + # Define a blank character + blank = space() + + # Make the canvas + lines = make_lines(width, height) + # Draw the sinoid, but add it to the list + result = visit(lines, pattern, mark, blank) + # Add it the result to the list of layers + layers.append(result) + +# Merge the layers into one layer again +merged = merge(width, height, blank(), layers) + +# Print the result +print_lines(merged) diff --git a/ascii_writer/sinus-text.py b/ascii_writer/sinus-text.py new file mode 100644 index 0000000..cb3479d --- /dev/null +++ b/ascii_writer/sinus-text.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 + +from asciiWriter.patterns import sinus_vertical +from asciiWriter.utils import make_lines, visit, print_lines +from asciiWriter.marks import sentence, space, single + +# Define width and height of the output +width = 75 +height = 75 + +# Set the pattern we use to draw, in this case a +# sinoid, with period of 40 lines, and an amplitude +# of 30 characters. Slightly less than half our canvas width +pattern = sinus_vertical(period=40, amplitude=30) +# We use a sentence to draw the text +mark = sentence('tools shape pratices shape tools shape pratices ') +# Define a blank character +blank = single(' ') + +# Make the canvas +lines = make_lines(width, height) + +# Draw the sinoid +result = visit(lines, pattern, mark, blank) + +# Output the result +print_lines(result) diff --git a/ascii_writer/sinus.py b/ascii_writer/sinus.py new file mode 100644 index 0000000..df5f548 --- /dev/null +++ b/ascii_writer/sinus.py @@ -0,0 +1,51 @@ +from asciiWriter.patterns import sinus_vertical +from asciiWriter.utils import make_lines, visit, print_lines, merge +from asciiWriter.marks import sentence, space + +# Define width and height of the output +width = 70 +height = 25 + +# As we draw multiple sinoids we will collect +# them in a list of layers +layers = [] + +# Loop through a range of X, X in steps of X +# (the amount of loops is the amount of layers) +for x in range(-50, 50, 5): + # Set the pattern with the changing offset + pattern = sinus_vertical(period=10, amplitude=40, offset=x) + # We use a sentence to draw the text + mark = sentence('▚▒▓▞') + # Define a blank character + blank = space(' ') + + # Make the canvas + lines = make_lines(width, height) + + # Draw the sinoid, but add it to the list + result = visit(lines, pattern, mark, blank) + # Add it the result to the list of layers + layers.append(result) + + + +# Merge the layers into one layer again +merged = merge(width, height, blank(), layers) + +# Print the result +print_lines(merged) + +# Write the result in txt file +# with open("sinus.txt", 'w') as newFile: +# for els in merged: +# for el in els: +# # print(el) +# # write each item on a new line +# newFile.write(el) +# print('Done') + + + + + diff --git a/font_pixellizer/Decor-Regular.ttf b/font_pixellizer/Decor-Regular.ttf new file mode 100644 index 0000000..01bb5c9 Binary files /dev/null and b/font_pixellizer/Decor-Regular.ttf differ diff --git a/font_pixellizer/IBMPlexMono-Regular.ttf b/font_pixellizer/IBMPlexMono-Regular.ttf new file mode 100644 index 0000000..601ae94 Binary files /dev/null and b/font_pixellizer/IBMPlexMono-Regular.ttf differ diff --git a/font_pixellizer/README.md b/font_pixellizer/README.md new file mode 100644 index 0000000..fae3959 --- /dev/null +++ b/font_pixellizer/README.md @@ -0,0 +1,160 @@ +# Font Pixelizer + +Ensemble d'outils Python pour transformer n'importe quelle typographie en version pixelisée avec paramètres. + +## Fonctionnalités + +- **Pixelisation complète** : Convertit une fonte vectorielle en version pixelisée +- **Contrôle de la taille des pixels** : De très détaillé à très pixellisé +- **Modification des proportions** : Hauteur d'x et largeur ajustables +- **Support monospace** : Option pour forcer un espacement fixe +- **Formes de pixels personnalisables** : Carré, rond, losange + +## Dépendances + +Ce site peut aider à l'installation: https://avoidsoftware.sarahgarcin.com/dependances.html + +### Installer python fontforge +FontForge est un éditeur de polices de caractères libre et open source. En plus d’intégrer Python, FontForge installe généralement un module Python accessible à l’exécutable Python du système. FontForge est disponible dans son ensemble selon les termes de la GNU GPL version 3 ou toute version ultérieure. Cette extension permet la manipulation de typographies. + +#### sur macOS + +Nécessite Homebrew + +- Ouvrir le terminal +-Lancer la commande ```brew install fontforge``` +-Tester si Fontforge est bien exécutable par Python : ```python3 import fontforge, psMat``` +-Si la commande ne fonctionne pas (Mac est un peu capricieux quand il s’agit de Fontforge), il faut déplacer le module Fontforge vers les dépendances de Python : +```sudo cp $(find $(brew --prefix fontforge)/. -name "fontforge.so") $(python3 -c "import site;print(site.getsitepackages()[0]);")``` +```sudo cp $(find $(brew --prefix fontforge)/. -name "psMat.so") $(python3 -c "import site;print(site.getsitepackages()[0]);")``` + +#### sur Linux +- Ouvrir le Terminal en utilisateur root +- Mettre à jour le gestionnaire de paquets : +```sudo apt update``` +```sudo apt upgrade``` +- Installer Python Fontforge +```python3-fontforge``` + + +### Installer les autres dépendances +```bash +pip install fontforge svgpathtools lxml --break-system-packages +``` + +## Utilisation + +### Script simple (une fonte) + +```bash +python3 font_pixelizer.py votre_fonte.ttf +``` + +#### Paramètres modifiables dans le script : + +```python +# Taille des pixels +pixel_size = 80 # Plus grand = moins de détails, effet plus rétro + +# Échelles de déformation +x_height_scale = 1.0 # 0.5 = écrasé, 2.0 = étiré verticalement +width_scale = 1.0 # 0.5 = condensé, 2.0 = étendu horizontalement + +# Monospace +monospace = False # True pour forcer espacement fixe +monospace_width = None # Largeur fixe (None = auto-calculé) + +# Style des pixels +pixel_shape = "square" # "square", "round", "diamond" +pixel_gap = 0 # Espace entre pixels (0 = collés) +round_corners = 0 # Rayon arrondi des coins (0 = coins droits) + +# Export +new_family_name = "Pixel" +font_weight = "Regular" +export_formats = ["ttf"] # ["ttf", "otf", "woff", "woff2"] +``` + +## Exemples de configuration + +### Fonte rétro 8-bit classique +```python +pixel_size = 120 +x_height_scale = 1.0 +width_scale = 1.0 +pixel_shape = "square" +pixel_gap = 0 +monospace = True +``` + +### Fonte pixel moderne arrondie +```python +pixel_size = 60 +x_height_scale = 1.0 +width_scale = 1.0 +pixel_shape = "square" +pixel_gap = 2 +round_corners = 8 +monospace = False +``` + +### Fonte condensée haute résolution +```python +pixel_size = 40 +x_height_scale = 1.2 +width_scale = 0.8 +pixel_shape = "square" +monospace = False +``` + +### Fonte rétro avec pixels espacés +```python +pixel_size = 100 +x_height_scale = 1.0 +width_scale = 1.0 +pixel_shape = "round" +pixel_gap = 10 +monospace = True +``` + +## 🎨 Styles de pixels disponibles + +### Square (carré) +```python +pixel_shape = "square" +round_corners = 0 # Coins droits +``` + +### Square arrondi +```python +pixel_shape = "square" +round_corners = 15 # Coins arrondis +``` + +### Round (cercle) +```python +pixel_shape = "round" +``` + +### Diamond (losange) +```python +pixel_shape = "diamond" +``` + +## Comment ça marche ? + +1. **Extraction** : Le script extrait tous les glyphes de la fonte source en SVG +2. **Analyse** : Chaque glyphe est analysé pour déterminer sa forme +3. **Grille** : Une grille de pixels est calculée selon `pixel_size` +4. **Raycasting** : Pour chaque position de pixel, on teste si elle est dans le contour original +5. **Génération** : Les pixels sont créés sous forme de SVG +6. **Reconstruction** : Une nouvelle fonte est assemblée avec tous les glyphes pixelisés +7. **Export** : La fonte est exportée au format souhaité + + + +## Licence +GNU GPL + +--- + diff --git a/font_pixellizer/font_pixelizer.py b/font_pixellizer/font_pixelizer.py new file mode 100644 index 0000000..a1a93f2 --- /dev/null +++ b/font_pixellizer/font_pixelizer.py @@ -0,0 +1,505 @@ +#!/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")