382 lines
13 KiB
Python
Executable file
382 lines
13 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# 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()
|