@thlg057/mo5-rag-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,478 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Convertisseur PNG vers sprite Thomson MO5
4
+
5
+ Convertit une image PNG en tableaux C pour le Thomson MO5
6
+ Format: 1 octet = 8 pixels de 1 bit (forme/fond)
7
+ Génère 2 tableaux: FORME (bitmap) et COULEUR (attributs par groupe de 8 pixels)
8
+
9
+ Usage:
10
+ python png_to_mo5_v2.py image.png [--name SPRITE_NAME] [--bg-color 0-15] [--transparent]
11
+ """
12
+
13
+ import argparse
14
+ import sys
15
+ import os
16
+ from pathlib import Path
17
+ try:
18
+ from PIL import Image
19
+ except ImportError:
20
+ print("Erreur: PIL (Pillow) n'est pas installé.")
21
+ print("Installez-le avec: pip install Pillow")
22
+ sys.exit(1)
23
+
24
+ # Palette MO5 complète (16 couleurs, RGB approximatif)
25
+ MO5_PALETTE = {
26
+ 0: {'R': 0, 'G': 0, 'B': 0, 'Name': 'C_BLACK'},
27
+ 1: {'R': 255, 'G': 0, 'B': 0, 'Name': 'C_RED'},
28
+ 2: {'R': 0, 'G': 255, 'B': 0, 'Name': 'C_GREEN'},
29
+ 3: {'R': 255, 'G': 255, 'B': 0, 'Name': 'C_YELLOW'},
30
+ 4: {'R': 0, 'G': 0, 'B': 255, 'Name': 'C_BLUE'},
31
+ 5: {'R': 255, 'G': 0, 'B': 255, 'Name': 'C_MAGENTA'},
32
+ 6: {'R': 0, 'G': 255, 'B': 255, 'Name': 'C_CYAN'},
33
+ 7: {'R': 255, 'G': 255, 'B': 255, 'Name': 'C_WHITE'},
34
+ 8: {'R': 128, 'G': 128, 'B': 128, 'Name': 'C_GRAY'},
35
+ 9: {'R': 255, 'G': 128, 'B': 128, 'Name': 'C_LIGHT_RED'},
36
+ 10: {'R': 128, 'G': 255, 'B': 128, 'Name': 'C_LIGHT_GREEN'},
37
+ 11: {'R': 255, 'G': 255, 'B': 128, 'Name': 'C_LIGHT_YELLOW'},
38
+ 12: {'R': 128, 'G': 128, 'B': 255, 'Name': 'C_LIGHT_BLUE'},
39
+ 13: {'R': 255, 'G': 128, 'B': 255, 'Name': 'C_PURPLE'},
40
+ 14: {'R': 128, 'G': 255, 'B': 255, 'Name': 'C_LIGHT_CYAN'},
41
+ 15: {'R': 255, 'G': 128, 'B': 0, 'Name': 'C_ORANGE'}
42
+ }
43
+
44
+
45
+ def get_color_distance(r1, g1, b1, r2, g2, b2):
46
+ """Calcule la distance euclidienne entre deux couleurs RGB"""
47
+ return (r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2
48
+
49
+
50
+ def get_closest_mo5_color(r, g, b, a):
51
+ """Trouve la couleur MO5 la plus proche d'une couleur RGBA donnée"""
52
+ # Si transparent, retourner None
53
+ if a < 128:
54
+ return None
55
+
56
+ min_distance = float('inf')
57
+ closest_color = 0
58
+
59
+ for color_idx in range(16):
60
+ palette = MO5_PALETTE[color_idx]
61
+ distance = get_color_distance(r, g, b, palette['R'], palette['G'], palette['B'])
62
+
63
+ if distance < min_distance:
64
+ min_distance = distance
65
+ closest_color = color_idx
66
+
67
+ return closest_color
68
+
69
+
70
+ def get_dominant_colors(pixels, default_bg):
71
+ """Détermine les 2 couleurs dominantes d'un groupe de 8 pixels"""
72
+ # Compter les occurrences de chaque couleur
73
+ color_count = {}
74
+
75
+ for pixel in pixels:
76
+ r, g, b, a = pixel
77
+ if a < 128:
78
+ continue
79
+
80
+ color = get_closest_mo5_color(r, g, b, a)
81
+ if color is not None:
82
+ color_count[color] = color_count.get(color, 0) + 1
83
+
84
+ # Si pas de pixels visibles, retourner couleur par défaut
85
+ if not color_count:
86
+ return {
87
+ 'Background': default_bg,
88
+ 'Foreground': default_bg,
89
+ 'IsSingleColor': True
90
+ }
91
+
92
+ # Si une seule couleur
93
+ if len(color_count) == 1:
94
+ color = list(color_count.keys())[0]
95
+ return {
96
+ 'Background': default_bg,
97
+ 'Foreground': color,
98
+ 'IsSingleColor': True
99
+ }
100
+
101
+ # Le fond est toujours default_bg — chercher la couleur fg la plus fréquente
102
+ # parmi les couleurs non-fond
103
+ non_bg = {c: n for c, n in color_count.items() if c != default_bg}
104
+
105
+ if not non_bg:
106
+ # Toutes les couleurs du bloc sont le fond
107
+ return {
108
+ 'Background': default_bg,
109
+ 'Foreground': default_bg,
110
+ 'IsSingleColor': True
111
+ }
112
+
113
+ fg = max(non_bg, key=lambda c: non_bg[c])
114
+
115
+ return {
116
+ 'Background': default_bg,
117
+ 'Foreground': fg,
118
+ 'IsSingleColor': False
119
+ }
120
+
121
+ def get_dominant_colors_transparent(pixels):
122
+ """Version spécifique : force le fond à 0 pour mo5_sprite_bg"""
123
+ color_count = {}
124
+ for pixel in pixels:
125
+ r, g, b, a = pixel
126
+ if a < 128: continue
127
+ color = get_closest_mo5_color(r, g, b, a)
128
+ if color is not None:
129
+ color_count[color] = color_count.get(color, 0) + 1
130
+ if not color_count:
131
+ return {'Background': 0, 'Foreground': 0, 'IsSingleColor': True}
132
+ sorted_colors = sorted(color_count.items(), key=lambda x: x[1], reverse=True)
133
+ fg = sorted_colors[0][0]
134
+ return {'Background': 0, 'Foreground': fg, 'IsSingleColor': True}
135
+
136
+ def convert_png_to_mo5_sprite(image_path, sprite_name=None, default_bg=0, quiet=False, transparent=False):
137
+ """Convertit une image PNG en sprite MO5"""
138
+
139
+ if not os.path.exists(image_path):
140
+ print(f"[ERREUR] Le fichier '{image_path}' n'existe pas.")
141
+ return None
142
+
143
+ if not quiet:
144
+ print(f"[INFO] Chargement de l'image: {image_path}")
145
+
146
+ try:
147
+ img = Image.open(image_path)
148
+ # Convertir en RGBA si nécessaire
149
+ if img.mode != 'RGBA':
150
+ img = img.convert('RGBA')
151
+ except Exception as e:
152
+ print(f"[ERREUR] Erreur lors du chargement: {e}")
153
+ return None
154
+
155
+ width, height = img.size
156
+ if not quiet:
157
+ print(f" Dimensions: {width}x{height} pixels")
158
+
159
+ # Vérifier que la largeur est multiple de 8
160
+ original_width = width
161
+ if width % 8 != 0:
162
+ if not quiet:
163
+ print(f"[ATTENTION] Largeur ({width}) non multiple de 8.")
164
+ width = (width // 8) * 8
165
+ if not quiet:
166
+ print(f" Ajustée à {width} pixels")
167
+
168
+ bytes_per_line = width // 8
169
+
170
+ # Gérer le nom du sprite et le chemin de sortie
171
+ output_path = None
172
+ if sprite_name:
173
+ # Si un nom est fourni, il peut contenir un chemin
174
+ name_path = Path(sprite_name)
175
+ output_path = name_path # Conserver le chemin complet pour la sortie
176
+ sprite_name = name_path.stem # Extraire juste le nom pour les variables C
177
+ else:
178
+ sprite_name = Path(image_path).stem
179
+
180
+ # Remplacer les caractères non alphanumériques par des underscores (pour les noms de variables C)
181
+ sprite_name_clean = ''.join(c if c.isalnum() or c == '_' else '_' for c in sprite_name)
182
+
183
+ if not quiet:
184
+ print("[INFO] Analyse de l'image...")
185
+
186
+ # Tableaux pour stocker les données
187
+ form_data = []
188
+ color_data = []
189
+ color_stats = {}
190
+ total_blocks = 0
191
+ multi_color_blocks = 0
192
+
193
+ # Charger tous les pixels
194
+ pixels = img.load()
195
+
196
+ # Traiter chaque ligne
197
+ for y in range(height):
198
+ line_form_bytes = []
199
+ line_color_bytes = []
200
+ visual = ""
201
+
202
+ # Traiter les pixels 8 par 8
203
+ for x in range(0, width, 8):
204
+ # Récupérer les 8 pixels
205
+ pixel_group = []
206
+ for i in range(8):
207
+ if x + i < width:
208
+ pixel_group.append(pixels[x + i, y])
209
+ else:
210
+ pixel_group.append((0, 0, 0, 0))
211
+
212
+ # Déterminer les 2 couleurs dominantes
213
+ if transparent:
214
+ colors = get_dominant_colors_transparent(pixel_group)
215
+ else:
216
+ colors = get_dominant_colors(pixel_group, default_bg)
217
+
218
+ bg = colors['Background']
219
+ fg = colors['Foreground']
220
+
221
+ total_blocks += 1
222
+ if not colors['IsSingleColor']:
223
+ multi_color_blocks += 1
224
+
225
+ # Statistiques
226
+ color_key = f"{bg}-{fg}"
227
+ color_stats[color_key] = color_stats.get(color_key, 0) + 1
228
+
229
+ # Créer l'octet de COULEUR (FFFFBBBB: Forme en haut, Fond en bas)
230
+ color_byte = (bg & 0x0F) | ((fg & 0x0F) << 4)
231
+ line_color_bytes.append(f"0x{color_byte:02X}")
232
+
233
+ # Créer l'octet de FORME (bitmap: 1=forme, 0=fond)
234
+ form_byte = 0
235
+ for i in range(8):
236
+ if x + i < width:
237
+ pixel = pixel_group[i]
238
+ r, g, b, a = pixel
239
+
240
+ if a < 128:
241
+ # Transparent = fond (bit 0)
242
+ pixel_bit = 0
243
+ visual += "-"
244
+ else:
245
+ # Déterminer si c'est la couleur de forme ou de fond
246
+ pixel_color = get_closest_mo5_color(r, g, b, a)
247
+
248
+ if pixel_color is not None:
249
+ if fg == bg:
250
+ # Bloc monochrome fond : tous les pixels = fond
251
+ pixel_bit = 0
252
+ visual += "-"
253
+ else:
254
+ # Si la couleur est plus proche de fg que de bg
255
+ dist_fg = get_color_distance(r, g, b,
256
+ MO5_PALETTE[fg]['R'],
257
+ MO5_PALETTE[fg]['G'],
258
+ MO5_PALETTE[fg]['B'])
259
+ dist_bg = get_color_distance(r, g, b,
260
+ MO5_PALETTE[bg]['R'],
261
+ MO5_PALETTE[bg]['G'],
262
+ MO5_PALETTE[bg]['B'])
263
+
264
+ if dist_fg < dist_bg:
265
+ pixel_bit = 1 # Forme
266
+ visual += "█"
267
+ else:
268
+ pixel_bit = 0 # Fond
269
+ visual += "-"
270
+ else:
271
+ pixel_bit = 0
272
+ visual += "-"
273
+
274
+ # Positionner le bit (MSB = pixel de gauche)
275
+ shift = 7 - i
276
+ form_byte |= (pixel_bit << shift)
277
+ else:
278
+ visual += "-"
279
+
280
+ line_form_bytes.append(f"0x{form_byte:02X}")
281
+
282
+ # Ajouter les lignes
283
+ form_line = " " + ", ".join(line_form_bytes)
284
+ color_line = " " + ", ".join(line_color_bytes)
285
+
286
+ if y < height - 1:
287
+ form_line += ","
288
+ color_line += ","
289
+
290
+ form_line += f" // {y} {visual}"
291
+ color_line += f" // {y}"
292
+
293
+ form_data.append(form_line)
294
+ color_data.append(color_line)
295
+
296
+ # Construire le code C avec include guards
297
+ guard_name = f"SPRITE_{sprite_name_clean.upper()}_H"
298
+
299
+ output = []
300
+ output.append(f"#ifndef {guard_name}")
301
+ output.append(f"#define {guard_name}")
302
+ output.append("")
303
+ output.append("// =============================================")
304
+ output.append(f"// Sprite: {sprite_name_clean}")
305
+ output.append(f"// Source: {os.path.basename(image_path)}")
306
+ output.append(f"// Taille: {width}x{height} pixels ({bytes_per_line} octets x {height} lignes)")
307
+ output.append("// Format: 1 octet = 8 pixels (1 bit/pixel)")
308
+ output.append("// Contrainte: 2 couleurs par groupe de 8 pixels")
309
+ output.append("// =============================================")
310
+ output.append("")
311
+
312
+ # Ajouter les defines pour les dimensions
313
+ output.append(f"#define SPRITE_{sprite_name_clean.upper()}_WIDTH_BYTES {bytes_per_line}")
314
+ output.append(f"#define SPRITE_{sprite_name_clean.upper()}_HEIGHT {height}")
315
+ output.append("")
316
+
317
+ output.append("// Données de FORME (bitmap: 1=forme, 0=fond)")
318
+ output.append(f"unsigned char sprite_{sprite_name_clean}_form[{bytes_per_line * height}] = {{")
319
+ output.extend(form_data)
320
+ output.append("};")
321
+ output.append("")
322
+ output.append("// Données de COULEUR (attributs par groupe de 8 pixels)")
323
+ output.append("// Format: FFFFBBBB (Forme bits 4-7, Fond bits 0-3)")
324
+ output.append(f"unsigned char sprite_{sprite_name_clean}_color[{bytes_per_line * height}] = {{")
325
+ output.extend(color_data)
326
+ output.append("};")
327
+ output.append("")
328
+ output.append(f"// Taille totale: {bytes_per_line * height} octets par tableau")
329
+
330
+ if total_blocks > 0:
331
+ percentage = round(multi_color_blocks * 100.0 / total_blocks, 1)
332
+ output.append(f"// Blocs multi-couleurs: {multi_color_blocks} / {total_blocks} ({percentage}%)")
333
+ output.append("")
334
+
335
+ if color_stats:
336
+ output.append("// Combinaisons de couleurs utilisées:")
337
+ for key in sorted(color_stats.keys()):
338
+ parts = key.split('-')
339
+ bg = int(parts[0])
340
+ fg = int(parts[1])
341
+ count = color_stats[key]
342
+ bg_name = MO5_PALETTE[bg]['Name']
343
+ fg_name = MO5_PALETTE[fg]['Name']
344
+ output.append(f"// Fond={bg_name}, Forme={fg_name} : {count} blocs de 8 pixels")
345
+ output.append("")
346
+
347
+ # Macro d'initialisation MO5_Sprite
348
+ sn = sprite_name_clean
349
+ SN = sprite_name_clean.upper()
350
+ output.append(f"// Macro d'initialisation pour MO5_Sprite (voir mo5_sprite.h)")
351
+ output.append(f"#define SPRITE_{SN}_INIT \\")
352
+ output.append(f" {{ sprite_{sn}_form, sprite_{sn}_color, \\")
353
+ output.append(f" SPRITE_{SN}_WIDTH_BYTES, SPRITE_{SN}_HEIGHT }}")
354
+ output.append("")
355
+ output.append(f"// Utilisation:")
356
+ output.append(f"// MO5_Sprite sprite_{sn} = SPRITE_{SN}_INIT;")
357
+ output.append("")
358
+ output.append(f"#endif // {guard_name}")
359
+
360
+ img.close()
361
+
362
+ return {
363
+ 'Code': '\n'.join(output),
364
+ 'SpriteName': sprite_name_clean,
365
+ 'OutputPath': output_path, # Chemin de sortie si spécifié
366
+ 'Width': width,
367
+ 'Height': height,
368
+ 'BytesPerLine': bytes_per_line,
369
+ 'ColorStats': color_stats,
370
+ 'MultiColorBlocks': multi_color_blocks,
371
+ 'TotalBlocks': total_blocks
372
+ }
373
+
374
+
375
+ def main():
376
+ parser = argparse.ArgumentParser(
377
+ description='Convertisseur PNG vers sprite Thomson MO5',
378
+ formatter_class=argparse.RawDescriptionHelpFormatter,
379
+ epilog="""
380
+ Exemples:
381
+ python png_to_mo5_v2.py mon_sprite.png
382
+ python png_to_mo5_v2.py hero.png --name hero --bg-color 4
383
+ """
384
+ )
385
+
386
+ parser.add_argument('image_path', help='Chemin vers l\'image PNG à convertir')
387
+ parser.add_argument('--name', dest='sprite_name', help='Nom du sprite (optionnel)')
388
+ parser.add_argument('--bg-color', dest='bg_color', type=int, default=0,
389
+ choices=range(16), metavar='0-15',
390
+ help='Couleur de fond par défaut (0-15, défaut: 0=noir)')
391
+ parser.add_argument('--transparent', action='store_true',
392
+ help='Force le fond à 0 pour mo5_sprite_bg')
393
+ parser.add_argument('--quiet', '-q', action='store_true',
394
+ help='Mode silencieux (affiche uniquement le message final)')
395
+
396
+ args = parser.parse_args()
397
+
398
+ if not args.quiet:
399
+ print()
400
+ print("=" * 60)
401
+ print(" Convertisseur PNG -> Sprite Thomson MO5 (Multi-couleurs)")
402
+ print(" Format: 1 octet = 8 pixels (1 bit/pixel)")
403
+ print(" 2 couleurs auto-détectées par groupe de 8 pixels")
404
+ print("=" * 60)
405
+ print()
406
+
407
+ result = convert_png_to_mo5_sprite(args.image_path, args.sprite_name, args.bg_color, args.quiet, args.transparent)
408
+
409
+ if result:
410
+ if not args.quiet:
411
+ # Afficher le résultat
412
+ print("=" * 60)
413
+ print(result['Code'])
414
+ print("=" * 60)
415
+ print()
416
+ print("[OK] Sprite généré avec succès!")
417
+ print()
418
+ print("[INFO] Le sprite utilise 2 tableaux:")
419
+ print(f" - sprite_{result['SpriteName']}_form (bitmap 1 bit/pixel)")
420
+ print(f" - sprite_{result['SpriteName']}_color (attributs couleur)")
421
+ print()
422
+ print("[STATS] Analyse:")
423
+ print(f" Blocs multi-couleurs: {result['MultiColorBlocks']}/{result['TotalBlocks']}")
424
+ if result['TotalBlocks'] > 0:
425
+ percentage = round(result['MultiColorBlocks'] * 100.0 / result['TotalBlocks'], 1)
426
+ print(f" Pourcentage: {percentage}%")
427
+ print()
428
+ print("[INFO] Utilisation:")
429
+ print(f" draw_sprite_multicolor(x, y,")
430
+ print(f" sprite_{result['SpriteName']}_form,")
431
+ print(f" sprite_{result['SpriteName']}_color,")
432
+ print(f" {result['BytesPerLine']}, {result['Height']});")
433
+ print()
434
+
435
+ # Sauvegarder
436
+ if result['OutputPath']:
437
+ # Si --name a été spécifié, utiliser ce chemin
438
+ output_path = result['OutputPath']
439
+ # Si pas d'extension .c ou .h, ajouter .c
440
+ if output_path.suffix not in ['.c', '.h']:
441
+ output_path = output_path.with_suffix('.c')
442
+ # Créer les répertoires parents si nécessaire
443
+ output_path.parent.mkdir(parents=True, exist_ok=True)
444
+ else:
445
+ # Sinon, créer dans le même répertoire que l'image source
446
+ source_path = Path(args.image_path)
447
+ output_path = source_path.parent / (source_path.stem + '_sprite_mc.c')
448
+
449
+ with open(output_path, 'w', encoding='utf-8') as f:
450
+ f.write(result['Code'])
451
+
452
+ if not args.quiet:
453
+ print(f"[OK] Sprite sauvegardé dans: {output_path}")
454
+ print()
455
+
456
+ # Tenter de copier dans le presse-papier (optionnel)
457
+ if not args.quiet:
458
+ try:
459
+ import pyperclip
460
+ pyperclip.copy(result['Code'])
461
+ print("[OK] Code copié dans le presse-papier!")
462
+ except ImportError:
463
+ pass # pyperclip n'est pas installé, ce n'est pas grave
464
+ except Exception:
465
+ pass # Échec de la copie, ce n'est pas grave
466
+
467
+ # Message final (toujours affiché, même en mode quiet)
468
+ print(f"[OK] Fichier généré: {output_path}")
469
+ else:
470
+ print("[ERREUR] Échec de la conversion")
471
+ sys.exit(1)
472
+
473
+ if not args.quiet:
474
+ print()
475
+
476
+
477
+ if __name__ == '__main__':
478
+ main()