Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 108 additions & 10 deletions inkcut/core/svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@
ElementType = QPainterPath.ElementType
EtreeElement = etree._Element

# Inkcut assumes 90DPI for its internal units
# It's an odd choice, but it's fine as long as it's consistent
# throughout.
INKCUT_DPI = 90.0

class QtSvgItem(QPainterPath):
tag = None
_nodes = None
_uuconv = {'in': 90.0, 'pt': 1.25, 'px': 1, 'mm': 3.5433070866,
_uuconv = {'in': INKCUT_DPI, 'pt': 1.25, 'px': 1, 'mm': 3.5433070866,
'cm': 35.433070866, 'm': 3543.3070866,
'km': 3543307.0866, 'pc': 15.0, 'yd': 3240, 'ft': 1080}

Expand Down Expand Up @@ -141,6 +145,17 @@ def parseUnit(value):
pass
return retval

@staticmethod
def getUnit(value):
if not value:
return None
unit = re.compile('(%s)$' % '|'.join(QtSvgItem._uuconv.keys()))
u = unit.search(value)
if u:
return u.string[u.start():u.end()]
else:
return None

@staticmethod
def convertToUnit(val, unit='px'):
""" Convert from px to given unit """
Expand Down Expand Up @@ -685,7 +700,7 @@ class QtSvgSymbol(QtSvgG):
class QtSvgDoc(QtSvgG):
tag = "{http://www.w3.org/2000/svg}svg"

def __init__(self, e, ids=None, parent=False):
def __init__(self, e, ids=None, parent=False, dpi_default=96.0, dpi_auto_detect_inkscape=True):
"""
Creates a QtPainterPath from an SVG document applying all transforms.

Expand All @@ -698,6 +713,9 @@ def __init__(self, e, ids=None, parent=False):
ids: List
List of node ids to include. If not given all will be used.
"""
self.dpi_default = dpi_default
self.dpi_auto_detect_inkscape = dpi_auto_detect_inkscape

is_etree = isinstance(e, EtreeElement)
self.isParentSvg = parent or not is_etree
if self.isParentSvg:
Expand Down Expand Up @@ -726,29 +744,109 @@ def __init__(self, e, ids=None, parent=False):
self.viewBox = QRectF(0, 0, -1, -1)
super(QtSvgDoc, self).__init__(e, self._nodes)

# Return the DPI of the inkscape document detected
# based on the version information in the SVG
# or None if we could not definitely identify it
# as an Inkscape document.
@staticmethod
def dpiDetectFromInkscapeVersionAttribute(e):
inkscapeVersionAttribute = e.attrib.get("{http://www.inkscape.org/namespaces/inkscape}version", None)
if not inkscapeVersionAttribute:
return None
inkscapeVersionAttributeSplit = inkscapeVersionAttribute.split(" ")
if len(inkscapeVersionAttributeSplit) <= 0:
return None
inkscapeVersion = inkscapeVersionAttributeSplit[0]
inkscapeVersionSplit = inkscapeVersion.split(".")
# This try block is just in case we encounter a non-integer
# in one of the major or minor versions. In this case,
# we can't reliably detect the DPI based on unit, so we need to
# return None.
try:
# It's at least 2 digits, so pick out the major and minor versions.
if (len(inkscapeVersionSplit) >= 2):
inkscapeMajorVersion = int(inkscapeVersionSplit[0])
inkscapeMinorVersion = int(inkscapeVersionSplit[1])
if (inkscapeMajorVersion == 0 and inkscapeMinorVersion < 92):
# Inkscape units are assumed to be 90dpi by older versions of Inkscape
# if they are not explicitly specified, so we scale appropriately
# for older documents.
return 90.0
else:
return 96.0
elif (len(inkscapeVersionSplit) >= 1):
inkscapeMajorVersion = int(inkscapeVersionSplit[0])
if (inkscapeMajorVersion >= 1):
return 96.0
else:
# Not enough version information to make a correct decision,
# so we must return None.
return None
except ValueError:
# Version could not be parsed, assume it's a really old one
# at 90dpi.
return None
return None

def parseTransform(self, e):
t = QTransform()
# transforms don't apply to the root svg element, but we do need to
# take into account the viewBox there
if self.isParentSvg:
# Establish the default units.
# Inkscape assumes 96dpi if units are not explicitly given
# because this is specified in the CSS specification.

# If we're detecting DPI based on Inkscape
# defaults for various versions, go ahead and do that.
# Otherwise, we'll fall back to the configuration default DPI setting.
dpi = self.dpi_default
if self.dpi_auto_detect_inkscape:
dpi_detected = QtSvgDoc.dpiDetectFromInkscapeVersionAttribute(e)
# If we detected something, use it.
# otherwise, we just use the system default
if dpi_detected:
dpi = dpi_detected

xunit = QtSvgItem.getUnit(e.attrib.get("width", None))
xscale = 1.0
if not xunit:
xscale = INKCUT_DPI / dpi

yunit = QtSvgItem.getUnit(e.attrib.get("height", None))
yscale = 1.0
if not yunit:
yscale = INKCUT_DPI / dpi

# I think perhaps we need to keep this one
# because otherwise we won't correctly convert
# the units inside the document.
viewBox = e.attrib.get('viewBox', None)
x = 0
y = 0
if viewBox is not None:
# If there is a viewbox, we
# need to translate the origin, scale,
# and then translate it back in order to
# preserve the intended structure of the given SVG
# document.
(x, y, innerWidth, innerHeight) = map(self.parseUnit,
re.split("[ ,]+",
viewBox))

if x != 0 or y != 0:
raise ValueError(
"viewBox '%s' needs to be translated "
"because is not at the origin. "
"See https://github.com/codelv/inkcut/issues/69"
% viewBox)

outerWidth, outerHeight = map(self.parseUnit,
(e.attrib.get('width', None),
e.attrib.get('height', None)))
if outerWidth is not None and outerHeight is not None:
t.scale(outerWidth / innerWidth, outerHeight / innerHeight)
xscale = xscale * outerWidth / innerWidth
yscale = yscale * outerHeight / innerHeight

# First, translate the origin if the viewbox gives us one.
t.translate(-x*xscale, -y*yscale)
# Next, scale the drawing to the viewport and DPI because
# the remaining elements of the drawing will be based on
# the viewbox unit size
t.scale(xscale, yscale)
else:
x, y = map(self.parseUnit, (e.attrib.get('x', 0),
e.attrib.get('y', 0)))
Expand Down
2 changes: 1 addition & 1 deletion inkcut/device/filters/min_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class MinLineConfig(Model):
#: Units for display
units = Enum(*unit_conversions.keys()).tag(config=True)

# measured in 1/90inch like most other inkcut distances
# measured in 1/90inch (1/INKCUT_DPI) like most other inkcut distances

# don't lift the pen for distnaces shorter than this
min_jump = Float(strict=False).tag(config=True)
Expand Down
2 changes: 1 addition & 1 deletion inkcut/device/filters/repeat.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

class RepeatConfig(Model):
steps = Int(1).tag(config=True)
# measured in 1/90inch like most other inkcut distances
# measured in 1/90inch (1/INKCUT_DPI) like most other inkcut distances
closed_loop_distance = Float(0.1, strict=False).tag(config=True)


Expand Down
4 changes: 2 additions & 2 deletions inkcut/device/protocols/dmpl.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"""
from atom.api import Enum, Instance, Float
from inkcut.device.plugin import DeviceProtocol, Model

from inkcut.core.svg import INKCUT_DPI

class DMPLConfig(Model):
#: Version number
Expand All @@ -22,7 +22,7 @@ class DMPLProtocol(DeviceProtocol):
config = Instance(DMPLConfig, ()).tag(config=True)

#: Output scaling
scale = Float(1021/90.0)
scale = Float(1021/INKCUT_DPI)

def connection_made(self):
v = self.config.mode
Expand Down
3 changes: 2 additions & 1 deletion inkcut/device/protocols/hpgl.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from atom.api import Instance, Float, Bool, Int
from inkcut.device.plugin import DeviceProtocol, Model
from inkcut.core.utils import log
from inkcut.core.svg import INKCUT_DPI


class HPGLConfig(Model):
Expand All @@ -15,7 +16,7 @@ class HPGLConfig(Model):


class HPGLProtocol(DeviceProtocol):
scale = Float(1021/90.0)
scale = Float(1021/INKCUT_DPI)

#: Pad option
config = Instance(HPGLConfig, ()).tag(config=True)
Expand Down
5 changes: 5 additions & 0 deletions inkcut/job/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,11 @@ def __setstate__(self, *args, **kwargs):
def _observe_document(self, change):
""" Read the document from stdin """
source = self.document
from inkcut.core.workbench import InkcutWorkbench
workbench = InkcutWorkbench.instance()
plugin = workbench.get_plugin("inkcut.job")
self.document_kwargs["dpi_default"] = plugin.dpi_default
self.document_kwargs["dpi_auto_detect_inkscape"] = plugin.dpi_auto_detect_inkscape
if change['type'] == 'update' and source == '-':
#: Only load from stdin when explicitly changed to it (when doing
#: open from the cli) otherwise when restoring state this hangs
Expand Down
8 changes: 7 additions & 1 deletion inkcut/job/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import os
import sys
import enaml
from atom.api import Instance, Enum, List, Str, Int, Float, observe
from atom.api import Instance, Enum, List, Str, Int, Float, observe, Bool
from inkcut.core.api import Plugin, unit_conversions, log

from .models import Job, JobError, Material
Expand Down Expand Up @@ -48,6 +48,12 @@ class JobPlugin(Plugin):
#: Timeout for optimizing paths
optimizer_timeout = Float(10, strict=False).tag(config=True)

# Default DPI setting if no units are specified in document.
dpi_default = Float(96, strict=False).tag(config=True)

# Whether or not to auto-detect DPI settings for Inkscape SVG files.
dpi_auto_detect_inkscape = Bool(True).tag(config=True)

def _default_job(self):
return Job(material=self.material)

Expand Down
25 changes: 24 additions & 1 deletion inkcut/job/settings.enaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ Created on May 25, 2019

@author: jrm
"""
from enaml.widgets.api import Container, Form, Label, ObjectCombo
import textwrap
from enaml.widgets.api import Container, Form, Label, ObjectCombo, CheckBox
from enaml.qt.QtWidgets import QApplication
from enamlx.widgets.api import DoubleSpinBox

Expand All @@ -23,6 +24,28 @@ enamldef JobSettingsPage(Container):
ObjectCombo:
items = list(sorted(model.get_member('units').items))
selected := model.units
Label:
text = QApplication.translate("settings", "Default DPI")
DoubleSpinBox:
suffix = ' dpi'
value := model.dpi_default
tool_tip = textwrap.dedent("""
When importing SVG files, if no units are specified,
how many units per inch should be assumed. Changing
this requires closing and re-opening the working document.
""").strip()
Label:
text = QApplication.translate("settings", "Auto-detect Inkscape DPI")
CheckBox:
text = QApplication.translate("settings", "Enabled")
checked := model.dpi_auto_detect_inkscape
tool_tip = textwrap.dedent("""
Use 96DPI for Inkscape 0.92 and above, or 90dpi for older versions.
Inkscape version is detected based on the 'inkscape:version' tag found
in the root <svg> element of the document. Overrides default DPI
if Inkscape document is detected. Changing this requires closing and
re-opening the working document.
""").strip()
Label:
text = QApplication.translate("settings", "Optimizer timeout")
DoubleSpinBox:
Expand Down
65 changes: 65 additions & 0 deletions tests/data/scale/ScaleTest-cm-with-viewbox.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading