#!/usr/bin/env python # coding=utf-8 # # Copyright (C) 2012 Jabiertxo Arraiza, jabier.arraiza@marker.es # Copyright (C) 2016 su_v, # # 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 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 instances - check more 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 ().""" 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()