« Image vers ASCII vers SVG » : différence entre les versions
Aucun résumé des modifications |
|||
| (16 versions intermédiaires par le même utilisateur non affichées) | |||
| Ligne 1 : | Ligne 1 : | ||
= 🖼️ Conversion d’image en ASCII puis en SVG = | |||
3 méthodes pour convertir une image (<code>.png</code>) en une représentation '''ASCII''', puis en '''SVG'''. L’objectif est de produire une version vectorielle stylisée de l’image, utile pour des rendus légers ou artistiques. | |||
----- | ----- | ||
Voir : | |||
*[[Ascii| Ascii]] | |||
*[[SVG| SVG]] | |||
== 🥇 Solution 1 : img2txt + aasvg == | |||
=== 🧩 Prérequis === | |||
== 🧩 Prérequis == | |||
* <code>img2txt</code> (du paquet <code>caca-utils</code>) | * <code>img2txt</code> (du paquet <code>caca-utils</code>) | ||
* <code>aasvg</code> | * <code>aasvg</code> | ||
=== Étapes === | === Étapes === | ||
| Ligne 52 : | Ligne 41 : | ||
== 🥈 Solution 2 : ImageMagick + chafa + a2s == | == 🥈 Solution 2 : ImageMagick + chafa + a2s == | ||
=== 🧩 Prérequis === | |||
* <code>ImageMagick</code> (<code>convert</code>) | |||
* <code>chafa</code> | |||
* <code>a2s</code> | |||
* <code>sed</code> | |||
=== Étapes === | === Étapes === | ||
| Ligne 82 : | Ligne 77 : | ||
On supprime les lignes parasites :</p> | On supprime les lignes parasites :</p> | ||
<syntaxhighlight lang="bash">sed '/<g id="lines"/,/<\/g>/d' picture.svg > picture-clean.svg</syntaxhighlight></li></ol> | <syntaxhighlight lang="bash">sed '/<g id="lines"/,/<\/g>/d' picture.svg > picture-clean.svg</syntaxhighlight></li></ol> | ||
== 📁 Résultat == | == 📁 Résultat == | ||
| Ligne 92 : | Ligne 84 : | ||
* <code>picture-clean.svg</code> : SVG nettoyé (solution 2) | * <code>picture-clean.svg</code> : SVG nettoyé (solution 2) | ||
== 🥉 Solution 3 : img2txt + scrypt Python 3 == | |||
Avec couleurs Préservées | |||
=== 🧩 Prérequis === | |||
* <code>img2txt</code> (du paquet <code>caca-utils</code>) | |||
* <code>python</code> | |||
* <code>sed</code> | |||
=== On génère une version ASCII avec couleurs : === | |||
<syntaxhighlight lang="bash"> | |||
img2txt -W 100 -x 1 -y 2 picture.png > picture.txt | |||
</syntaxhighlight> | |||
=== Exécution du script Python === | |||
<syntaxhighlight lang="bash"> | |||
python3 ansi2svg_pure.py picture.txt picture.svg \ | |||
--font "DejaVu Sans Mono" \ | |||
--font-size 12 \ | |||
--line-height 1.2 \ | |||
--bg none \ | |||
--char-width-ratio 0.6 \ | |||
--margin 10 | |||
</syntaxhighlight> | |||
==== Script python ==== | |||
<syntaxhighlight lang="python" line> | |||
#!/usr/bin/env python3 | |||
# -*- coding: utf-8 -*- | |||
""" | |||
Convertit un ASCII art coloré (ANSI) en SVG coloré, sans dépendances externes. | |||
Usage: | |||
python3 ansi2svg_pure.py input.txt output.svg \ | |||
--font "DejaVu Sans Mono" --font-size 12 \ | |||
--line-height 1.2 --bg none --char-width-ratio 0.6 --margin 10 | |||
Conçu pour des sorties générées par `img2txt --ansi ...` (libcaca). | |||
Gère : SGR 0/1/3/4/22/23/24, 30-37/90-97 (fg), 40-47/100-107 (bg), | |||
38;5;n / 48;5;n (256c), 38;2;r;g;b / 48;2;r;g;b (truecolor). | |||
""" | |||
import argparse | |||
import html | |||
import re | |||
import sys | |||
from typing import Optional, Tuple, List | |||
CSI_SGR_RE = re.compile(r"\x1b\[((?:\d{1,3};?)*?)m") | |||
# Couleurs ANSI 16 de base (xterm) | |||
ANSI_16 = { | |||
# normal 30-37 / 40-47 | |||
30: (0, 0, 0), # black | |||
31: (205, 49, 49), # red | |||
32: (13, 188, 121), # green | |||
33: (229, 229, 16), # yellow | |||
34: (36, 114, 200), # blue | |||
35: (188, 63, 188), # magenta | |||
36: (17, 168, 205), # cyan | |||
37: (204, 204, 204), # white (light gray) | |||
# bright 90-97 / 100-107 | |||
90: (102, 102, 102), # bright black (gray) | |||
91: (241, 76, 76), # bright red | |||
92: (35, 209, 139), # bright green | |||
93: (245, 245, 67), # bright yellow | |||
94: (59, 142, 234), # bright blue | |||
95: (214, 112, 214), # bright magenta | |||
96: (41, 184, 219), # bright cyan | |||
97: (229, 229, 229), # bright white | |||
} | |||
def xterm256_to_rgb(n: int) -> Tuple[int, int, int]: | |||
"""Mappe un code couleur xterm 256 (0-255) vers (r,g,b).""" | |||
if n < 0: n = 0 | |||
if n > 255: n = 255 | |||
if n < 16: | |||
# 0-7 standard + 8-15 bright | |||
base = [ | |||
(0,0,0),(205,0,0),(0,205,0),(205,205,0), | |||
(0,0,238),(205,0,205),(0,205,205),(229,229,229), | |||
(127,127,127),(255,0,0),(0,255,0),(255,255,0), | |||
(92,92,255),(255,0,255),(0,255,255),(255,255,255), | |||
] | |||
return base[n] | |||
if 16 <= n <= 231: | |||
n -= 16 | |||
r = n // 36 | |||
g = (n % 36) // 6 | |||
b = n % 6 | |||
def level(v): | |||
return 55 + v * 40 if v > 0 else 0 | |||
return (level(r), level(g), level(b)) | |||
# 232..255 grayscale | |||
gray = 8 + (n - 232) * 10 | |||
return (gray, gray, gray) | |||
def rgb_to_hex(rgb: Optional[Tuple[int,int,int]]) -> str: | |||
if rgb is None: | |||
return "currentColor" # fallback | |||
r,g,b = rgb | |||
return f"#{r:02x}{g:02x}{b:02x}" | |||
class Style: | |||
__slots__ = ("fg", "bg", "bold", "italic", "underline") | |||
def __init__(self): | |||
self.fg: Optional[Tuple[int,int,int]] = (229,229,229) # défaut clair | |||
self.bg: Optional[Tuple[int,int,int]] = None # transparent | |||
self.bold = False | |||
self.italic = False | |||
self.underline = False | |||
def clone(self) -> 'Style': | |||
s = Style() | |||
s.fg = None if self.fg is None else tuple(self.fg) | |||
s.bg = None if self.bg is None else tuple(self.bg) | |||
s.bold = self.bold | |||
s.italic = self.italic | |||
s.underline = self.underline | |||
return s | |||
def apply_sgr(self, params: List[int]): | |||
if not params: | |||
params = [0] | |||
i = 0 | |||
while i < len(params): | |||
p = params[i] | |||
if p == 0: # reset | |||
self.__init__() | |||
elif p == 1: | |||
self.bold = True | |||
elif p == 3: | |||
self.italic = True | |||
elif p == 4: | |||
self.underline = True | |||
elif p == 22: | |||
self.bold = False | |||
elif p == 23: | |||
self.italic = False | |||
elif p == 24: | |||
self.underline = False | |||
elif p == 39: | |||
self.fg = (229,229,229) | |||
elif p == 49: | |||
self.bg = None | |||
elif (30 <= p <= 37) or (90 <= p <= 97): | |||
self.fg = ANSI_16.get(p) | |||
elif (40 <= p <= 47) or (100 <= p <= 107): | |||
self.bg = ANSI_16.get(p - 10) # bg code -> fg equivalent (30..) | |||
elif p in (38, 48): | |||
# 38 -> set fg ; 48 -> set bg | |||
is_fg = (p == 38) | |||
# expecting 5;n or 2;r;g;b | |||
if i+1 < len(params): | |||
mode = params[i+1] | |||
if mode == 5 and i+2 < len(params): | |||
n = params[i+2] | |||
rgb = xterm256_to_rgb(n) | |||
if is_fg: self.fg = rgb | |||
else: self.bg = rgb | |||
i += 2 | |||
elif mode == 2 and i+4 < len(params): | |||
r, g, b = params[i+2], params[i+3], params[i+4] | |||
rgb = (max(0,min(255,r)), max(0,min(255,g)), max(0,min(255,b))) | |||
if is_fg: self.fg = rgb | |||
else: self.bg = rgb | |||
i += 4 | |||
# autres codes ignorés | |||
i += 1 | |||
def parse_ansi_to_runs(line: str) -> List[Tuple[str, Style]]: | |||
""" | |||
Transforme une ligne ANSI en une liste de (texte_sans_ansi, style). | |||
Chaque élément est un run de même style. | |||
""" | |||
runs: List[Tuple[str, Style]] = [] | |||
style = Style() | |||
pos = 0 | |||
for m in CSI_SGR_RE.finditer(line): | |||
text_chunk = line[pos:m.start()] | |||
if text_chunk: | |||
# Ajouter le texte courant avec le style actuel | |||
runs.append((text_chunk, style.clone())) | |||
sgr_params = [int(x) for x in m.group(1).split(";") if x != ""] | |||
style.apply_sgr(sgr_params) | |||
pos = m.end() | |||
# Reste de la ligne | |||
tail = line[pos:] | |||
if tail: | |||
runs.append((tail, style.clone())) | |||
return runs | |||
def strip_ansi_visible_len(s: str) -> int: | |||
"""Longueur visible (sans séquences ANSI).""" | |||
return len(CSI_SGR_RE.sub("", s)) | |||
def main(): | |||
ap = argparse.ArgumentParser(description="ANSI colored text -> Colored SVG (no deps)") | |||
ap.add_argument("input", help="Fichier texte ANSI (ex: picture.txt)") | |||
ap.add_argument("output", help="Fichier SVG de sortie (ex: picture.svg)") | |||
ap.add_argument("--font", default="DejaVu Sans Mono", help="Police monospace (déf: DejaVu Sans Mono)") | |||
ap.add_argument("--font-size", type=float, default=12.0, help="Taille de police en px (déf: 12)") | |||
ap.add_argument("--line-height", type=float, default=1.2, help="Multiplicateur de hauteur de ligne (déf: 1.2)") | |||
ap.add_argument("--char-width-ratio", type=float, default=0.6, help="Largeur/FontSize pour monospace (déf: 0.6)") | |||
ap.add_argument("--margin", type=float, default=10.0, help="Marge en px autour (déf: 10)") | |||
ap.add_argument("--bg", default="none", help="Arrière-plan: 'none' (transparent) ou #rrggbb (déf: none)") | |||
args = ap.parse_args() | |||
try: | |||
with open(args.input, "r", encoding="utf-8", errors="replace") as f: | |||
raw_lines = f.read().splitlines() | |||
except Exception as e: | |||
print(f"Erreur: impossible de lire {args.input}: {e}", file=sys.stderr) | |||
sys.exit(1) | |||
# Mesures de grille | |||
fs = args.font_size | |||
lh = args.line_height * fs | |||
cw = args.char_width_ratio * fs | |||
margin = args.margin | |||
rows = len(raw_lines) | |||
cols = max((strip_ansi_visible_len(L) for L in raw_lines), default=0) | |||
width = 2*margin + cols * cw | |||
height = 2*margin + rows * lh | |||
# Prépare le SVG | |||
out = [] | |||
out.append('<?xml version="1.0" encoding="UTF-8"?>') | |||
out.append(f'<svg xmlns="http://www.w3.org/2000/svg" version="1.1" ' | |||
f'width="{width:.2f}" height="{height:.2f}" ' | |||
f'viewBox="0 0 {width:.2f} {height:.2f}">') | |||
# Fond | |||
if args.bg and args.bg.lower() != "none": | |||
out.append(f'<rect x="0" y="0" width="{width:.2f}" height="{height:.2f}" fill="{html.escape(args.bg)}"/>') | |||
# Style texte global | |||
out.append('<g>') | |||
out.append(f'<g font-family="{html.escape(args.font)}" font-size="{fs:.2f}px" ' | |||
f'xml:space="preserve">') | |||
# Dessine ligne par ligne | |||
# Pour placer le texte correctement: y = margin + (row+1)*lh - (lh - fs)/2 | |||
# (approximation: baseline ~ fs, visuellement suffisant) | |||
for r, line in enumerate(raw_lines): | |||
runs = parse_ansi_to_runs(line) | |||
x_cursor = 0 # colonne visible | |||
y = margin + (r + 1) * lh - (lh - fs) * 0.5 | |||
# On reconstruit la ligne en runs, en tenant compte des espaces | |||
for text, style in runs: | |||
if not text: | |||
continue | |||
# Avance la colonne pour les espaces "invisibles" avant le run ? | |||
# Ici, on place le run à la colonne courante et on dessine exactement son contenu. | |||
# Calcul du x | |||
x = margin + x_cursor * cw | |||
# Échappe le texte et remplace les tabulations par espaces (rare dans img2txt) | |||
safe_text = text.replace("\t", " ") | |||
safe_text = html.escape(safe_text) | |||
# Calcul des couleurs / décorations | |||
fill = rgb_to_hex(style.fg) | |||
# Background: on peut dessiner un rect derrière le run si besoin | |||
if style.bg is not None and safe_text: | |||
run_cols = len(text) | |||
rx = x | |||
ry = margin + r * lh | |||
rw = run_cols * cw | |||
rh = lh | |||
out.append(f'<rect x="{rx:.2f}" y="{ry:.2f}" width="{rw:.2f}" height="{rh:.2f}" ' | |||
f'fill="{rgb_to_hex(style.bg)}"/>') | |||
deco = [] | |||
if style.bold: | |||
deco.append("font-weight:bold") | |||
if style.italic: | |||
deco.append("font-style:italic") | |||
if style.underline: | |||
deco.append("text-decoration:underline") | |||
style_attr = f' fill="{fill}"' | |||
if deco: | |||
style_attr += f' style="{";".join(deco)}"' | |||
# Texte du run à la position calculée | |||
out.append(f'<text x="{x:.2f}" y="{y:.2f}"{style_attr}>{safe_text}</text>') | |||
# Avancer le curseur de colonnes visibles | |||
x_cursor += len(text) | |||
# S'il reste du vide en fin de ligne, rien à dessiner (fond transparent) | |||
out.append('</g>') | |||
out.append('</g>') | |||
out.append('</svg>') | |||
try: | |||
with open(args.output, "w", encoding="utf-8") as f: | |||
f.write("\n".join(out)) | |||
except Exception as e: | |||
print(f"Erreur: impossible d'écrire {args.output}: {e}", file=sys.stderr) | |||
sys.exit(1) | |||
print(f"[OK] SVG coloré généré : {args.output}") | |||
print(f" Dimensions: {width:.0f} x {height:.0f}px (cols={cols}, rows={rows}, fs={fs}px, cw≈{cw:.2f}, lh≈{lh:.2f})") | |||
if __name__ == "__main__": | |||
main() | |||
</syntaxhighlight> | |||
=== Suppresion du fond === | |||
<syntaxhighlight lang="bash"> | |||
sed -i '/<rect/d' picture.svg | |||
</syntaxhighlight> | |||
----- | ----- | ||
| Ligne 99 : | Ligne 404 : | ||
* La '''solution 1''' est plus rapide et produit un SVG avec des glyphes colorés si on garde les séquences ANSI. | * La '''solution 1''' est plus rapide et produit un SVG avec des glyphes colorés si on garde les séquences ANSI. | ||
* La '''solution 2''' est plus adaptée pour des rendus en noir et blanc ou des effets de contours. | * La '''solution 2''' est plus adaptée pour des rendus en noir et blanc ou des effets de contours. | ||
* La '''solution 3''' permet une conversion avec couleurs préservées, en interprétant les séquences ANSI (y compris les couleurs 256 et truecolor) et en les traduisant en éléments SVG. Elle gère également les styles typographiques comme le gras, italique et souligné, ainsi que les fonds colorés. Le rendu est plus fidèle à l’ASCII d’origine, mais nécessite Python 3 et un script dédié sans dépendances externes. | |||
= Exemple d'intégration de l'image SVG dans un fichier HTML = | |||
<syntaxhighlight lang="html" line> | |||
<!DOCTYPE html> | |||
<html lang="fr"> | |||
<head> | |||
<meta charset="utf-8"> | |||
<meta name="viewport" content="width=device-width initial-scale=1" /> | |||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |||
<title>Test</title> | |||
<meta name="description" content="Du web, du code."> | |||
<style> | |||
div#background{ | |||
width: 100%; | |||
height: 100%; | |||
position: fixed; | |||
background-image: url("picture.svg"); | |||
background-position: center center; | |||
background-repeat: no-repeat; | |||
background-size: contain; | |||
color:#FFF; | |||
} | |||
</style> | |||
</head> | |||
<body> | |||
<div id="background"></div> | |||
</body> | |||
</html> | |||
</syntaxhighlight> | |||
[[Catégorie:Tools]] | [[Catégorie:Tools]] | ||
[[Catégorie:Dev]] | [[Catégorie:Dev]] | ||
[[Catégorie:Geek]] | [[Catégorie:Geek]] | ||
[[Catégorie: Terminal Tools]] | |||