File: annotation.py

package info (click to toggle)
slm 2.11-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 24,504 kB
  • sloc: python: 15,349; javascript: 5,043; makefile: 184; sh: 182; xml: 57
file content (178 lines) | stat: -rw-r--r-- 7,043 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178

from pyx import path, canvas, style, deco, text, color, connector, document, \
    svgwriter
from svg.path import parse_path
from subprocess import call
import tempfile, os
from PIL import Image
from io import BytesIO
from cairosvg import svg2png
import xml.etree.ElementTree as ET
import re

class TextArrow:
    """
    Crée un texte sur un fond de couleur de largeur donnée,
    attaché à une flèche
    Les paramètres du constructeur sont :
    @param xt abscisse du centre du texte
    @param yt ordonnée du centre du texte
    @param wt largeur max. de la boîte de texte
    @param x2 abscisse du bout de la flèche
    @param y2 ordonnée du bout de la flèche
    @param atext un texte (unicode)
    @param height_minus (None par défaut); s'il est défini, les ordonnees
        sont calculées comme height_minus - y
    """
    
    def __init__(self, xt, yt, wt, x2, y2, atext, height_minus=None):
        self.xt = float(xt)
        self.yt = float(yt)
        self.wt = float(wt)
        self.x2 = float(x2)
        self.y2 = float(y2)
        self.atext = atext
        if height_minus is not None:
            height_minus = float(height_minus)
            self.yt = height_minus - self.yt
            self.y2 = height_minus - self.y2
        self.attrs = [text.halign.center, text.vshift.middlezero]
        return

    def to_python(self):
        """
        produit la source python pour créer un clone de l'instance
        avec la possibilité de traduire le texte
        """
        return """\
TextArrow({xt:.1f}, {yt:.1f}, {wt:.1f}, {x2:.1f}, {y2:.1f},
    _('''{atext}'''))\
""".format(**self.__dict__)

    def __str__(self):
        return "« {atext} » : position, largeur = ({xt}, {yt}), {wt} ; pointe vers : ({x2}, {y2})".format(**self.__dict__)
    
    @staticmethod
    def from_SVG(source):
        """
        Crée une liste d'instances de TextArrow étant donné
        un fichier SVG
        @param source un nom de fichier, ou un flux de données
        """
        ns = {
            "inkscape": "http://www.inkscape.org/namespaces/inkscape",
            "sodipodi": "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd",
            "xlink": "http://www.w3.org/1999/xlink",
            "svg": "http://www.w3.org/2000/svg",
        }
        root = ET.parse(source).getroot()
        result=[]
        for group in root.findall(".//svg:g", ns):
            text = group.find("./svg:text", ns)
            has_text = isinstance(text, ET.Element)
            path = group.find("./svg:path", ns)
            has_path = isinstance(path, ET.Element)
            rect = group.find("./svg:rect", ns)
            has_rect = isinstance(rect, ET.Element)
            if has_text and has_path and has_rect:
                letexte = " ".join([t. text for t in text.findall("./svg:tspan", ns)])
                xt = text.attrib.get('x')
                yt = text.attrib.get('y')
                wt = rect.attrib.get('width')
                lapointe = parse_path(path.attrib.get("d"))
                z = lapointe[-1].end
                x2, y2 = z.real, z.imag
                result.append(TextArrow(xt, yt, wt, x2, y2, letexte, height_minus = root.attrib.get("height")))
        return result
        

def annote(img, *textarrows):
    """
    Annote une image "en place" si c'est une instance de PIL.Image.Image,
    mais accepte aussi un nom de fichier.
    @param img un nom de fichier ou une instance de PIL.Image.Image
    @param textarrows une liste d'instances de TextArrow
    @return une instance de PIL.Image.Image
    """
    if not isinstance(img, Image.Image):
        img = Image.open(img)
    img.convert("RGBA")
    # calcul d'échelle pour que les tailles des annotations et des flèches
    # soient raisonnables par rapport à l'image bitmap
    scale = 18/max(img.width, img.height)
    # LaTex devra accepter du texte unicode
    text.set(engine=text.LatexRunner, texenc='utf-8')
    text.preamble(r'\usepackage{ucs}')
    text.preamble(r'\usepackage[utf8x]{inputenc}')
    frame = path.rect(
        0, 0, img.width*scale, img.height*scale)
    c = canvas.canvas()
    c.draw(frame, []) # rectangle englobant, invisible
    backgroundstyle = [style.linewidth.Thick, color.rgb.blue,
                       deco.filled([color.rgb.white])]
    curvestyle = [color.rgb.blue, deco.earrow.normal, style.linewidth.Thick]
    for t in textarrows:
        # création d'objets à dessiner
        textstyle = [text.parbox(t.wt*scale)] + t.attrs
        text_ = text.text(t.xt*scale, t.yt*scale, t.atext, textstyle)
        background = text_.bbox().enlarged(0.1).path()
        curve = connector.curve(
            text_, text.text(t.x2*scale, t.y2*scale, " "), boxdists=[0.1, 0.1])
        # dessin sur le canevas
        c.stroke(background, backgroundstyle)
        c.insert(text_)
        c.stroke(curve, curvestyle)
    # export et conversion au format PNG lisible par PIL.Image
    SVGbuffer = BytesIO()
    PNGbuffer = BytesIO()
    svgwriter.SVGwriter(document.document([document.page(c)]), SVGbuffer)
    SVGbuffer.seek(0)
    svg2png(
        file_obj = SVGbuffer,
        output_width = img.width,
        output_height = img.height,
        write_to = PNGbuffer)
    PNGbuffer.seek(0)
    # récupération des annotations et surimpression sur l'image
    annotations = Image.open(PNGbuffer, formats=["png"])
    img.paste(annotations, (0,0), annotations)
    return img

def annote_par_svg(PNGimage,SVGfilename):
    """
    crée une instance de PIL.Image.Image, à partir d'un fichier PNG
    et d'un fichier SVG ; ce dernier doit contenir des groupes, chacun
    contenant un texte, un rectangle et une flèche. Chaque groupe forme
    une annotation, et la taille du fichier SVG doit correspondre à celle
    de l'image PNG (en pixels)
    @param PNGimage nom d'un fichier d'image au format PNG, ou instance
           de PIL.Image.Image
    @param SVGfilename nom d'un fichier d'image au format SVG
    @return une instance de PIL.Image.Image
    """
    if type(PNGimage) is str:
        PNGimage = Image.open(PNGimage)
    return annote(PNGimage, *TextArrow.from_SVG(SVGfilename))

def to_python_avec_svg(SVGfilename):
    """
    crée une source en Python qui fait en partie le travail de la fonction
    précédente : la liste TextArrow.from_SVG(SVGfilename)
    @param PNGfilename nom d'un fichier d'image au format PNG
    @param SVGfilename nom d'un fichier d'image au format SVG
    @return une instance de PIL.Image.Image
    """
    tas = TextArrow.from_SVG(SVGfilename)
    return "[\n" + ",\n".join(["  "+ ta.to_python() for ta in tas]) + "\n]"

def demo():
    source = Image.open("content/images/fr-fr/cartemembre4.png")
    img = annote_par_svg(source, "annotations/cartemembre4.svg")
    img.save("/tmp/resultat.png")
    print("L'annotation est ajoutée, dans /tmp/resultat.png")
    print("==== un peu de code source Python ====")
    print(to_python_avec_svg("annotations/cartemembre4.svg"))
    
if __name__ == "__main__":
    demo()