#!/usr/bin/env python3 # coding=utf-8 """ Extension Inkscape : remplit l'interieur des formes selectionnees avec un motif isometrique de « Y » en relief. Chaque forme (chemin, rectangle, cercle... ; les groupes sont parcourus) est aplatie en polygone, puis le motif, calcule par `relief_core`, est decoupe a l'interieur de ce polygone (trous compris). Le resultat est un groupe place juste au-dessus de la forme, avec un seul chemin par forme. La logique vit dans `relief_core`, sans dependance a inkex. """ import os import sys import inkex from inkex.bezier import cspsubdiv from inkex.localization import localize from inkex.units import convert_unit # L'extension vit dans son propre sous-dossier des extensions Inkscape, qui # n'est pas forcement dans sys.path : on l'y ajoute pour trouver relief_core. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from relief_core import (fill_rings, parse_color, polylines_to_d, # noqa: E402 rings_bbox, segments_to_polylines) def use_system_language(): """Renseigne LANGUAGE avec la langue du systeme si rien ne la fixe. Sous Windows, Inkscape regle sur « langue du systeme » ne transmet aucune variable de langue au script : gettext retomberait sur l'anglais alors que la boite de dialogue, traduite par Inkscape lui-meme, est dans la langue de l'utilisateur. """ if any(os.environ.get(name) for name in ("LANGUAGE", "LC_ALL", "LC_MESSAGES", "LANG")): return try: import ctypes import locale language = locale.windows_locale.get( ctypes.windll.kernel32.GetUserDefaultUILanguage()) except Exception: # pylint: disable=broad-except return # hors Windows : LANG est toujours defini if language: os.environ["LANGUAGE"] = language # Installe _() dans les builtins : catalogue « relief_fill » de la langue # de l'interface (Inkscape transmet domaine et dossier par l'environnement), # textes anglais d'origine sinon. use_system_language() localize() def uu_per_mm(svg): """Unites utilisateur par millimetre, comme Inkscape les affiche. svg.unittouu() devine l'unite du document par arrondi a l'unite connue la plus proche : il se trompe des que l'echelle n'est pas standard (page redimensionnee). On prend le rapport reel page / viewBox ; Inkscape applique une echelle uniforme (preserveAspectRatio « meet »), soit la plus petite des deux. """ try: vb_w, vb_h = svg.viewbox_width, svg.viewbox_height page_w = svg.to_dimensional(svg.viewport_width, "mm") page_h = svg.to_dimensional(svg.viewport_height, "mm") except AttributeError: # inkex anterieur a 1.2 return svg.unittouu("1mm") if not (vb_w and vb_h and page_w and page_h): return svg.unittouu("1mm") return 1 / min(page_w / vb_w, page_h / vb_h) def length_to_uu(svg, value, unit="mm"): """Longueur reelle (valeur + unite) -> unites utilisateur du document.""" return convert_unit("{}{}".format(value, unit), "mm") * uu_per_mm(svg) # Formes dont on sait lire le contour ; les groupes sont parcourus. SHAPE_TYPES = tuple( getattr(inkex, name) for name in ("PathElement", "Rectangle", "Circle", "Ellipse", "Polygon", "Polyline", "Line") if hasattr(inkex, name)) def shape_rings(elem, flatness): """Contour de `elem` en coordonnees absolues : liste d'anneaux de points. Les courbes sont aplaties a la tolerance `flatness` (unites utilisateur). Un sous-chemin ouvert est traite comme ferme, comme le fait un remplissage. """ path = elem.path.transform(elem.composed_transform()) csp = path.to_superpath() cspsubdiv(csp, flatness) rings = [] for sub in csp: ring = [(node[1][0], node[1][1]) for node in sub] if len(ring) > 1 and ring[0] == ring[-1]: ring.pop() if len(ring) >= 3: rings.append(ring) return rings class ReliefFill(inkex.EffectExtension): """Remplit chaque forme selectionnee avec le motif de Y en relief.""" def add_arguments(self, pars): # Un argument par du .inx, memes noms et memes valeurs par defaut. pars.add_argument("--tab", default="pattern") pars.add_argument("--edge", type=float, default=5.0) pars.add_argument("--unit", default="mm") pars.add_argument("--angle", type=float, default=0.0) pars.add_argument("--origin", default="shape") pars.add_argument("--stroke_color", default="255") pars.add_argument("--stroke_width", type=float, default=0.3) pars.add_argument("--draw_outline", type=inkex.Boolean, default=False) pars.add_argument("--keep_original", type=inkex.Boolean, default=True) def effect(self): opt = self.options elements = list(self.svg.selection.values()) if not elements: inkex.errormsg(_("Select at least one shape.")) return edge = length_to_uu(self.svg, opt.edge, opt.unit) if edge <= 0: inkex.errormsg(_("The edge length must be strictly positive.")) return width = length_to_uu(self.svg, opt.stroke_width, opt.unit) if width < 0: inkex.errormsg(_("The stroke width cannot be negative.")) return color, opacity = parse_color(opt.stroke_color, default=("#000000", 1.0)) style = inkex.Style({ "fill": "none", "stroke": color, "stroke-opacity": str(opacity), "stroke-width": str(width), "stroke-linecap": "round", "stroke-linejoin": "round", }) flatness = length_to_uu(self.svg, 0.02) # 0.02 mm : invisible a l'impression for elem in elements: shapes = [e for e in elem.descendants() if isinstance(e, SHAPE_TYPES)] if not shapes: inkex.errormsg(_("The selection contains no shape with an outline " "(path, rectangle, circle...). Convert text to " "paths first.")) continue if not self.process(elem, shapes, edge, style, flatness): return def process(self, elem, shapes, edge, style, flatness): """Remplit les `shapes` de `elem` ; renvoie False en cas d'erreur fatale.""" opt = self.options results = [] # (anneaux, segments) par forme for shape in shapes: rings = shape_rings(shape, flatness) if not rings: continue if opt.origin == "document": anchor = (0.0, 0.0) else: xmin, ymin, xmax, ymax = rings_bbox(rings) anchor = ((xmin + xmax) / 2.0, (ymin + ymax) / 2.0) try: segments = fill_rings(rings, edge, opt.angle, anchor) except OverflowError: inkex.errormsg(_("The pattern would have too many lines. " "Increase the edge length or select a smaller shape.")) return False results.append((rings, segments)) if not any(segments for _rings, segments in results): inkex.errormsg(_("The edge length is too large for this shape: no line " "fits inside. Reduce the edge length.")) return True parent = elem.getparent() group = inkex.Group() group.label = _("Relief fill") if parent is not None: # Resultat calcule en coordonnees absolues du document : on # neutralise la transformation heritee du parent d'accueil. parent_transform = parent.composed_transform() if parent_transform != inkex.Transform(): group.transform = -parent_transform parent.insert(parent.index(elem) + 1, group) else: self.svg.add(group) for rings, segments in results: if opt.draw_outline: outline = inkex.PathElement() outline.path = polylines_to_d([ring + [ring[0]] for ring in rings]) outline.style = style group.add(outline) if segments: node = inkex.PathElement() node.path = polylines_to_d(segments_to_polylines(segments), precision=3) node.style = style group.add(node) if not opt.keep_original: elem.delete() return True if __name__ == "__main__": ReliefFill().run()