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()
|