diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
commit | cca66b9ec4e494c1d919bff0f71a820d8afab1fa (patch) | |
tree | 146f39ded1c938019e1ed42d30923c2ac9e86789 /share/extensions/dpiswitcher.py | |
parent | Initial commit. (diff) | |
download | inkscape-cca66b9ec4e494c1d919bff0f71a820d8afab1fa.tar.xz inkscape-cca66b9ec4e494c1d919bff0f71a820d8afab1fa.zip |
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'share/extensions/dpiswitcher.py')
-rwxr-xr-x | share/extensions/dpiswitcher.py | 384 |
1 files changed, 384 insertions, 0 deletions
diff --git a/share/extensions/dpiswitcher.py b/share/extensions/dpiswitcher.py new file mode 100755 index 0000000..48c2c6e --- /dev/null +++ b/share/extensions/dpiswitcher.py @@ -0,0 +1,384 @@ +#!/usr/bin/env python +# coding=utf-8 +# +# Copyright (C) 2012 Jabiertxo Arraiza, jabier.arraiza@marker.es +# Copyright (C) 2016 su_v, <suv-sf@users.sf.net> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +""" +Version 0.6 - DPI Switcher + +This extension scales a document to fit different SVG DPI -90/96- + +Changes since v0.5: + - transform all top-level containers and graphics elements + - support scientific notation in SVG lengths + - fix scaling with existing matrix() + - support different units for document width, height attributes + - improve viewBox support (syntax, offset) + - support common cases of text-put-on-path in SVG root + - support common cases of <use> references in SVG root + - examples from http://tavmjong.free.fr/INKSCAPE/UNITS/ tested + +TODO: + - check grids/guides created with 0.91: + http://tavmjong.free.fr/INKSCAPE/UNITS/units_mm_nv_90dpi.svg + - check <symbol> instances + - check more <use> and text-on-path cases (reverse scaling needed?) + - scale perspective of 3dboxes + +""" + +import re +import math +import inkex +from inkex import Use, TextElement, transforms + +# globals +SKIP_CONTAINERS = [ + "defs", + "glyph", + "marker", + "mask", + "missing-glyph", + "pattern", + "symbol", +] +CONTAINER_ELEMENTS = [ + "a", + "g", + "switch", +] +GRAPHICS_ELEMENTS = [ + "circle", + "ellipse", + "image", + "line", + "path", + "polygon", + "polyline", + "rect", + "text", + "use", +] + + +def is_3dbox(element): + """Check whether element is an Inkscape 3dbox type.""" + return element.get("sodipodi:type") == "inkscape:box3d" + + +def is_text_on_path(element): + """Check whether text element is put on a path.""" + if isinstance(element, TextElement): + text_path = element.find("svg:textPath") + if text_path is not None and len(text_path): + return True + return False + + +def is_sibling(element1, element2): + """Check whether element1 and element2 are siblings of same parent.""" + return element2 in element1.getparent() + + +def is_in_defs(doc, element): + """Check whether element is in defs.""" + if element is not None: + defs = doc.find("defs") + if defs is not None: + return element in defs.iterdescendants() + return False + + +def check_3dbox(svg, element, scale_x, scale_y): + """Check transformation for 3dbox element.""" + skip = False + if skip: + # 3dbox elements ignore preserved transforms + # FIXME: manually update geometry of 3dbox? + pass + return skip + + +def check_text_on_path(svg, element, scale_x, scale_y): + """Check whether to skip scaling a text put on a path.""" + skip = False + path = element.find("textPath").href + if not is_in_defs(svg, path): + if is_sibling(element, path): + # skip common element scaling if both text and path are siblings + skip = True + # scale offset + if "transform" in element.attrib: + element.transform.add_scale(scale_x, scale_y) + # scale font size + mat = inkex.Transform("scale({},{})".format(scale_x, scale_y)).matrix + det = abs(mat[0][0] * mat[1][1] - mat[0][1] * mat[1][0]) + descrim = math.sqrt(abs(det)) + prop = "font-size" + # outer text + sdict = dict(inkex.Style.parse_str(element.get("style"))) + if prop in sdict: + sdict[prop] = float(sdict[prop]) * descrim + element.set("style", str(inkex.Style(sdict))) + # inner tspans + for child in element.iterdescendants(): + if isinstance(element, inkex.Tspan): + sdict = dict(inkex.Style.parse_str(child.get("style"))) + if prop in sdict: + sdict[prop] = float(sdict[prop]) * descrim + child.set("style", str(inkex.Style(sdict))) + return skip + + +def check_use(svg, element, scale_x, scale_y): + """Check whether to skip scaling an instantiated element (<use>).""" + skip = False + path = element.href + if not is_in_defs(svg, path): + if is_sibling(element, path): + skip = True + # scale offset + if "transform" in element.attrib: + element.transform.add_scale(scale_x, scale_y) + return skip + + +class DPISwitcher(inkex.EffectExtension): + multi_inx = True + factor_a = 90.0 / 96.0 + factor_b = 96.0 / 90.0 + units = "px" + + def add_arguments(self, pars): + pars.add_argument( + "--switcher", type=str, default="0", help="Select the DPI switch you want" + ) + + # dictionaries of unit to user unit conversion factors + __uuconvLegacy = { + "in": 90.0, + "pt": 1.25, + "px": 1.0, + "mm": 3.5433070866, + "cm": 35.433070866, + "m": 3543.3070866, + "km": 3543307.0866, + "pc": 15.0, + "yd": 3240.0, + "ft": 1080.0, + } + __uuconv = { + "in": 96.0, + "pt": 1.33333333333, + "px": 1.0, + "mm": 3.77952755913, + "cm": 37.7952755913, + "m": 3779.52755913, + "km": 3779527.55913, + "pc": 16.0, + "yd": 3456.0, + "ft": 1152.0, + } + + def parse_length(self, length, percent=False): + """Parse SVG length.""" + if self.options.switcher == "0": # dpi90to96 + known_units = list(self.__uuconvLegacy) + else: # dpi96to90 + known_units = list(self.__uuconv) + if percent: + unitmatch = re.compile("(%s)$" % "|".join(known_units + ["%"])) + else: + unitmatch = re.compile("(%s)$" % "|".join(known_units)) + param = re.compile( + r"(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)" + ) + p = param.match(length) + u = unitmatch.search(length) + val = 100 # fallback: assume default length of 100 + unit = "px" # fallback: assume 'px' unit + if p: + val = float(p.string[p.start() : p.end()]) + if u: + unit = u.string[u.start() : u.end()] + return val, unit + + def convert_length(self, val, unit): + """Convert length to self.units if unit differs.""" + doc_unit = self.units or "px" + if unit != doc_unit: + if self.options.switcher == "0": # dpi90to96 + val_px = val * self.__uuconvLegacy[unit] + val = val_px / ( + self.__uuconvLegacy[doc_unit] / self.__uuconvLegacy["px"] + ) + unit = doc_unit + else: # dpi96to90 + val_px = val * self.__uuconv[unit] + val = val_px / (self.__uuconv[doc_unit] / self.__uuconv["px"]) + unit = doc_unit + return val, unit + + def check_attr_unit(self, element, attr, unit_list): + """Check unit of attribute value, match to units in *unit_list*.""" + if attr in element.attrib: + unit = self.parse_length(element.get(attr), percent=True)[1] + return unit in unit_list + + def scale_attr_val(self, element, attr, unit_list, factor): + """Scale attribute value if unit matches one in *unit_list*.""" + if attr in element.attrib: + val, unit = self.parse_length(element.get(attr), percent=True) + if unit in unit_list: + element.set(attr, "{}{}".format(val * factor, unit)) + + def scale_root(self, unit_exponent=1.0): + """Scale all top-level elements in SVG root.""" + + # update viewport + width_num = self.parse_length(self.svg.get("width"))[0] + height_num = self.convert_length(*self.parse_length(self.svg.get("height")))[0] + width_doc = width_num * self.factor_a * unit_exponent + height_doc = height_num * self.factor_a * unit_exponent + + svg = self.svg + if svg.get("height"): + svg.set("height", str(height_doc)) + if svg.get("width"): + svg.set("width", str(width_doc)) + + # update viewBox + if svg.get("viewBox"): + viewboxstring = re.sub(" +|, +|,", " ", svg.get("viewBox")) + viewboxlist = [float(i) for i in viewboxstring.strip().split(" ", 4)] + svg.set( + "viewBox", + "{} {} {} {}".format(*[(val * self.factor_a) for val in viewboxlist]), + ) + + # update guides, grids + if self.options.switcher == "1": + # FIXME: dpi96to90 only? + self.scale_guides() + self.scale_grid() + + for element in svg: # iterate all top-level elements of SVGRoot + + # init variables + tag = element.TAG + width_scale = self.factor_a + height_scale = self.factor_a + + if tag in GRAPHICS_ELEMENTS or tag in CONTAINER_ELEMENTS: + + # test for specific elements to skip from scaling + if is_3dbox(element): + if check_3dbox(svg, element, width_scale, height_scale): + continue + if is_text_on_path(element): + if check_text_on_path(svg, element, width_scale, height_scale): + continue + if isinstance(element, Use): + if check_use(svg, element, width_scale, height_scale): + continue + + # relative units ('%') in presentation attributes + for attr in ["width", "height"]: + self.scale_attr_val(element, attr, ["%"], 1.0 / self.factor_a) + for attr in ["x", "y"]: + self.scale_attr_val(element, attr, ["%"], 1.0 / self.factor_a) + + # set preserved transforms on top-level elements + if width_scale != 1.0 and height_scale != 1.0: + scale_mat = transforms.Transform(scale=(width_scale, height_scale)) + element.transform = scale_mat @ element.transform + + def scale_element(self, elem): + pass # TODO: optionally scale graphics elements only? + + def scale_guides(self): + """Scale the guidelines""" + for guide in self.svg.namedview.get_guides(): + point = guide.get("position").split(",") + guide.set( + "position", + str(float(point[0].strip()) * self.factor_a) + + "," + + str(float(point[1].strip()) * self.factor_a), + ) + + def scale_grid(self): + """Scale the inkscape grid""" + grids = self.svg.xpath("//inkscape:grid") + for grid in grids: + grid.set("units", "px") + if grid.get("spacingx"): + spacingx = ( + str( + float(re.sub("[a-zA-Z]", "", grid.get("spacingx"))) + * self.factor_a + ) + + "px" + ) + grid.set("spacingx", str(spacingx)) + if grid.get("spacingy"): + spacingy = ( + str( + float(re.sub("[a-zA-Z]", "", grid.get("spacingy"))) + * self.factor_a + ) + + "px" + ) + grid.set("spacingy", str(spacingy)) + if grid.get("originx"): + originx = ( + str( + float(re.sub("[a-zA-Z]", "", grid.get("originx"))) + * self.factor_a + ) + + "px" + ) + grid.set("originx", str(originx)) + if grid.get("originy"): + originy = ( + str( + float(re.sub("[a-zA-Z]", "", grid.get("originy"))) + * self.factor_a + ) + + "px" + ) + grid.set("originy", str(originy)) + + def effect(self): + svg = self.svg + if self.options.switcher == "0": + self.factor_a = 96.0 / 90.0 + self.factor_b = 90.0 / 96.0 + svg.namedview.set("inkscape:document-units", "px") + self.units = self.parse_length(svg.get("width"))[1] + unit_exponent = 1.0 + if self.units and self.units != "px" and self.units != "" and self.units != "%": + if self.options.switcher == "0": + unit_exponent = 1.0 / (self.factor_a / self.__uuconv[self.units]) + else: + unit_exponent = 1.0 / (self.factor_a / self.__uuconvLegacy[self.units]) + self.scale_root(unit_exponent) + + +if __name__ == "__main__": + DPISwitcher().run() |