« Image vers ASCII vers SVG » : différence entre les versions

 
(13 versions intermédiaires par le même utilisateur non affichées)
Ligne 1 : Ligne 1 :
Voici une proposition de page '''README.md''' en Markdown qui explique clairement ce que font les deux solutions que tu as listées. Elle est structurée pour être facilement compréhensible par des développeurs ou utilisateurs techniques :
= 🖼️ 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.
 
-----
-----


= 🖼️ Conversion d’image en ASCII puis en SVG =
Voir : 
*[[Ascii| Ascii]]
*[[SVG| SVG]]


Ce projet propose deux 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.
== 🥇 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>sed</code>
* <code>aasvg</code>
* <code>aasvg</code>
* <code>ImageMagick</code> (<code>convert</code>)
* <code>chafa</code>
* <code>a2s</code>
-----
== 🥇 Solution 1 : img2txt + aasvg ==


=== É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 =
= Exemple d'intégration de l'image SVG dans un fichier HTML =
Ligne 115 : Ligne 421 :
         height: 100%;
         height: 100%;
         position: fixed;
         position: fixed;
         background-image: url("picture-clean.svg");
         background-image: url("picture.svg");
         background-position: center center;
         background-position: center center;
         background-repeat: no-repeat;
         background-repeat: no-repeat;
Ligne 132 : Ligne 438 :
[[Catégorie:Dev]]
[[Catégorie:Dev]]
[[Catégorie:Geek]]
[[Catégorie:Geek]]
[[Catégorie: Terminal Tools]]