diff --git a/inkscape/extensions/tikz_export.py b/inkscape/extensions/tikz_export.py
new file mode 100644
index 0000000..af5d603
--- /dev/null
+++ b/inkscape/extensions/tikz_export.py
@@ -0,0 +1,1622 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""\
+Convert SVG to TikZ/PGF commands for use with (La)TeX
+
+This script is an Inkscape extension for exporting from SVG to (La)TeX. The
+extension recreates the SVG drawing using TikZ/PGF commands, a high quality TeX
+macro package for creating graphics programmatically.
+
+The script is tailored to Inkscape SVG, but can also be used to convert arbitrary
+SVG files from the command line.
+
+Author: Kjell Magne Fauske, Devillez Louis
+"""
+
+import platform
+
+__version__ = "3.2.1"
+__author__ = "Devillez Louis, Kjell Magne Fauske"
+__maintainer__ = "Deville Louis"
+__email__ = "louis.devillez@gmail.com"
+
+import sys
+
+from textwrap import wrap
+import codecs
+import io
+import os
+from subprocess import Popen, PIPE
+
+from math import sin, cos, atan2, radians, degrees
+from math import pi as mpi
+
+import logging
+
+import ctypes
+import inkex
+from inkex.transforms import Vector2d
+from lxml import etree
+
+try:
+ SYS_OUTPUT_BUFFER = sys.stdout.buffer
+except AttributeError: # pragma: no cover
+ logging.warning("Sys has no output buffer, redirecting to None")
+ SYS_OUTPUT_BUFFER = None
+
+#### Utility functions and classes
+
+TIKZ_BASE_COLOR = [
+ "black",
+ "red",
+ "green",
+ "blue",
+ "cyan",
+ "yellow",
+ "magenta",
+ "white",
+ "gray",
+]
+
+LIST_OF_SHAPES = [
+ "path",
+ "rect",
+ "circle",
+ "ellipse",
+ "line",
+ "polyline",
+ "polygon",
+]
+
+SPECIAL_TEX_CHARS = ["$", "\\", "%", "_", "#", "{", r"}", "^", "&"]
+SPECIAL_TEX_CHARS_REPLACE = [
+ r"\$",
+ r"$\backslash$",
+ r"\%",
+ r"\_",
+ r"\#",
+ r"\{",
+ r"\}",
+ r"\^{}",
+ r"\&",
+]
+_tex_charmap = dict(list(zip(SPECIAL_TEX_CHARS, SPECIAL_TEX_CHARS_REPLACE)))
+
+
+def escape_texchars(input_string):
+ r"""Escape the special LaTeX-chars %{}_^
+
+ Examples:
+
+ >>> escape_texchars('10%')
+ '10\\%'
+ >>> escape_texchars('%{}_^\\$')
+ '\\%\\{\\}\\_\\^{}$\\backslash$\\$'
+ """
+ return "".join([_tex_charmap.get(c, c) for c in input_string])
+
+
+def copy_to_clipboard(text): # pragma: no cover
+ """Copy text to the clipboard
+
+ Returns True if successful. False otherwise.
+ """
+
+ text_type = str
+
+ def _do_windows_clipboard(text):
+ # from http://pylabeditor.svn.sourceforge.net/viewvc/pylabeditor/trunk/src/shells.py?revision=82&view=markup
+
+ cf_unicode_text = 13
+ ghnd = 66
+
+ ctypes.windll.kernel32.GlobalAlloc.restype = ctypes.c_void_p
+ ctypes.windll.kernel32.GlobalLock.restype = ctypes.c_void_p
+
+ text = text_type(text, "utf8")
+ buffer_size = (len(text) + 1) * 2
+ h_global_mem = ctypes.windll.kernel32.GlobalAlloc(
+ ctypes.c_uint(ghnd), ctypes.c_size_t(buffer_size)
+ )
+ lp_global_mem = ctypes.windll.kernel32.GlobalLock(ctypes.c_void_p(h_global_mem))
+ ctypes.cdll.msvcrt.memcpy(
+ ctypes.c_void_p(lp_global_mem),
+ ctypes.c_wchar_p(text),
+ ctypes.c_int(buffer_size),
+ )
+ ctypes.windll.kernel32.GlobalUnlock(ctypes.c_void_p(h_global_mem))
+ if ctypes.windll.user32.OpenClipboard(0):
+ ctypes.windll.user32.EmptyClipboard()
+ ctypes.windll.user32.SetClipboardData(
+ ctypes.c_int(cf_unicode_text), ctypes.c_void_p(h_global_mem)
+ )
+ ctypes.windll.user32.CloseClipboard()
+ return True
+ return False
+
+ def _call_command(command, text):
+ # see https://bugs.launchpad.net/ubuntu/+source/inkscape/+bug/781397/comments/2
+ try:
+ devnull = os.open(os.devnull, os.O_RDWR)
+ with Popen(command, stdin=PIPE, stdout=devnull, stderr=devnull) as proc:
+ proc.communicate(text)
+ if not proc.returncode:
+ return True
+
+ except OSError:
+ pass
+ return False
+
+ def _do_linux_clipboard(text):
+ # try xclip first, then xsel
+ xclip_cmd = ["xclip", "-selection", "clipboard"]
+ success = _call_command(xclip_cmd, text)
+ if success:
+ return True
+
+ xsel_cmd = ["xsel"]
+ success = _call_command(xsel_cmd, text)
+ return success
+
+ def _do_osx_clipboard(text):
+ pbcopy_cmd = ["pbcopy"]
+ return _call_command(pbcopy_cmd, text)
+ # try os /linux
+
+ if os.name == "nt" or platform.system() == "Windows":
+ return _do_windows_clipboard(text)
+ if os.name == "mac" or platform.system() == "Darwin":
+ return _do_osx_clipboard(text)
+ return _do_linux_clipboard(text)
+
+
+def filter_tag(node):
+ """
+ A function to see if a node should be draw or not
+ """
+ # pylint: disable=comparison-with-callable
+ # As it is done in lxml
+ if node.tag == etree.Comment:
+ return False
+ if node.TAG in [
+ "desc",
+ "namedview",
+ "defs",
+ "svg",
+ "symbol",
+ "title",
+ "style",
+ "metadata",
+ ]:
+ return False
+ return True
+
+
+#### Output configuration section
+
+TEXT_INDENT = " "
+
+CROP_TEMPLATE = r"""
+\usepackage[active,tightpage]{preview}
+\PreviewEnvironment{tikzpicture}
+"""
+
+# Templates
+STANDALONE_TEMPLATE = (
+ r"""
+\documentclass{article}
+\usepackage[utf8]{inputenc}
+\usepackage{tikz}
+%(cropcode)s
+\begin{document}
+%(colorcode)s
+%(gradientcode)s
+\def \globalscale {%(scale)f}
+\begin{tikzpicture}[y=1%(unit)s, x=1%(unit)s, yscale=%(ysign)s\globalscale,"""
+ r"""xscale=\globalscale, every node/.append style={scale=\globalscale}, inner sep=0pt, outer sep=0pt]
+%(pathcode)s
+\end{tikzpicture}
+\end{document}
+"""
+)
+
+FIG_TEMPLATE = (
+ r"""
+%(colorcode)s
+%(gradientcode)s
+\def \globalscale {%(scale)f}
+\begin{tikzpicture}[y=1%(unit)s, x=1%(unit)s, yscale=%(ysign)s\globalscale,"""
+ r"""xscale=\globalscale, every node/.append style={scale=\globalscale}, inner sep=0pt, outer sep=0pt]
+%(pathcode)s
+\end{tikzpicture}
+"""
+)
+
+SCALE = "scale"
+DICT = "dict"
+DIMENSION = "dimension"
+FACTOR = "factor" # >= 1
+
+# Map Inkscape/SVG stroke and fill properties to corresponding TikZ options.
+# Format:
+# 'svg_name' : ('tikz_name', value_type, data)
+PROPERTIES_MAP = {
+ "text-anchor": (
+ "anchor",
+ DICT,
+ {"start": "south west", "middle": "south", "end": "south east"},
+ ),
+ "opacity": ("opacity", SCALE, ""),
+ # filling
+ "fill-opacity": ("fill opacity", SCALE, ""),
+ "fill-rule": ("", DICT, {"nonzero": "nonzero rule", "evenodd": "even odd rule"}),
+ # stroke
+ "stroke-opacity": ("draw opacity", SCALE, ""),
+ "stroke-linecap": (
+ "line cap",
+ DICT,
+ {"butt": "butt", "round": "round", "rect": "rect"},
+ ),
+ "stroke-linejoin": (
+ "line join",
+ DICT,
+ {
+ "miter": "miter",
+ "round": "round",
+ "bevel": "bevel",
+ },
+ ),
+ "stroke-width": ("line width", DIMENSION, ""),
+ "stroke-miterlimit": ("miter limit", FACTOR, ""),
+ "stroke-dashoffset": ("dash phase", DIMENSION, "0"),
+}
+
+
+def calc_arc(cp: Vector2d, r_i: Vector2d, ang, fa, fs, pos: Vector2d):
+ """
+ Calc arc paths
+
+ It computes the start and end angle for a non rotated ellipse
+
+ cp: initial control point
+ r_i: x and y radius
+ ang: x-axis-rotation
+ fa: sweep flag
+ fs: large sweep flag
+ pos: final control point
+
+ The calc_arc function is based on the calc_arc function in the
+ paths_svg2obj.py script bundled with Blender 3D
+ Copyright (c) jm soler juillet/novembre 2004-april 2007,
+ Resource: https://developer.mozilla.org/fr/docs/Web/SVG/Tutorial/Paths#elliptical_arc (in french)
+ """
+ # print(ang)
+ ang = radians(ang)
+
+ r = Vector2d(abs(r_i.x), abs(r_i.y))
+
+ p_pos = Vector2d(
+ abs((cos(ang) * (cp.x - pos.x) + sin(ang) * (cp.y - pos.y)) * 0.5) ** 2.0,
+ abs((cos(ang) * (cp.y - pos.y) - sin(ang) * (cp.x - pos.x)) * 0.5) ** 2.0,
+ )
+ rp = Vector2d(
+ p_pos.x / (r.x**2.0) if abs(r.x) > 0.0 else 0.0,
+ p_pos.y / (r.y**2.0) if abs(r.y) > 0.0 else 0.0,
+ )
+
+ p_l = rp.x + rp.y
+ if p_l > 1.0:
+ p_l = p_l**0.5
+ r.x *= p_l
+ r.y *= p_l
+
+ car = Vector2d(
+ cos(ang) / r.x if abs(r.x) > 0.0 else 0.0,
+ cos(ang) / r.y if abs(r.y) > 0.0 else 0.0,
+ )
+
+ sar = Vector2d(
+ sin(ang) / r.x if abs(r.x) > 0.0 else 0.0,
+ sin(ang) / r.y if abs(r.y) > 0.0 else 0.0,
+ )
+
+ p0 = Vector2d(car.x * cp.x + sar.x * cp.y, (-sar.y) * cp.x + car.y * cp.y)
+ p1 = Vector2d(car.x * pos.x + sar.x * pos.y, (-sar.y) * pos.x + car.y * pos.y)
+
+ hyp = (p1.x - p0.x) ** 2 + (p1.y - p0.y) ** 2
+
+ if abs(hyp) > 0.0:
+ s_q = 1.0 / hyp - 0.25
+ else:
+ s_q = -0.25
+
+ s_f = max(0.0, s_q) ** 0.5
+ if fs == fa:
+ s_f *= -1
+ c = Vector2d(
+ 0.5 * (p0.x + p1.x) - s_f * (p1.y - p0.y),
+ 0.5 * (p0.y + p1.y) + s_f * (p1.x - p0.x),
+ )
+ ang_0 = atan2(p0.y - c.y, p0.x - c.x)
+ ang_1 = atan2(p1.y - c.y, p1.x - c.x)
+ ang_arc = ang_1 - ang_0
+ if ang_arc < 0.0 and fs == 1:
+ ang_arc += 2.0 * mpi
+ elif ang_arc > 0.0 and fs == 0:
+ ang_arc -= 2.0 * mpi
+
+ ang0 = degrees(ang_0)
+ ang1 = degrees(ang_1)
+
+ if ang_arc > 0:
+ if ang_0 < ang_1:
+ pass
+ else:
+ ang0 -= 360
+ else:
+ if ang_0 < ang_1:
+ ang1 -= 360
+
+ return ang0, ang1, r
+
+
+def parse_arrow_style(arrow_name):
+ """
+ Convert an svg arrow_name to tikz name of the arrow
+ """
+ strip_name = arrow_name.split("url")[1][1:-1]
+
+ if "Arrow1" in strip_name:
+ return "latex"
+ if "Arrow2" in strip_name:
+ return "stealth"
+ if "Stop" in strip_name:
+ return "|"
+ return "latex"
+
+
+def marking_interpret(marker):
+ """
+ Interpret the arrow from its name and its direction and convert it to tikz code
+ """
+ raw_marker = ""
+ if marker:
+ arrow_style = parse_arrow_style(marker)
+ raw_marker = arrow_style[:]
+ if "end" in marker:
+ raw_marker += " reversed"
+ return raw_marker
+
+
+def options_to_str(options: list) -> str:
+ """
+ Convert a list of options to a str with comma separated value.
+ If the list is empty, return an empty str
+ """
+ return f"[{','.join(options)}]" if len(options) > 0 else ""
+
+
+def return_arg_parser_doc():
+ """
+ Methode to return the arg parser of TikzPathExporter to help generate the doc
+ """
+ tzp = TikZPathExporter()
+ return tzp.arg_parser
+
+
+# pylint: disable=too-many-ancestors
+class TikZPathExporter(inkex.Effect, inkex.EffectExtension):
+ """Class to convert a svg to tikz code"""
+
+ def __init__(self, inkscape_mode=True):
+ self.inkscape_mode = inkscape_mode
+ inkex.Effect.__init__(self)
+ inkex.EffectExtension.__init__(self)
+ self._set_up_options()
+
+ self.text_indent = TEXT_INDENT
+ self.colors = []
+ self.color_code = ""
+ self.gradient_code = ""
+ self.output_code = ""
+ self.used_gradients = set()
+ self.height = 0
+ self.args_parsed = False
+
+ def _set_up_options(self):
+ parser = self.arg_parser
+ parser.set_defaults(
+ codeoutput="standalone",
+ crop=False,
+ clipboard=False,
+ wrap=False,
+ indent=True,
+ returnstring=False,
+ scale=1,
+ mode="effect",
+ notext=False,
+ verbose=False,
+ texmode="escape",
+ markings="ignore",
+ )
+ parser.add_argument(
+ "--codeoutput",
+ dest="codeoutput",
+ choices=("standalone", "codeonly", "figonly"),
+ help="Amount of boilerplate code (standalone, figonly, codeonly).",
+ )
+ parser.add_argument(
+ "-t",
+ "--texmode",
+ dest="texmode",
+ default="escape",
+ choices=("math", "escape", "raw", "attribute"),
+ help="Set text mode (escape, math, raw, attribute). Defaults to 'escape'",
+ )
+ parser.add_argument(
+ "--texmode-attribute",
+ default=None,
+ action="store",
+ dest="texmode_attribute",
+ help="The SVG attribute that specifies how to handle text",
+ )
+
+ parser.add_argument(
+ "--markings",
+ dest="markings",
+ default="arrows",
+ choices=("ignore", "include", "interpret", "arrows"),
+ help="Set markings mode. Defaults to 'ignore'",
+ )
+ parser.add_argument(
+ "--arrow",
+ dest="arrow",
+ default="latex",
+ choices=("latex", "stealth", "to", ">"),
+ help="Set arrow style for markings mode arrow. Defaults to 'latex'",
+ )
+ parser.add_argument(
+ "--output-unit",
+ dest="output_unit",
+ default="cm",
+ choices=("mm", "cm", "m", "in", "pt", "px", "Q", "pc"),
+ help="Set output units. Defaults to 'cm'",
+ )
+
+ self._add_booloption(
+ parser,
+ "--crop",
+ dest="crop",
+ help="Use the preview package to crop the tikzpicture",
+ )
+ self._add_booloption(
+ parser, "--clipboard", dest="clipboard", help="Export to clipboard"
+ )
+ self._add_booloption(parser, "--wrap", dest="wrap", help="Wrap long lines")
+ self._add_booloption(parser, "--indent", default=True, help="Indent lines")
+
+ parser.add_argument(
+ "--round-number",
+ dest="round_number",
+ type=int,
+ default=4,
+ help="Number of significative number after the decimal",
+ )
+
+ self._add_booloption(
+ parser,
+ "--latexpathtype",
+ dest="latexpathtype",
+ default=True,
+ help="Allow path modification for image",
+ )
+ self._add_booloption(
+ parser,
+ "--noreversey",
+ dest="noreversey",
+ help="Do not reverse the y axis (Inkscape axis)",
+ default=False,
+ )
+
+ parser.add_argument(
+ "--removeabsolute",
+ dest="removeabsolute",
+ default="",
+ help="Remove the value of removeabsolute from image path",
+ )
+
+ if self.inkscape_mode:
+ parser.add_argument(
+ "--returnstring",
+ action="store_true",
+ dest="returnstring",
+ help="Return as string",
+ )
+ parser.add_argument(
+ "--tab"
+ ) # Dummy option. Needed because Inkscape passes the notebook
+ # tab as an option.
+
+ parser.add_argument(
+ "-m",
+ "--mode",
+ dest="mode",
+ choices=("output", "effect", "cli"),
+ default="cli",
+ help="Extension mode (effect default)",
+ )
+ self._add_booloption(
+ parser,
+ "--notext",
+ dest="ignore_text",
+ default=False,
+ help="Ignore all text",
+ )
+ parser.add_argument(
+ "--scale",
+ dest="scale",
+ type=float,
+ default=1,
+ help="Apply scale to resulting image, defaults to 1.0",
+ )
+ if not self.inkscape_mode:
+ parser.add_argument(
+ "--standalone",
+ dest="codeoutput",
+ action="store_const",
+ const="standalone",
+ help="Generate a standalone document",
+ )
+ parser.add_argument(
+ "--figonly",
+ dest="codeoutput",
+ action="store_const",
+ const="figonly",
+ help="Generate figure only",
+ )
+ parser.add_argument(
+ "--codeonly",
+ dest="codeoutput",
+ action="store_const",
+ const="codeonly",
+ help="Generate drawing code only",
+ )
+ parser.add_argument(
+ "-V",
+ "--version",
+ dest="printversion",
+ action="store_true",
+ help="Print version information and exit",
+ default=False,
+ )
+ self._add_booloption(
+ parser,
+ "--verbose",
+ dest="verbose",
+ default=False,
+ help="Verbose output (useful for debugging)",
+ )
+
+ def _add_booloption(self, parser, *args, **kwargs):
+ if self.inkscape_mode:
+ kwargs["action"] = "store"
+ kwargs["type"] = inkex.Boolean
+ parser.add_argument(*args, **kwargs)
+ else:
+ kwargs["action"] = "store_true"
+ parser.add_argument(*args, **kwargs)
+
+ def sanitize_angles(self, start_raw: float, end_raw: float):
+ """
+ Sanitizes angles from arc to put them in [-360, 360] range
+
+ start_raw: start angle of the arc
+ end_raw: end angle of the arc
+ """
+
+ start_ang = self.round_value(start_raw % 360)
+ end_ang = self.round_value(end_raw % 360)
+ # # Does not to seem a problem anymore
+ if start_raw < end_raw and not start_ang < end_ang:
+ start_ang -= 360
+ elif start_raw > end_raw and not start_ang > end_ang:
+ end_ang -= 360
+ return start_ang, end_ang
+
+ def convert_unit(self, value: float) -> float:
+ """Convert value from the user unit to the output unit which is an option"""
+ ret = self.svg.unit_to_viewport(value, self.options.output_unit)
+ return ret
+
+ def convert_unit_coord(self, coord: Vector2d, update_height=True) -> Vector2d:
+ """
+ Convert a coord (Vector2D)) from the user unit to the output unit
+ """
+ y = self.convert_unit(coord[1])
+ return Vector2d(
+ self.convert_unit(coord[0]),
+ self.update_height(y) if update_height else y,
+ )
+
+ def convert_unit_coords(self, coords, update_height=True):
+ """
+ Convert a list of coords (Vector2D)) from the user unit to the output unit
+ """
+ return [self.convert_unit_coord(coord, update_height) for coord in coords]
+
+ def round_value(self, value):
+ """Round a value with respect to the round number of the class"""
+ return round(value, self.options.round_number)
+
+ def round_coord(self, coord):
+ """Round a coordinante(Vector2D) with respect to the round number of the class"""
+ return Vector2d(self.round_value(coord[0]), self.round_value(coord[1]))
+
+ def round_coords(self, coords):
+ """Round a coordinante(Vector2D) with respect to the round number of the class"""
+ return [self.round_coord(coord) for coord in coords]
+
+ def rotate_coord(self, coord: Vector2d, angle: float) -> Vector2d:
+ """
+ rotate a coordinate around (0,0) of angle radian
+ """
+ return Vector2d(
+ coord.x * cos(angle) - coord.y * sin(angle),
+ coord.x * sin(angle) + coord.y * cos(angle),
+ )
+
+ def coord_to_tz(self, coord: Vector2d) -> str:
+ """
+ Convert a coord (Vector2d) which is round and converted to tikz code
+ """
+ c = self.round_coord(coord)
+ return f"({c.x}, {c.y})"
+
+ def update_height(self, y_val):
+ """Compute the distance between the point and the bottom of the document"""
+ if not self.options.noreversey:
+ return self.height - y_val
+ return y_val
+
+ def convert_color_to_tikz(self, color):
+ """
+ Convert a svg color to tikzcode and add it to the list of known colors
+ """
+ color = color.to_rgb()
+ xcolorname = str(color.to_named()).replace("#", "c")
+ if xcolorname in TIKZ_BASE_COLOR:
+ return xcolorname
+ if xcolorname not in self.colors:
+ self.colors.append(xcolorname)
+ self.color_code += "\\definecolor{" + f"{xcolorname}" + "}{RGB}{"
+ self.color_code += f"{color.red},{color.green},{color.blue}" + "}\n"
+ return xcolorname
+
+ # def _convert_gradient(self, gradient_node, gradient_tikzname):
+ # """Convert an SVG gradient to a PGF gradient"""
+
+ # # http://www.w3.org/TR/SVG/pservers.html
+ # def bpunit(offset):
+ # bp_unit = ""
+ # if offset.endswith("%"):
+ # bp_unit = offset[0:-1]
+ # else:
+ # bp_unit = str(int(round((float(offset)) * 100)))
+ # return bp_unit
+
+ # if gradient_node.tag == _ns("linearGradient"):
+ # c = ""
+ # c += (
+ # r"\pgfdeclarehorizontalshading{"
+ # + f"{gradient_tikzname}"
+ # + "}{100bp}{\n"
+ # )
+ # stops = []
+ # for n in gradient_node:
+ # if n.tag == _ns("stop"):
+ # stops.append(
+ # f"color({bpunit(n.get('offset'))}pt)="
+ # f"({self.get_color(n.get('stop-color'))})"
+ # )
+ # c += ";".join(stops)
+ # c += "\n}\n"
+ # return c
+
+ # return ""
+
+ # def _handle_gradient(self, gradient_ref):
+ # grad_node = self.get_node_from_id(gradient_ref)
+ # gradient_id = grad_node.get("id")
+ # if grad_node is None:
+ # return []
+ # gradient_tikzname = gradient_id
+ # if gradient_id not in self.used_gradients:
+ # grad_code = self._convert_gradient(grad_node, gradient_tikzname)
+ # if grad_code:
+ # self.gradient_code += grad_code
+ # self.used_gradients.add(gradient_id)
+ # if gradient_id in self.used_gradients:
+ # return ["shade", f"shading={gradient_tikzname}"]
+ # return []
+
+ def _handle_markers(self, style):
+ """
+ Convert marking style from svg to tikz code
+ """
+ # http://www.w3.org/TR/SVG/painting.html#MarkerElement
+ ms = style.get("marker-start")
+ me = style.get("marker-end")
+
+ # Avoid options "-" on empty path
+ if ms is None and me is None:
+ return []
+
+ if self.options.markings == "ignore":
+ return []
+
+ if self.options.markings == "include":
+ # TODO to implement:
+ # Include arrow as path object
+ # Define custom arrow and use them
+ return []
+
+ if self.options.markings == "interpret":
+ start_arrow = marking_interpret(ms)
+ end_arrow = marking_interpret(me)
+
+ return [start_arrow + "-" + end_arrow]
+
+ if self.options.markings == "arrows":
+ start_arrow = self.options.arrow[:] if ms is not None else ""
+
+ if ms is not None and "end" in ms:
+ start_arrow += " reversed"
+
+ if start_arrow == self.options.arrow:
+ start_arrow = "<"
+ if me is not None and "end" in me:
+ start_arrow = ">"
+
+ end_arrow = self.options.arrow[:] if me is not None else ""
+ if me and "start" in me:
+ end_arrow += " reversed"
+
+ if end_arrow == self.options.arrow:
+ end_arrow = ">"
+ if me is not None and "start" in me:
+ end_arrow = "<"
+
+ return [start_arrow + "-" + end_arrow]
+ return []
+
+ def _handle_dasharray(self, style):
+ """
+ Convert dasharry style from svg to tikz code
+ """
+ dasharray = style.get("stroke-dasharray")
+
+ if dasharray is None or dasharray == "none":
+ return []
+
+ lengths = dasharray.replace(",", " ").replace(" ", " ").split(" ")
+ dashes = []
+ for idx, length in enumerate(lengths):
+ l = self.round_value(self.convert_unit(float(length)))
+ lenstr = f"{l}{self.options.output_unit}"
+ if idx % 2:
+ dashes.append(f"off {lenstr}")
+ else:
+ dashes.append(f"on {lenstr}")
+
+ return [f"dash pattern={' '.join(dashes)}"]
+
+ def get_shape_inside(self, node=None):
+ """
+ Get back the shape from the shape_inside style attribute
+ """
+ style = node.specified_style()
+ url = style.get("shape-inside")
+ if url is None:
+ return None
+ shape = inkex.properties.match_url_and_return_element(url, self.svg)
+ return shape
+
+ def style_to_tz(self, node=None): # pylint: disable=too-many-branches
+ """
+ Convert the style from the svg to the option to apply to tikz code
+ """
+
+ style = node.specified_style()
+
+ # No display of the node
+ # Special handling of switch as they are meta elements
+ if node.TAG == "switch":
+ if style.get("display") == "none":
+ return ["none"]
+
+ elif style.get("display") == "none" or not node.is_visible:
+ if node.TAG == "g":
+ return ["none"]
+ return []
+
+ options = []
+
+ # Stroke and fill
+ for use_path in (
+ [("fill", "text")]
+ if node.TAG == "text"
+ else [("stroke", "draw"), ("fill", "fill")]
+ ):
+ value = style.get(use_path[0])
+ if value != "none" and value is not None:
+ options.append(
+ f"{use_path[1]}={self.convert_color_to_tikz(style.get_color(use_path[0]))}"
+ )
+
+ if value is None and use_path[0] == "fill" and node.TAG in LIST_OF_SHAPES:
+ # svg shapes with no fill option should fill by black
+ # https://www.w3.org/TR/2011/REC-SVG11-20110816/painting.html#FillProperty
+ options.append("fill")
+
+ # Other props
+ for svgname, tikzdata in PROPERTIES_MAP.items():
+ tikzname, valuetype, data = tikzdata
+ value = style.get(svgname)
+
+ if value is None or value == "none":
+ continue
+
+ if valuetype in [SCALE, FACTOR]:
+ val = float(value)
+
+ if val < 1 and valuetype == FACTOR:
+ continue
+
+ if val != 1:
+ options.append(f"{tikzname}={self.round_value(float(value))}")
+ elif valuetype == DICT:
+ if tikzname:
+ options.append(f"{tikzname}={data.get(value, '')}")
+ else:
+ options.append(data.get(value, ""))
+
+ elif valuetype == DIMENSION and value and value != data:
+ options.append(
+ f"{tikzname}="
+ f"{self.round_value(self.convert_unit(value))}"
+ f"{self.options.output_unit}"
+ )
+
+ # Arrow marker handling
+ options += self._handle_markers(style)
+
+ # Dash-array
+ options += self._handle_dasharray(style)
+
+ return options
+
+ def trans_to_tz(self, node=None, is_node=False):
+ """
+ Convert inkex transform to tikz code
+ """
+ transform = node.transform
+ # TODO decompose matrix in list of transform
+
+ options = []
+
+ for trans in [transform]:
+ # Empty transform
+ if str(trans) == "":
+ continue
+
+ # Translation
+ if trans.is_translate():
+ tr = self.convert_unit_coord(Vector2d(trans.e, trans.f), False)
+
+ # Global scale do not impact transform
+ if not self.options.noreversey or is_node:
+ tr.y *= -1
+
+ tr.x *= self.options.scale
+ tr.y *= self.options.scale
+
+ options.append("shift={" + self.coord_to_tz(tr) + "}")
+
+ # Rotation
+ elif trans.is_rotate():
+ # get angle
+ ang = -self.round_value(trans.rotation_degrees())
+
+ # If reverse coord, we rotate around old origin
+ if not self.options.noreversey:
+ options.append(
+ "rotate around={"
+ + f"{ang}:{self.coord_to_tz(Vector2d(0.0, self.update_height(0)))}"
+ + "}"
+ )
+ else:
+ options.append(f"rotate={ang}")
+
+ elif trans.is_scale():
+ x = self.round_value(trans.a)
+ y = self.round_value(trans.d)
+
+ if not self.options.noreversey and not is_node:
+ options.append("shift={(0," + f"{y * self.update_height(0)}" + ")}")
+
+ if x == y:
+ options.append(f"scale={x}")
+ else:
+ options.append(f"xscale={x},yscale={y}")
+
+ elif "matrix" in str(trans):
+ tr = self.convert_unit_coord(Vector2d(trans.e, trans.f), False)
+ a = self.round_value(trans.a)
+ b = self.round_value(trans.b)
+ c = self.round_value(trans.c)
+ d = self.round_value(trans.d)
+
+ # globalscale do not impact transform
+ if not self.options.noreversey or is_node:
+ tr.y *= -1
+ b *= -1
+ c *= -1
+
+ if not self.options.noreversey and not is_node:
+ tr.x += -c * self.update_height(0)
+ tr.y += (1 - d) * self.update_height(0)
+
+ tr.x *= self.options.scale
+ tr.y *= self.options.scale
+ options.append(f"cm={{ {a},{b},{c}" f",{d},{self.coord_to_tz(tr)}}}")
+
+ # Not possible to get them directly
+ # elif "skewX" in str(trans):
+ # options.append(f"xslant={math.tan(trans.c * math.pi / 180)}")
+ # elif "skewY" in str(trans):
+ # options.append(f"yslant={math.tan(trans.b * math.pi / 180)}")
+ # elif "scale" in str(trans):
+ # if trans.a == trans.d:
+ # options.append(f"scale={trans.a}")
+ # else:
+ # options.append(f"xscale={trans.a},yscale={trans.d}")
+ return options
+
+ def _handle_group(self, groupnode):
+ """
+ Convert a svg group to tikzcode
+ """
+ options = self.style_to_tz(groupnode) + self.trans_to_tz(groupnode)
+
+ old_indent = self.text_indent
+
+ if len(options) > 0:
+ self.text_indent += TEXT_INDENT
+
+ group_id = groupnode.get_id()
+ code = self._output_group(groupnode)
+
+ self.text_indent = old_indent
+
+ if code == "":
+ return ""
+
+ extra = ""
+ if self.options.verbose and group_id:
+ extra = f"%% {group_id}"
+
+ hide = "none" in options
+
+ s = ""
+ if len(options) > 0 or self.options.verbose:
+ # Remove it from the list
+ if hide or self.options.verbose:
+ if "none" in options:
+ options.remove("none")
+
+ pstyles = [",".join(options)]
+
+ if "opacity" in pstyles[0]:
+ pstyles.append("transparency group")
+
+ s += self.text_indent + "\\begin{scope}"
+ s += f"[{','.join(pstyles)}]{extra}\n{code}"
+ s += self.text_indent + "\\end{scope}\n"
+
+ if hide:
+ s = "%" + s.replace("\n", "\n%")[:-1]
+ else:
+ s = code
+ return s
+
+ def _handle_switch(self, groupnode):
+ """
+ Convert a svg switch to tikzcode
+ All the elements are returned for now
+ """
+ options = self.style_to_tz(groupnode) + self.trans_to_tz(groupnode)
+
+ old_indent = self.text_indent
+
+ if len(options) > 0:
+ self.text_indent += TEXT_INDENT
+
+ group_id = groupnode.get_id()
+ code = self._output_group(groupnode)
+
+ self.text_indent = old_indent
+
+ if code == "":
+ return ""
+
+ extra = ""
+ if self.options.verbose and group_id:
+ extra = f"%% {group_id}"
+
+ hide = "none" in options
+
+ s = ""
+ if len(options) > 0 or self.options.verbose:
+ # Remove it from the list
+ if hide:
+ options.remove("none")
+ # TODO ID of foreignObject are not consistent
+
+ pstyles = [",".join(options)]
+
+ if "opacity" in pstyles[0]:
+ pstyles.append("transparency group")
+
+ s += self.text_indent + "\\begin{scope}"
+ s += f"[{','.join(pstyles)}]{extra}\n{code}"
+ s += self.text_indent + "\\end{scope}\n"
+
+ if hide:
+ s = "%" + s.replace("\n", "\n%")[:-1]
+ else:
+ s = code
+ return s
+
+ def _handle_image(self, node):
+ """Handles the image tag and returns tikz code"""
+ p = self.convert_unit_coord(Vector2d(node.left, node.top))
+
+ width = self.round_value(self.convert_unit(node.width))
+ height = self.round_value(self.convert_unit(node.height))
+
+ href = node.get("xlink:href")
+ isvalidhref = href is not None and "data:image/png;base64" not in href
+ if not isvalidhref:
+ href = "base64 still not supported"
+ return f"% Image {node.get_id()} not included. Base64 still not supported"
+
+ if self.options.latexpathtype:
+ href = href.replace(self.options.removeabsolute, "")
+
+ return (
+ r"\node[anchor=north west,inner sep=0, scale=\globalscale]"
+ + f" ({node.get_id()}) at {self.coord_to_tz(p)} "
+ + r"{\includegraphics[width="
+ + f"{width}{self.options.output_unit},height={height}{self.options.output_unit}]"
+ + "{"
+ + href
+ + "}}"
+ )
+
+ def convert_path_to_tikz(self, path):
+ """
+ Convert a path from inkex to tikz code
+ """
+ s = ""
+
+ for command in path.proxy_iterator():
+ letter = command.letter.upper()
+
+ # transform coords
+ tparams = self.convert_unit_coords(command.control_points)
+ # moveto
+ if letter == "M":
+ s += self.coord_to_tz(tparams[0])
+
+ # lineto
+ elif letter in ["L", "H", "V"]:
+ s += f" -- {self.coord_to_tz(tparams[0])}"
+
+ # cubic bezier curve
+ elif letter in ["C", "S"]:
+ s += f".. controls {self.coord_to_tz(tparams[0])} and {self.coord_to_tz(tparams[1])} .. {self.coord_to_tz(tparams[2])}"
+ # s_point = 2 * tparams[2] - tparams[1]
+
+ # quadratic bezier curve
+ elif letter == "Q":
+ # http://fontforge.sourceforge.net/bezier.html
+
+ # current_pos is qp0
+ qp1, qp2 = tparams
+ cp1 = current_pos + (2.0 / 3.0) * (qp1 - current_pos)
+ cp2 = cp1 + (qp2 - current_pos) / 3.0
+ s += f" .. controls {self.coord_to_tz(cp1)} and {self.coord_to_tz(cp2)} .. {self.coord_to_tz(qp2)}"
+ # close path
+ elif letter == "Z":
+ s += " -- cycle"
+ # arc
+ elif letter == "A":
+ # Do not shift other values
+ command = command.to_absolute()
+
+ r = Vector2d(
+ self.convert_unit(command.rx), self.convert_unit(command.ry)
+ )
+ # Get acces to this vect2D ?
+ pos = Vector2d(command.x, command.y)
+ pos = self.convert_unit_coord(pos)
+ sweep = command.sweep
+
+ if not self.options.noreversey:
+ current_pos.y = self.update_height(current_pos.y)
+ pos.y = self.update_height(pos.y)
+
+ start_ang_o, end_ang_o, r = calc_arc(
+ current_pos,
+ r,
+ command.x_axis_rotation,
+ command.large_arc,
+ sweep,
+ pos,
+ )
+
+ r = self.round_coord(r)
+ if not self.options.noreversey:
+ r.y *= -1
+
+ # For Pgf 2.0
+ start_ang, end_ang = self.sanitize_angles(start_ang_o, end_ang_o)
+
+ if not self.options.noreversey:
+ command.x_axis_rotation *= -1
+
+ ang = self.round_value(command.x_axis_rotation)
+ if r.x == r.y:
+ # Todo: Transform radi
+ radi = f"{r.x}"
+ else:
+ radi = f"{r.x} and {r.y}"
+ if ang != 0.0:
+ s += (
+ "{" + f"[rotate={ang}] arc({start_ang}"
+ f":{end_ang}:{radi})" + "}"
+ )
+ else:
+ s += f"arc({start_ang}:{end_ang}:{radi})"
+ # Get the last position
+ current_pos = tparams[-1]
+ return s
+
+ def _handle_shape(self, node):
+ """Extract shape data from node"""
+ options = []
+ if node.TAG == "rect":
+ inset = node.rx or node.ry
+ x = node.left
+ y = node.top
+ corner_a = self.convert_unit_coord(Vector2d(x, y))
+
+ width = node.width
+ height = node.height
+
+ # map from svg to tikz
+ if width == 0.0 or height == 0.0:
+ return "", []
+
+ corner_b = self.convert_unit_coord(Vector2d(x + width, y + height))
+
+ if inset and abs(inset) > 1e-5:
+ unit_to_scale = self.round_value(self.convert_unit(inset))
+ options = [f"rounded corners={unit_to_scale}{self.options.output_unit}"]
+
+ return (
+ f"{self.coord_to_tz(corner_a)} rectangle {self.coord_to_tz(corner_b)}",
+ options,
+ )
+
+ if node.TAG in ["polyline", "polygon"]:
+ points = node.get_path().control_points
+ points = self.round_coords(self.convert_unit_coords(points))
+ points = [f"({vec.x}, {vec.y})" for vec in points]
+
+ path = " -- ".join(points)
+
+ if node.TAG == "polygon":
+ path += "-- cycle"
+
+ return f"{path};", []
+
+ if node.TAG == "line":
+ p_a = self.convert_unit_coord(Vector2d(node.x1, node.y1))
+ p_b = self.convert_unit_coord(Vector2d(node.x2, node.y2))
+ # check for zero lenght line
+ if not ((p_a[0] == p_b[0]) and (p_a[1] == p_b[1])):
+ return f"{self.coord_to_tz(p_a)} -- {self.coord_to_tz(p_b)}", []
+
+ if node.TAG == "circle":
+ center = self.convert_unit_coord(Vector2d(node.center.x, node.center.y))
+
+ r = self.round_value(self.convert_unit(node.radius))
+ if r > 0.0:
+ return (
+ f"{self.coord_to_tz(center)} circle ({r}{self.options.output_unit})",
+ [],
+ )
+
+ if node.TAG == "ellipse":
+ center = Vector2d(node.center.x, node.center.y)
+ center = self.round_coord(self.convert_unit_coord(center))
+ r = self.round_coord(self.convert_unit_coord(node.radius, False))
+ if r.x > 0.0 and r.y > 0.0:
+ return (
+ f"{self.coord_to_tz(center)} ellipse ({r.x}{self.options.output_unit} and {r.y}{self.options.output_unit})",
+ [],
+ )
+
+ return "", []
+
+ def _find_attribute_in_hierarchy(self, current, attr):
+ """Try to find the attribute with the given name in the current node or any of its parents.
+ If the attribute is found, return its value, otherwise None."""
+ while current is not None:
+ value = current.get(attr)
+ if value:
+ return value
+ return self._find_attribute_in_hierarchy(current.getparent(), attr)
+
+ return None
+
+ def _handle_text(self, node):
+ if self.options.ignore_text:
+ return ""
+
+ raw_textstr = node.get_text(" ").strip()
+ mode = self.options.texmode
+
+ if mode == "attribute":
+ attribute = self._find_attribute_in_hierarchy(
+ node, self.options.texmode_attribute
+ )
+ if attribute:
+ mode = attribute
+
+ if mode == "raw":
+ textstr = raw_textstr
+ elif mode == "math":
+ textstr = f"${raw_textstr}$"
+ else:
+ textstr = escape_texchars(raw_textstr)
+
+ shape = self.get_shape_inside(node)
+ if shape is None:
+ p = Vector2d(node.x, node.y)
+ else: # pragma: no cover
+ # TODO Not working yet
+ p = Vector2d(shape.left, shape.bottom)
+
+ # We need to apply a rotation to coord
+ # In tikz rotate only rotate the node, not its coordinate
+ ang = 0.0
+ trans = node.transform
+ if trans.is_rotate():
+ # get angle
+ ang = atan2(trans.b, trans.a)
+ p = self.convert_unit_coord(self.rotate_coord(p, ang))
+
+ # scale do not impact node
+ if self.options.noreversey:
+ p.y *= -1
+
+ return f"({node.get_id()}) at {self.coord_to_tz(p)}" + "{" + f"{textstr}" + "}"
+
+ def get_text(self, node):
+ """Return content of a text node as string"""
+ return etree.tostring(node, method="text").decode("utf-8")
+
+ # pylint: disable=too-many-branches
+ def _output_group(self, group):
+ """Process a group of SVG nodes and return corresponding TikZ code
+
+ The group is processed recursively if it contains sub groups.
+ """
+ string = ""
+ for node in group:
+ if not filter_tag(node):
+ continue
+
+ if node.TAG == "use":
+ node = node.unlink()
+
+ if node.TAG == "switch":
+ string += self._handle_switch(node)
+ continue
+
+ if node.TAG == "g":
+ string += self._handle_group(node)
+ continue
+ try:
+ goptions = self.style_to_tz(node) + self.trans_to_tz(
+ node, node.TAG in ["text", "flowRoot", "image"]
+ )
+ except AttributeError as msg:
+ attr = msg.args[0].split("attribute")[1].split(".")[0]
+ logging.warning("%s attribute cannot be represented", attr)
+
+ pathcode = ""
+
+ if self.options.verbose:
+ string += self.text_indent + f"%{node.get_id()}\n"
+
+ if node.TAG == "path":
+ optionscode = options_to_str(goptions)
+
+ pathcode = f"\\path{optionscode} {self.convert_path_to_tikz(node.path)}"
+
+ elif node.TAG in LIST_OF_SHAPES:
+ # Add indent
+ pathcode, options = self._handle_shape(node)
+
+ optionscode = options_to_str(goptions + options)
+
+ pathcode = f"\\path{optionscode} {pathcode}"
+
+ elif node.TAG in ["text", "flowRoot"]:
+ pathcode = self._handle_text(node)
+
+ # Check if the anchor is set, otherwise default to south west
+ contains_anchor = False
+ for goption in goptions:
+ if goption.startswith("anchor="):
+ contains_anchor = True
+ if not contains_anchor:
+ goptions += ["anchor=south west"]
+
+ optionscode = options_to_str(goptions)
+ # Convert a rotate around to a rotate option
+ if "rotate around={" in optionscode:
+ splited_options = optionscode.split("rotate around={")
+ ang = splited_options[1].split(":")[0]
+ optionscode = (
+ splited_options[0]
+ + f"rotate={ang}"
+ + splited_options[1].split("}", 1)[1]
+ )
+
+ pathcode = f"\\node{optionscode} {pathcode}"
+
+ elif node.TAG == "image":
+ pathcode = self._handle_image(node)
+
+ # elif node.TAG == "symbol":
+ # # to implement: handle symbol as reusable code
+ # pass
+
+ else:
+ logging.debug("Unhandled element %s", node.tag)
+ continue
+
+ if self.options.wrap:
+ string += "\n".join(
+ wrap(
+ self.text_indent + pathcode,
+ 80,
+ subsequent_indent=" ",
+ break_long_words=False,
+ drop_whitespace=False,
+ replace_whitespace=False,
+ )
+ )
+ else:
+ string += self.text_indent + pathcode
+
+ string += ";\n\n\n\n"
+
+ return string
+
+ def effect(self):
+ """Apply the conversion on the svg and fill the template"""
+ string = ""
+
+ if not self.options.indent:
+ self.text_indent = ""
+
+ root = self.document.getroot()
+ if "height" in root.attrib:
+ self.height = self.convert_unit(self.svg.viewbox_height)
+ nodes = self.svg.selected
+ # If no nodes is selected convert whole document.
+
+ if len(nodes) == 0:
+ nodes = root
+
+ # Recursively process list of nodes or root node
+ string = self._output_group(nodes)
+
+ # Add necessary boiling plate code to the generated TikZ code.
+ codeoutput = self.options.codeoutput
+ if not self.options.crop:
+ cropcode = ""
+ else:
+ cropcode = CROP_TEMPLATE
+ if codeoutput == "standalone":
+ output = STANDALONE_TEMPLATE % {
+ "pathcode": string,
+ "colorcode": self.color_code,
+ "unit": self.options.output_unit,
+ "ysign": "-" if self.options.noreversey else "",
+ "cropcode": cropcode,
+ "gradientcode": self.gradient_code,
+ "scale": self.options.scale,
+ }
+ elif codeoutput == "figonly":
+ output = FIG_TEMPLATE % {
+ "pathcode": string,
+ "colorcode": self.color_code,
+ "unit": self.options.output_unit,
+ "ysign": "-" if self.options.noreversey else "",
+ "gradientcode": self.gradient_code,
+ "scale": self.options.scale,
+ }
+ else:
+ output = string
+
+ self.output_code = output
+ if self.options.returnstring:
+ return output
+ return ""
+
+ def save_raw(self, _):
+ """Save the file from the save as menu from inkscape"""
+ if self.options.clipboard: # pragma: no cover
+ success = copy_to_clipboard(self.output_code.encode("utf8"))
+ if not success:
+ logging.error("Failed to put output on clipboard")
+
+ elif self.options.output is not None:
+ if isinstance(self.options.output, str):
+ with codecs.open(self.options.output, "w", "utf8") as stream:
+ stream.write(self.output_code)
+ else:
+ out = self.output_code
+
+ if isinstance(self.options.output, (io.BufferedWriter, io.FileIO)):
+ out = out.encode("utf8")
+
+ self.options.output.write(out)
+
+ def run(self, args=None, output=SYS_OUTPUT_BUFFER):
+ """
+ Custom inkscape entry point to remove agr processing
+ """
+ try:
+ # We parse it ourself in command line but letting it with inkscape
+ if not self.args_parsed: # pragma: no cover
+ if args is None:
+ args = sys.argv[1:]
+
+ self.parse_arguments(args)
+
+ if (
+ isinstance(self.options.input_file, str)
+ and "DOCUMENT_PATH" not in os.environ
+ ):
+ os.environ["DOCUMENT_PATH"] = self.options.input_file
+
+ if self.options.output is None:
+ self.options.output = output
+ self.load_raw()
+ self.save_raw(self.effect())
+ except inkex.utils.AbortExtension as err: # pragma: no cover
+ inkex.utils.errormsg(str(err))
+ sys.exit(inkex.utils.ABORT_STATUS)
+ finally:
+ self.clean_up()
+
+ def convert(self, svg_file=None, no_output=False, **kwargs):
+ """Convert SVG file to tikz path"""
+ self.options = self.arg_parser.parse_args()
+ self.args_parsed = True
+
+ # Update args before everything else
+ self.options.__dict__.update(kwargs)
+
+ # Get version
+ if self.options.printversion:
+ print_version_info()
+ return ""
+
+ # if attribute mode, texmode_attribute should not be None
+ if (
+ self.options.texmode == "attribute"
+ and self.options.texmode_attribute is None
+ ):
+ print("Need to specify a texmode attribute with --texmode-attribute")
+ return ""
+
+ # Updating input source
+ if svg_file is not None:
+ self.options.input_file = svg_file
+
+ # If there is no file, end the code
+ if self.options.input_file is None:
+ print("No input file -- aborting")
+ return ""
+
+ # Run the code
+ if no_output:
+ self.run(output=None)
+ else:
+ self.run()
+
+ # Return the output if necessary
+ if self.options.returnstring:
+ return self.output_code
+ return ""
+
+
+def convert_file(svg_file, no_output=True, returnstring=True, **kwargs):
+ """
+ Convert SVG file to tikz code
+
+ :param svg_file: input file representend by a path or a stream
+ :type svg_file: str, stream object
+ :param no_output: If the output is redirected to None (default: True)
+ :type no_output: Bool
+ :param returnstring: if the output code should be returned
+ :type returnstring: Bool
+ :param kwargs: See argparse output / svg2tikz -h / commandline documentation
+ :return: tikz code or empty string
+ :rtype: str
+ """
+
+ kwargs["returnstring"] = returnstring
+ effect = TikZPathExporter(inkscape_mode=False)
+ return effect.convert(svg_file, no_output, **kwargs)
+
+
+def convert_svg(svg_source, no_output=True, returnstring=True, **kwargs):
+ """
+ Convert SVG to tikz code
+
+ :param svg_source: content of svg file
+ :type svg_source: str
+ :param no_output: If the output is redirected to None (default: True)
+ :type no_output: Bool
+ :param returnstring: if the output code should be returned
+ :type returnstring: Bool
+ :param kwargs: See argparse output / svg2tikz -h / commandline documentation
+ :return: tikz code or empty string
+ :rtype: str
+ """
+
+ kwargs["returnstring"] = returnstring
+ effect = TikZPathExporter(inkscape_mode=False)
+ return effect.convert(io.StringIO(svg_source), no_output, **kwargs)
+
+
+def main_inkscape(): # pragma: no cover
+ """Inkscape interface"""
+ # Create effect instance and apply it.
+ effect = TikZPathExporter(inkscape_mode=True)
+ effect.run()
+
+
+def print_version_info():
+ """Print the version of svg2tikz"""
+ print(f"svg2tikz version {__version__}")
+
+
+def main_cmdline(**kwargs): # pragma: no cover
+ """Main command line interface"""
+ effect = TikZPathExporter(inkscape_mode=False)
+ effect.convert(**kwargs)
+
+
+if __name__ == "__main__": # pragma: no cover
+ main_inkscape()
diff --git a/inkscape/extensions/tikz_export_effect.inx b/inkscape/extensions/tikz_export_effect.inx
new file mode 100644
index 0000000..08d9286
--- /dev/null
+++ b/inkscape/extensions/tikz_export_effect.inx
@@ -0,0 +1,90 @@
+
+
+ Export to TikZ path v3.2.1
+ net.texample.tools.svg.export_tikz.effect
+ tikz_export.py
+
+
+
+ none
+ false
+
+
+
+
+
+
+
+ false
+ true
+ true
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+ 1
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+ all
+
+
+
+
+
+
diff --git a/inkscape/extensions/tikz_export_output.inx b/inkscape/extensions/tikz_export_output.inx
new file mode 100644
index 0000000..a0ef488
--- /dev/null
+++ b/inkscape/extensions/tikz_export_output.inx
@@ -0,0 +1,87 @@
+
+
+ Export as TikZ code for use with LaTeX v3.2.1
+ net.texample.tools.svg.export_tikz.output
+ tikz_export.py
+
+
+
+
+
+
+
+
+ false
+ true
+ true
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+ 1
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+ output
+
+
+
diff --git a/inkscape/extensions/update_svg2tikz.sh b/inkscape/extensions/update_svg2tikz.sh
new file mode 100644
index 0000000..20e4664
--- /dev/null
+++ b/inkscape/extensions/update_svg2tikz.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+RAW_GITHUB_URL="https://raw.githubusercontent.com/xyz2tex/svg2tikz/refs/heads/master/svg2tikz"
+INKSCAPE_EXTENSIONS="$HOME/.config/inkscape/extensions"
+FILES="tikz_export.py tikz_export_effect.inx tikz_export_output.inx"
+
+for file in $FILES; do
+ wget -qO "$INKSCAPE_EXTENSIONS/$file" "$RAW_GITHUB_URL/$file"
+done