Image vers ASCII vers SVG
Apparence
🖼️ Conversion d’image en ASCII puis en SVG
3 méthodes pour convertir une image (.png) 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 :
🥇 Solution 1 : img2txt + aasvg
🧩 Prérequis
img2txt(du paquetcaca-utils)aasvg
Étapes
Conversion en ASCII avec codes ANSI
On utiliseimg2txtpour générer une version ASCII colorée de l’image :img2txt -W 150 -x 1 -y 2 -d fstein -f ansi picture.png | sed -r 's/\x1B\[[0-9;]*[mK]//g' > picture.txt
-W 150: largeur de sortie en caractères-x 1 -y 2: facteur d’étirement horizontal et vertical-d fstein: algorithme de dithering-f ansi: sortie avec codes ANSI (couleurs)sed ...: suppression des séquences ANSI pour ne garder que les caractères ASCII
Conversion ASCII → SVG
On transforme le fichier ASCII en SVG :aasvg --spaces=0 --stretch --fill < picture.txt > picture.svg
- Chaque glyphe devient un élément
<text>dans le SVG --spaces=0: pas d’espacement entre les caractères--stretch: étirement automatique--fill: remplissage des glyphes
- Chaque glyphe devient un élément
🥈 Solution 2 : ImageMagick + chafa + a2s
🧩 Prérequis
ImageMagick(convert)chafaa2ssed
Étapes
Prétraitement de l’image
On prépare l’image pour une meilleure détection des contours :convert picture.png -colorspace gray -normalize -edge 1 -threshold 10% picture-pre.png
- Conversion en niveaux de gris
- Normalisation du contraste
- Détection des bords
- Seuillage pour accentuer les contours
Conversion en ASCII simplifié
On génère une version ASCII sans couleurs :chafa --format=symbols --colors=none --dither=none \ --symbols=ascii --size=200x \ picture-pre.png > picture.txt
--symbols=ascii: uniquement des caractères ASCII--colors=none: pas de couleurs--dither=none: pas de tramage--size=200x: largeur de 200 caractères
ASCII → SVG
On convertit le fichier ASCII en SVG :a2s < picture.txt > picture.svg
Nettoyage du SVG
On supprime les lignes parasites :sed '/<g id="lines"/,/<\/g>/d' picture.svg > picture-clean.svg
📁 Résultat
picture.txt: version ASCII de l’imagepicture.svg: version vectoriellepicture-clean.svg: SVG nettoyé (solution 2)
🥉 Solution 3 : img2txt + scrypt Python 3
Avec couleurs Préservées
🧩 Prérequis
img2txt(du paquetcaca-utils)pythonsed
On génère une version ASCII avec couleurs :
img2txt -W 100 -x 1 -y 2 picture.png > picture.txt
Exécution du script Python
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
Script python
#!/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()
Suppresion du fond
sed -i '/<rect/d' picture.svg
📌 Notes
- 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 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
<!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>