diff --git a/docinfo.py b/docinfo.py
index 1c39b90a033014b7fbb9ca6be4e1e16bce1d83bf..64f1ef1a1a6f66af3dcada5f062de8b31d17c8c5 100755
--- a/docinfo.py
+++ b/docinfo.py
@@ -28,8 +28,8 @@ class DocInfo(inkex.EffectExtension):
namedview = self.svg.namedview
self.msg(":::SVG document related info:::")
self.msg("version: " + self.svg.get('inkscape:version', 'New Document (unsaved)'))
- self.msg("width: {}".format(self.svg.width))
- self.msg("height: {}".format(self.svg.height))
+ self.msg("width: {}".format(self.svg.viewport_width))
+ self.msg("height: {}".format(self.svg.viewport_height))
self.msg("viewbox: {}".format(str(self.svg.get_viewbox())))
self.msg("document-units: {}".format(namedview.get('inkscape:document-units', 'None')))
self.msg("units: " + namedview.get('units', 'None'))
diff --git a/docs/index.rst b/docs/index.rst
index 69c78adc41a05ef36d235efc57166c6fb1e5ae00..5d07f11258c6773b51292b1ee2d915362ab2aede 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -11,6 +11,7 @@ Welcome to inkex's documentation!
:caption: Quickstart
quickstart
+ units
.. toctree::
:maxdepth: 4
diff --git a/docs/samples/unit_camera.svg b/docs/samples/unit_camera.svg
new file mode 100644
index 0000000000000000000000000000000000000000..3a4bad4e6610e300bed2e1922dda3d39b3b5b841
--- /dev/null
+++ b/docs/samples/unit_camera.svg
@@ -0,0 +1,730 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ canvas (infinitely large)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 25 mm
+ 50 mm
+ 75 mm
+ 0 mm
+
+
+
+ 25 mm
+ 50 mm
+ 0 mm
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ viewport coordinate system
+
+
diff --git a/docs/samples/units1.svg b/docs/samples/units1.svg
new file mode 100644
index 0000000000000000000000000000000000000000..b221eda385673bfeec44538c21cea49ce4bc361a
--- /dev/null
+++ b/docs/samples/units1.svg
@@ -0,0 +1,4 @@
+
+
+
\ No newline at end of file
diff --git a/docs/samples/units2.svg b/docs/samples/units2.svg
new file mode 100644
index 0000000000000000000000000000000000000000..cf2fe1725bf09c5022e54bb697520a9481dd85e0
--- /dev/null
+++ b/docs/samples/units2.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/units.rst b/docs/units.rst
new file mode 100644
index 0000000000000000000000000000000000000000..d0e3c4238832cfab1d8cbff4400ca56ba844d395
--- /dev/null
+++ b/docs/units.rst
@@ -0,0 +1,282 @@
+Units
+=================
+
+If you are reading this page, you are probably confused about how units work in Inkscape, inkex and
+SVG in general. This is understandable, since there's quite a bit of conflicting information online.
+
+Units in SVG
+------------
+
+SVG means "scalable vector graphics". This introduces an inherent difficulty with how to map units
+to the real world. Should units be pixels? Millimeters? Something else? The answer to this depends
+on the output format you're targeting.
+
+The authors of the SVG specification solved this problem by introducing an abstract "dimensionless"
+unit called **user units**. The SVG1.1 specification [1]_ is quite clear about their definition:
+
+ *One px unit is defined to be equal to one user unit.
+ Thus, a length of "5px" is the same as a length of "5".*
+
+So whenever you read "user unit", think "pixel". And when you encounter a coordinate without unit,
+it's specified in user units, i.e. pixels. You might have heard or read something like "I can choose
+the unit of a document, so that one user unit equals one millimeter". This statement is misleading,
+although not entirely wrong. It will be explained below.
+
+An `` tag has two different properties that influence its size and the mapping of coordinates.
+These are called *viewport coordinate system* and *user coordinate system*.
+
+And as the name indicates, **user units always refer to the user coordinate system**. So for
+the next section, forget user units.
+
+Viewport coordinate system
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The viewport coordinate system is [2]_
+
+ *[...] a top-level SVG viewport that establishes a mapping between the coordinate system used by the
+ containing environment (for example, CSS pixels in web browsers) and user units.*
+
+The viewport coordinate system is established by the ``width`` and ``height`` attributes of the SVG tag.
+To reformulate the quote above: The viewport tells the SVG viewer how big the visible part of the
+canvas should be *rendered*. It may be ``200px x 100px`` on your screen (``width="200px" height="100px"``)
+or ``210mm x 297mm`` (``width="210mm" height="297mm"``), i.e. one A4 page.
+
+ *If the width or height presentation attributes on the outermost svg element are in user units
+ (i.e., no unit identifier has been provided), then the value is assumed to be equivalent to the
+ same number of "px" units.* [3]_
+
+Expressed in simple terms: if no unit has been specified in the ``width`` or ``height`` attributes,
+assume the user means pixels. Otherwise, the unit is converted by the SVG viewer. Inkscape uses a
+DPI of 96 px/in, and corresponding conversions for mm, yd etc. are used.
+
+Consider the following SVG file:
+
+.. code-block:: XML
+
+
+
+
+
+which renders as follows:
+
+.. image:: samples/units1.svg
+
+If your browser zoom is set to 100%, this image should have a size of 100 times 200 pixels,
+and is filled with a grey rectangle. You can verify this by taking a screenshot.
+
+Likewise, in ``mm`` based documents, you might see code such as
+``width="210mm" height="297mm"`` which tells an standard-compliant program that if printed or
+exported to PDF, the document should span an entire A4 page.
+
+User coordinate system
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+You may have noticed that we didn't explicitly specify in the above svg that we want to draw
+everything with coordinates ``0 ≤ x ≤ 200`` and ``0 ≤ y ≤ 100``. This was done for us automatically
+since we specified ``width`` and ``height``. The ``viewBox`` attribute allows to change this.
+
+Again from the specification [4]_:
+
+ *The effect of the viewBox attribute is that the user agent automatically supplies the
+ appropriate transformation matrix to map the specified rectangle in user coordinate system
+ to the bounds of a designated region (often, the SVG viewport).*
+
+Let's break this down. Imagine the ``viewBox`` attribute as a camera that moves over the infinite
+canvas. It can zoom in or out and move around - but whatever image the camera outputs, it is
+rendered in the rectangle defined by ``width`` and ``height``, i.e. the viewport. Initially, the
+camera is located such that the region ``viewBox="0 0 width height"`` is pictured. We may
+however modify the viewBox as we wish.
+
+In a ``mm`` based documents, where we specified ``width="210mm" height="297mm"``, the viewbox is
+initially ``viewBox="0 0 793.7 1122.5"`` due to the conversion from mm to px. This means that the
+bottom right corner is at ``(210, 297) mm * 1/25.4 in/mm * 96 px/in ≈ (793.7, 1122.5) px``.
+
+As already mentioned: no units means user unit means pixels. So a rectangle with
+``x="793.7" y="1122.5"`` (no units specified) is at the bottom right corner of the page. It would be
+nicer if unitless values would be implicitly in millimeters, so we could specify such a rectangle
+with ``x="210" y="297"``. This can be done with the ``viewBox`` attribute and will be explained with
+and example SVG.
+
+Let's say we want to design a business card that should eventually be *printed on 84mm x 56mm*, so
+we specifiy ``width="84mm" height="56mm"```. We also want the user units to behave like real-world
+millimeters, so we have to zoom the viewbox camera: ``viewBox="0 0 84 56"``. As mentioned above,
+no units means px, so these attributes together tell the SVG viewer "move the camera in such a way
+that (84, 56) in user units, i.e. px, is the bottom right corner, and scale the image such that when
+printed or rendered it has a size of 84mm by 56mm".
+
+You can imagine this situation like this [5]_:
+
+.. image:: samples/unit_camera.svg
+
+To illustrate this, we draw a crosshair at ``(14, 21)`` (note: no units in the path specification!),
+i.e. a fourth horizontally and vertically for reference. Then we draw three circles: one at
+``(21, 14)``, one at ``(21px, 14px)`` and one at ``(21mm, 14mm)``.
+
+.. code-block:: XML
+
+
+
+
+
+
+
+
+
+
+.. image:: samples/units2.svg
+
+The rendered image at 100% browser resolution should be approximatly ``85mm`` by ``56mm``, but this
+highly depends on your screen resolution.
+
+Note that the first two circles specified without unit
+(i.e. user units) and specified in px are at the correct position and identical except for radius
+and stroke color.
+
+The third circle's coordinates, radius and stroke-width are specified in mm. It should be located
+somewhere near the bottom right corner (where exactly depends on the DPI conversion of your browser,
+but most browsers use ``96dpi = 96 px/in`` today, which yields a conversion factor of approx.
+``3.77px/mm``). The stroke is thicker by the same factor and the radius has been reduced to be
+comparable to the first circle.
+
+This is somewhat unintuitive. Didn't we create a mm based document? Now we can explain the
+statement from the introduction
+"I can choose the unit of a document, so that one user unit equals one millimeter".
+We didnt change the core statement "no unit equals user unit equals pixels" by specifying width and
+height in mm. But the special choice of the viewbox attribute - the same width and height, but
+without the unit) makes the following statement true: "**One user unit looks like one millimeter on
+the output device** (e.g. screen or paper)".
+
+Now you understand why appending "mm" to the circle's position moved it. The transformation px->mm
+has been applied twice! Once in the coordinate specification itself, and once by the "camera".
+
+
+Units in Inkex
+-----------------
+
+As an extension autor, you may have four different questions regarding units.
+
+What is the position of this object [in the user coordinate system]?
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This is a question that typically needs to be answered if you want to position an object relative
+to other objects, whose coordinates may be specified in a different unit.
+
+The most conventient way to deal with this is to get rid of the units, and that means converting
+everything to user units.
+
+Each :class:`BaseElement ` has a method
+:meth:`unittouu `. This method parses a ``length`` value
+and returns it, converted to px (user units).
+
+In these and the following examples, the above "business card" SVG will be used.
+
+>>> svg = inkex.load_svg("docs/samples/units2.svg").getroot()
+>>> svg.unittouu(svg.getElementById("c1").get("cx"))
+21.0
+>>> svg.unittouu(svg.getElementById("c2").get("cx"))
+21.0
+>>> svg.unittouu(svg.getElementById("c3").get("cx"))
+79.370078
+
+For some classes, e.g. :class:`Rectangle `, convenience
+properties are available which do the conversion for you, e.g.
+:attr:`Rectangle.left `. Similarly there are some
+properties for circles:
+
+>>> svg.getElementById("c3").center
+Vector2d(79.3701, 52.9134)
+>>> svg.getElementById("c2").radius
+4.0
+
+What is the dimension of an object in a specified unit in the user coordinate system?
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+There are relatively few use cases for this, but if you want to, you can also convert from
+user units to any unit. This is done using
+:meth:`BaseElement.uutounit `.
+
+>>> svg.uutounit(svg.getElementById("c2").radius, "px")
+4.0
+>>> svg.uutounit(svg.getElementById("c2").radius, "mm")
+1.0583333333333333
+
+What is the dimension of an object on the viewport in arbitrary units?
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This is useful if you want to draw a property of a shape (for example its area) as text on the
+canvas, in a unit specified by the user. The default unit to convert to is px.
+
+The method for this is called :meth:`BaseElement.unit_to_viewport `.
+
+>>> svg.unit_to_viewport(svg.getElementById("c2").radius)
+15.118110236220472
+>>> svg.unit_to_viewport(svg.getElementById("c2").radius, "mm")
+4.0
+>>> svg.unit_to_viewport("4", "mm")
+4.0
+
+Obviously the element needs to know the viewport of its SVG document for this. This method therefore
+does not work if the element is unrooted.
+
+How big does an object have to be to have the specified size on the viewport?
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This is useful if you want to draw a shape at a given location on the viewport, regardless of
+what the user coordinate system is. This is done using
+:meth:`BaseElement.unit_to_viewport `.
+
+>>> svg.viewport_to_unit("4mm", "px")
+4.0
+>>> svg.viewport_to_unit("4mm", "mm")
+1.0583333333333333
+
+An example for this would be text elements. In order for text to show up in Inkscape's text
+tool as ``9pt``, you have to user
+
+>>> element.style["font-size"] = self.svg.viewport_to_unit("9pt")
+
+Again, this method does not work if the element is unrooted.
+
+
+Document dimensions
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+* :attr:`SvgDocumentElement.viewport_width `
+ and
+ :attr:`SvgDocumentElement.viewport_height `
+ are the width and height of the viewport coordinate system, i.e. the "output screen" of the
+ viewBox camera, in pixels. In above example: ``(317.480314, 211.653543)``
+* :attr:`SvgDocumentElement.viewbox_width `
+ and
+ :attr:`SvgDocumentElement.viewbox_height `
+ are the width and height of the user coordinate system, i.e. for a viewport without offset, the
+ largest ``x`` and ``y`` values that are visible to the viewport camera.
+ In above example: ``(84, 56)``
+
+Conversion between arbitrary units
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The functions listed above are methods of :class:`BaseElement `
+because they use properties of the root SVG. For an unrooted SVG fragment,
+:meth:`BaseElement.unittouu `.
+:meth:`BaseElement.uutounit ` work as well.
+
+If you want to convert between arbitrary units, you can do so using the
+:meth:`convert_unit ` method:
+
+>>> inkex.units.convert_unit("4mm", "px")
+15.118110236220472
+
+
+Note that inkex doesn't support relative units (percentage, `em` and `ex`) yet. You will have to
+implement these yourself if you want your extension to support them.
+
+.. [1] https://www.w3.org/TR/SVG11/coords.html#Units
+.. [2] https://www.w3.org/TR/SVG2/coords.html#Introduction
+.. [3] https://www.w3.org/TR/SVG2/coords.html#ViewportSpace
+.. [4] https://www.w3.org/TR/SVG2/coords.html#ViewBoxAttribute
+.. [5] Note that this drawing has ``width="100%" height="" viewBox="0 0 88.540985 36.87265"``.
+ This instructs the viewer that the SVG should span the entire width of the containing
+ element (in this case, an HTML div) and the height should be chosen such that the image
+ is scaled proportionally. Inkex doesn't support these relative units and these don't really
+ make sense in standalone SVGs anyway.
\ No newline at end of file
diff --git a/dxf12_outlines.py b/dxf12_outlines.py
index 2bc001fd4215570034370cc5f8db8285f9c573fd..e4e724228882eef327473bf11243d6909b55c883 100755
--- a/dxf12_outlines.py
+++ b/dxf12_outlines.py
@@ -112,7 +112,7 @@ class DxfTwelve(inkex.OutputExtension):
self.dxf_add(r12_header)
scale = 25.4 / 90.0
- h = self.svg.height
+ h = self.svg.viewport_height
path = '//svg:path'
for node in self.svg.xpath(path):
diff --git a/dxf_outlines.py b/dxf_outlines.py
index bb775d3259771201bfaedb08854d460a31797e7d..f341ca1221c5ccde3c1eccc158093f046248fce0 100755
--- a/dxf_outlines.py
+++ b/dxf_outlines.py
@@ -301,7 +301,7 @@ class DxfOutlines(inkex.OutputExtension):
if not scale:
scale = 25.4 / 96 # if no scale is specified, assume inch as baseunit
scale /= self.svg.unittouu('1px')
- h = self.svg.height
+ h = self.svg.viewport_height
doc = self.document.getroot()
# process viewBox height attribute to correct page scaling
viewBox = doc.get('viewBox')
diff --git a/gimp_xcf.py b/gimp_xcf.py
index 826b12ebef482ab5b121aec1fa17a6245db228c7..d667a6b7f16803259de65afadae2c2e9cbeaf6d8 100755
--- a/gimp_xcf.py
+++ b/gimp_xcf.py
@@ -54,12 +54,12 @@ class GimpXcf(TempDirMixin, inkex.OutputExtension):
for guide in self.svg.namedview.get_guides():
if guide.is_horizontal:
# GIMP doesn't like guides that are outside of the image
- if 0 < guide.point.y < self.svg.height:
+ if 0 < guide.point.y < self.svg.viewbox_height:
# The origin is at the top in GIMP land
horz_guides.append(str(guide.point.y))
elif guide.is_vertical:
# GIMP doesn't like guides that are outside of the image
- if 0 < guide.point.x < self.svg.width:
+ if 0 < guide.point.x < self.svg.viewbox_width:
vert_guides.append(str(guide.point.x))
return ('h', ' '.join(horz_guides)), ('v', ' '.join(vert_guides))
diff --git a/guides_creator.py b/guides_creator.py
index 4ee531d9f93245b8f8ba251e14fb4863e3c3dad3..7f640e86b5458914643a834ccd24db2f8755e776 100755
--- a/guides_creator.py
+++ b/guides_creator.py
@@ -57,8 +57,8 @@ class GuidesCreator(inkex.EffectExtension):
def effect(self):
# getting the width and height attributes of the canvas
- self.width = float(self.svg.width)
- self.height = float(self.svg.height)
+ self.width = float(self.svg.viewbox_width)
+ self.height = float(self.svg.viewbox_height)
# getting edges coordinates
self.h_orientation = '0,' + str(round(self.width, 4))
diff --git a/guillotine.py b/guillotine.py
index 1e2c988a64e1202ebe65bc2c0aaf2962103d2a5a..727eda08319d8f8d6c4cd69cd15799d5a690d39e 100755
--- a/guillotine.py
+++ b/guillotine.py
@@ -74,7 +74,7 @@ class Guillotine(inkex.EffectExtension):
those outside of the canvas
"""
horizontals = [0.0]
- height = float(self.svg.height)
+ height = float(self.svg.viewbox_height)
for y in self.get_all_horizontal_guides():
if 0.0 < y <= height:
horizontals.append(y)
@@ -88,7 +88,7 @@ class Guillotine(inkex.EffectExtension):
those outside of the canvas.
"""
verticals = [0.0]
- width = float(self.svg.width)
+ width = float(self.svg.viewbox_width)
for x in self.get_all_vertical_guides():
if 0.0 < x <= width:
verticals.append(x)
@@ -104,6 +104,9 @@ class Guillotine(inkex.EffectExtension):
"""
hs = self.get_horizontal_slice_positions()
vs = self.get_vertical_slice_positions()
+ # The --export-width argument is in viewport units
+ hs = [self.svg.unit_to_viewport(i) for i in hs]
+ vs = [self.svg.unit_to_viewport(j) for j in vs]
slices = []
for i in range(len(hs) - 1):
for j in range(len(vs) - 1):
@@ -151,6 +154,7 @@ class Guillotine(inkex.EffectExtension):
given.
"""
coords = ":".join([self.get_localised_string(dim) for dim in sli])
+ inkex.errormsg(coords)
inkscape(self.options.input_file, export_area=coords, export_filename=filename)
def export_slices(self, slices):
diff --git a/hpgl_encoder.py b/hpgl_encoder.py
index 322e5b640c429703793a73049fbfa9a5cef4b607..e2676a5dc15473442215acfb97b0719a949536d4 100644
--- a/hpgl_encoder.py
+++ b/hpgl_encoder.py
@@ -60,8 +60,8 @@ class hpglEncoder(object):
"""
self.options = effect.options
self.doc = effect.svg
- self.docWidth = effect.svg.unittouu(effect.svg.get('width'))
- self.docHeight = effect.svg.unittouu(effect.svg.get('height'))
+ self.docWidth = effect.svg.viewbox_width
+ self.docHeight = effect.svg.viewbox_height
self.hpgl = ''
self.divergenceX = 'False'
self.divergenceY = 'False'
@@ -76,13 +76,13 @@ class hpglEncoder(object):
self.offsetY = 0
# dots per inch to dots per user unit:
- self.scaleX = self.options.resolutionX / effect.svg.unittouu("1.0in")
- self.scaleY = self.options.resolutionY / effect.svg.unittouu("1.0in")
+ self.scaleX = self.options.resolutionX / effect.svg.viewport_to_unit("1.0in")
+ self.scaleY = self.options.resolutionY / effect.svg.viewport_to_unit("1.0in")
scaleXY = (self.scaleX + self.scaleY) / 2
# mm to dots (plotter coordinate system):
- self.overcut = effect.svg.unittouu(str(self.options.overcut) + "mm") * scaleXY
- self.toolOffset = effect.svg.unittouu(str(self.options.toolOffset) + "mm") * scaleXY
+ self.overcut = effect.svg.viewport_to_unit(str(self.options.overcut) + "mm") * scaleXY
+ self.toolOffset = effect.svg.viewport_to_unit(str(self.options.toolOffset) + "mm") * scaleXY
# scale flatness to resolution:
self.flat = self.options.flat / (1016 / ((self.options.resolutionX + \
@@ -98,8 +98,8 @@ class hpglEncoder(object):
self.viewBoxTransformY = 1
viewBox = effect.svg.get_viewbox()
if viewBox and viewBox[2] and viewBox[3]:
- self.viewBoxTransformX = self.docWidth / effect.svg.unittouu(effect.svg.add_unit(viewBox[2]))
- self.viewBoxTransformY = self.docHeight / effect.svg.unittouu(effect.svg.add_unit(viewBox[3]))
+ self.viewBoxTransformX = self.docWidth / effect.svg.viewport_to_unit(effect.svg.add_unit(viewBox[2]))
+ self.viewBoxTransformY = self.docHeight / effect.svg.viewport_to_unit(effect.svg.add_unit(viewBox[3]))
def getHpgl(self):
"""Return the HPGL instructions"""
diff --git a/inkex/deprecated.py b/inkex/deprecated.py
index 262ffd9ea76ab5519958223f60bd7d52d8b51e05..6662b35559c1afb56fe9daf3870a784924d6263e 100644
--- a/inkex/deprecated.py
+++ b/inkex/deprecated.py
@@ -409,3 +409,15 @@ def composed_style(element: ShapeElement):
return element.specified_style()
ShapeElement.composed_style = deprecate(composed_style)
+
+
+def width(self):
+ """Use BaseElement.viewport_width instead"""
+ return self.viewport_width
+
+def height(self):
+ """Use BaseElement.viewport_height instead"""
+ return self.viewport_height
+
+BaseElement.width = property(deprecate(width))
+BaseElement.height = property(deprecate(height))
\ No newline at end of file
diff --git a/inkex/elements/_base.py b/inkex/elements/_base.py
index 9d01884870385185f20a836fb518c394ae4a3b9d..1248e0958a466f91012800459d6600d0232a301f 100644
--- a/inkex/elements/_base.py
+++ b/inkex/elements/_base.py
@@ -448,13 +448,25 @@ class BaseElement(etree.ElementBase):
except FragmentError:
return 'px' # Don't cache.
- def uutounit(self, value, to_unit='px'):
- """Convert the unit the given unit type"""
- return convert_unit(value, to_unit, default=self.unit)
-
- def unittouu(self, value):
- """Convert a unit value into the document's units"""
- return convert_unit(value, self.unit)
+ @staticmethod
+ def uutounit(value, to_unit='px'):
+ """Convert a value given in user units (px) the given unit type"""
+ return convert_unit(value, to_unit)
+
+ @staticmethod
+ def unittouu(value):
+ """Convert a length value into user units (px)"""
+ return convert_unit(value, "px")
+
+ def unit_to_viewport(self, value, unit="px"):
+ """Converts a length value to viewport units, as defined by the width/height
+ element on the root"""
+ return self.uutounit(self.unittouu(value) * self.root.equivalent_transform_scale, unit)
+
+ def viewport_to_unit(self, value, unit="px"):
+ """Converts a length given on the viewport to the specified unit in the user
+ coordinate system"""
+ return self.uutounit(self.unittouu(value) / self.root.equivalent_transform_scale, unit)
def add_unit(self, value):
"""Add document unit when no unit is specified in the string """
diff --git a/inkex/elements/_filters.py b/inkex/elements/_filters.py
index bd56f8480247e47dfbb954334056147ef3a4489f..d3ab5f6b253d9fc155f6332b713627a3f841c2d2 100644
--- a/inkex/elements/_filters.py
+++ b/inkex/elements/_filters.py
@@ -174,15 +174,15 @@ class LinearGradient(Gradient):
def apply_transform(self): # type: () -> None
"""Apply transform to orientation points and set it to identity."""
trans = self.pop('gradientTransform')
- p1 = (self.uutounit(self.get('x1')), self.uutounit(self.get('y1')))
- p2 = (self.uutounit(self.get('x2')), self.uutounit(self.get('y2')))
+ p1 = (self.unittouu(self.get('x1')), self.unittouu(self.get('y1')))
+ p2 = (self.unittouu(self.get('x2')), self.unittouu(self.get('y2')))
p1t = trans.apply_to_point(p1)
p2t = trans.apply_to_point(p2)
self.update(
x1=self.unittouu(p1t[0]),
- y1=self.uutounit(p1t[1]),
- x2=self.uutounit(p2t[0]),
- y2=self.uutounit(p2t[1]))
+ y1=self.unittouu(p1t[1]),
+ x2=self.unittouu(p2t[0]),
+ y2=self.unittouu(p2t[1]))
class RadialGradient(Gradient):
@@ -192,15 +192,15 @@ class RadialGradient(Gradient):
def apply_transform(self): # type: () -> None
"""Apply transform to orientation points and set it to identity."""
trans = self.pop('gradientTransform')
- p1 = (self.uutounit(self.get('cx')), self.uutounit(self.get('cy')))
- p2 = (self.uutounit(self.get('fx')), self.uutounit(self.get('fy')))
+ p1 = (self.unittouu(self.get('cx')), self.unittouu(self.get('cy')))
+ p2 = (self.unittouu(self.get('fx')), self.unittouu(self.get('fy')))
p1t = trans.apply_to_point(p1)
p2t = trans.apply_to_point(p2)
self.update(
- cx=self.uutounit(p1t[0]),
- cy=self.uutounit(p1t[1]),
- fx=self.uutounit(p2t[0]),
- fy=self.uutounit(p2t[1]))
+ cx=self.unittouu(p1t[0]),
+ cy=self.unittouu(p1t[1]),
+ fx=self.unittouu(p2t[0]),
+ fy=self.unittouu(p2t[1]))
class PathEffect(BaseElement):
"""Inkscape LPE element"""
diff --git a/inkex/elements/_polygons.py b/inkex/elements/_polygons.py
index b7fdcc88e4727322964f2713ecb184113c096b9d..8e502f399e59e42ba6050a90ff66594a5d7b8e6a 100644
--- a/inkex/elements/_polygons.py
+++ b/inkex/elements/_polygons.py
@@ -128,14 +128,14 @@ class Line(ShapeElement):
class RectangleBase(ShapeElement):
"""Provide a useful extension for rectangle elements"""
- left = property(lambda self: self.uutounit(self.get('x', '0'), 'px'))
- top = property(lambda self: self.uutounit(self.get('y', '0'), 'px'))
+ left = property(lambda self: self.unittouu(self.get('x', '0')))
+ top = property(lambda self: self.unittouu(self.get('y', '0')))
right = property(lambda self: self.left + self.width)
bottom = property(lambda self: self.top + self.height)
- width = property(lambda self: self.uutounit(self.get('width', '0'), 'px'))
- height = property(lambda self: self.uutounit(self.get('height', '0'), 'px'))
- rx = property(lambda self: self.uutounit(self.get('rx', self.get('ry', 0.0)), 'px'))
- ry = property(lambda self: self.uutounit(self.get('ry', self.get('rx', 0.0)), 'px')) # pylint: disable=invalid-name
+ width = property(lambda self: self.unittouu(self.get('width', '0')))
+ height = property(lambda self: self.unittouu(self.get('height', '0')))
+ rx = property(lambda self: self.unittouu(self.get('rx', self.get('ry', 0.0))))
+ ry = property(lambda self: self.unittouu(self.get('ry', self.get('rx', 0.0)))) # pylint: disable=invalid-name
def get_path(self):
"""Calculate the path as the box around the rect"""
@@ -174,7 +174,7 @@ class EllipseBase(ShapeElement):
@property
def center(self):
- return ImmutableVector2d(self.uutounit(self.get('cx', '0')), self.uutounit(self.get('cy', '0')))
+ return ImmutableVector2d(self.unittouu(self.get('cx', '0')), self.unittouu(self.get('cy', '0')))
@center.setter
def center(self, value):
@@ -201,7 +201,7 @@ class Circle(EllipseBase):
@property
def radius(self):
- return self.uutounit(self.get('r', '0'), 'px')
+ return self.unittouu(self.get('r', '0'))
@radius.setter
def radius(self, value):
@@ -218,7 +218,7 @@ class Ellipse(EllipseBase):
@property
def radius(self):
- return ImmutableVector2d(self.uutounit(self.get('rx', '0')), self.uutounit(self.get('ry', '0')))
+ return ImmutableVector2d(self.unittouu(self.get('rx', '0')), self.unittouu(self.get('ry', '0')))
@radius.setter
def radius(self, value):
diff --git a/inkex/elements/_svg.py b/inkex/elements/_svg.py
index 9c1fa800b48fd5eea9f348ed020a0dd01d65770d..4661fbe81e825186c25288015bbfa5c72fb4bbad 100644
--- a/inkex/elements/_svg.py
+++ b/inkex/elements/_svg.py
@@ -75,7 +75,7 @@ class SvgDocumentElement(DeprecatedSvgMixin, BaseElement):
def get_page_bbox(self):
"""Gets the page dimensions as a bbox"""
- return BoundingBox((0, float(self.width)), (0, float(self.height)))
+ return BoundingBox((0, float(self.viewbox_width)), (0, float(self.viewbox_height)))
def get_current_layer(self):
"""Returns the currently selected layer"""
@@ -146,28 +146,54 @@ class SvgDocumentElement(DeprecatedSvgMixin, BaseElement):
return ret
@property
- def width(self): # getDocumentWidth(self):
- """Fault tolerance for lazily defined SVG"""
+ def viewbox_width(self): # getDocumentWidth(self):
+ """Returns the width of the `user coordinate system
+ `_ in user units, i.e. the width
+ of the viewbox, as defined in the SVG file. If no viewbox is defined, the value of the
+ width attribute is returned. If the height is not defined, return 0."""
+ return self.get_viewbox()[2] or self.viewport_width
+
+ @property
+ def viewport_width(self):
+ """Returns the width of the `viewport coordinate system
+ `_ in user units, i.e. the width
+ attribute of the svg element converted to px"""
return self.unittouu(self.get('width')) or self.get_viewbox()[2]
@property
- def height(self): # getDocumentHeight(self):
- """Returns a string corresponding to the height of the document, as
- defined in the SVG file. If it is not defined, returns the height
- as defined by the viewBox attribute. If viewBox is not defined,
- returns the string '0'."""
+ def viewbox_height(self): # getDocumentHeight(self):
+ """Returns the height of the `user coordinate system
+ `_ in user units, i.e. the height
+ of the viewbox, as defined in the SVG file. If no viewbox is defined, the value of the
+ height attribute is returned. If the height is not defined, return 0."""
+ return self.get_viewbox()[3] or self.viewport_height
+
+ @property
+ def viewport_height(self):
+ """Returns the width of the `viewport coordinate system
+ `_ in user units, i.e. the height
+ attribute of the svg element converted to px"""
return self.unittouu(self.get('height')) or self.get_viewbox()[3]
@property
def scale(self):
- """Return the ratio between the page width and the viewBox width"""
+ """Return the ratio between the viewBox width and the page width, which is displayed
+ as "scale" in the Inkscape document properties"""
try:
- scale_x = float(self.width) / float(self.get_viewbox()[2])
- scale_y = float(self.height) / float(self.get_viewbox()[3])
- return max([scale_x, scale_y])
+ scale_x = self.viewbox_width / self.viewport_width
+ scale_y = self.viewbox_height / self.viewport_height
+ value = min([scale_x, scale_y])
+ return 1.0 if value == 0 else value
except (ValueError, ZeroDivisionError):
return 1.0
+ @property
+ def equivalent_transform_scale(self) -> float:
+ """Return the scale of the equivalent transform of the svg tag, as defined by
+ https://www.w3.org/TR/SVG2/coords.html#ComputingAViewportsTransform
+ (highly simplified)"""
+ return 1/self.scale
+
@property
def unit(self):
"""Returns the unit used for in the SVG document.
diff --git a/inkex/elements/_text.py b/inkex/elements/_text.py
index 77c33fa3112bdd51ca6ffb412ca4535d71a69cff..4d343910cfedeadf182ed2f3da23be1a3b087a83 100644
--- a/inkex/elements/_text.py
+++ b/inkex/elements/_text.py
@@ -79,8 +79,8 @@ class FlowSpan(ShapeElement):
class TextElement(ShapeElement):
"""A Text element"""
tag_name = 'text'
- x = property(lambda self: self.uutounit(self.get('x', 0)))
- y = property(lambda self: self.uutounit(self.get('y', 0)))
+ x = property(lambda self: self.unittouu(self.get('x', 0)))
+ y = property(lambda self: self.unittouu(self.get('y', 0)))
def get_path(self):
return Path()
@@ -116,8 +116,8 @@ class TextPath(ShapeElement):
class Tspan(ShapeElement):
"""A tspan text element"""
tag_name = 'tspan'
- x = property(lambda self: self.uutounit(self.get('x', 0)))
- y = property(lambda self: self.uutounit(self.get('y', 0)))
+ x = property(lambda self: self.unittouu(self.get('x', 0)))
+ y = property(lambda self: self.unittouu(self.get('y', 0)))
@classmethod
def superscript(cls, text):
@@ -134,7 +134,7 @@ class Tspan(ShapeElement):
"""
effective_transform = Transform(transform) * self.transform
x1, y1 = effective_transform.apply_to_point((self.x, self.y))
- fontsize = self.uutounit(self.style.get('font-size', '1em'))
+ fontsize = self.unittouu(self.style.get('font-size', '12px'))
x2 = self.x + 0 # XXX This is impossible to calculate!
y2 = self.y + float(fontsize)
x2, y2 = effective_transform.apply_to_point((x2, y2))
diff --git a/inkex/tester/svg.py b/inkex/tester/svg.py
index b1bd20e454f0e799ef62a20ba701a531fe60fa43..bb01e45a54ac329ac4676f9708c3ce967ffbd43d 100644
--- a/inkex/tester/svg.py
+++ b/inkex/tester/svg.py
@@ -35,12 +35,12 @@ def svg(svg_attrs=''):
f' '), parser=SVG_PARSER)
-def uu_svg(user_unit):
- """Same as svg, but takes a user unit for the new document.
+def svg_unit_scaled(width_unit):
+ """Same as svg, but takes a width unit (top-level transform) for the new document.
- It's based on the ratio between the SVG width and the viewBox width.
+ The transform is the ratio between the SVG width and the viewBox width.
"""
- return svg(f'width="1{user_unit}" viewBox="0 0 1 1"')
+ return svg(f'width="1{width_unit}" viewBox="0 0 1 1"')
def svg_file(filename):
"""Parse an svg file and return it's document root"""
diff --git a/lorem_ipsum.py b/lorem_ipsum.py
index b0a6a56d679b954b5ef17ae0765fe4a899f15970..f97d6c6930c622713aa8ac2aa947f265984dcbc0 100755
--- a/lorem_ipsum.py
+++ b/lorem_ipsum.py
@@ -244,18 +244,18 @@ class LoremIpsum(inkex.EffectExtension):
style["shape-inside"] = shape.get_id(as_url=2)
else:
parent = self.get_layer()
- style["inline-size"] = self.svg.width
+ style["inline-size"] = self.svg.viewbox_width
textelement = parent.add(TextElement())
textelement.style = style
textelement.style["white-space"] = "pre"
- textelement.style["font-size"] = self.svg.unittouu("8pt")
+ textelement.style["font-size"] = self.svg.viewport_to_unit("8pt")
self.add_text_svg2(textelement)
def create_text_svg12(self, shape):
"""Creates a new SVG1.2 flowed text with the given shape inside. If no shape inside was set,
the flowed text is appended to the selected layer"""
root = FlowRoot()
root.set('xml:space', 'preserve')
- root.style["font-size"] = self.svg.unittouu("8pt")
+ root.style["font-size"] = self.svg.viewport_to_unit("8pt")
region = root.add(FlowRegion())
if shape is not None and not isinstance(shape, TextElement):
parent = shape.getparent()
@@ -265,8 +265,8 @@ class LoremIpsum(inkex.EffectExtension):
# Nothing selected, create a new flowtext
parent = self.get_layer()
shape = region.add(Rectangle(x='0', y='0',\
- width=str(int(self.svg.width)),\
- height=str(int(self.svg.height))))
+ width=str(int(self.svg.viewbox_width)),\
+ height=str(int(self.svg.viewbox_height))))
parent.add(root)
self.add_text_svg12(root)
diff --git a/path_mesh_m2p.py b/path_mesh_m2p.py
index c0a4aeccbc07f25c7a84c4028dc49d3981fa0c04..daeb43b72063914d7ce9086d7e0afb24302dcb50 100755
--- a/path_mesh_m2p.py
+++ b/path_mesh_m2p.py
@@ -296,7 +296,7 @@ class MeshToPath(inkex.EffectExtension):
def csp_to_path(self, node, csp_list, transform=None):
"""Create new paths based on csp data, return group with paths."""
# set up stroke width, group
- stroke_width = self.svg.unittouu('1px')
+ stroke_width = self.svg.viewport_to_unit('1px')
stroke_color = '#000000'
style = {
'fill': 'none',
diff --git a/printing_marks.py b/printing_marks.py
index 96f240482a7faf8eb3e33d85616050462e4ad793..424759aa39359a515aacd1470d66089bacb3bbb1 100755
--- a/printing_marks.py
+++ b/printing_marks.py
@@ -151,8 +151,8 @@ class PrintingMarks(inkex.EffectExtension):
i += 0.1
def effect(self):
- self.mark_size = self.svg.unittouu('1cm')
- self.min_mark_margin = self.svg.unittouu('3mm')
+ self.mark_size = self.svg.viewport_to_unit('1cm')
+ self.min_mark_margin = self.svg.viewport_to_unit('3mm')
if self.options.where == 'selection':
bbox = self.svg.selection.bounding_box()
@@ -166,12 +166,12 @@ class PrintingMarks(inkex.EffectExtension):
svg = self.document.getroot()
# Convert parameters to user unit
- offset = self.svg.unittouu(str(self.options.crop_offset) +
+ offset = self.svg.viewport_to_unit(str(self.options.crop_offset) +
self.options.unit)
- bt = self.svg.unittouu(str(self.options.bleed_top) + self.options.unit)
- bb = self.svg.unittouu(str(self.options.bleed_bottom) + self.options.unit)
- bl = self.svg.unittouu(str(self.options.bleed_left) + self.options.unit)
- br = self.svg.unittouu(str(self.options.bleed_right) + self.options.unit)
+ bt = self.svg.viewport_to_unit(str(self.options.bleed_top) + self.options.unit)
+ bb = self.svg.viewport_to_unit(str(self.options.bleed_bottom) + self.options.unit)
+ bl = self.svg.viewport_to_unit(str(self.options.bleed_left) + self.options.unit)
+ br = self.svg.viewport_to_unit(str(self.options.bleed_right) + self.options.unit)
# Bleed margin
if bt < offset:
bmt = 0
@@ -387,16 +387,17 @@ class PrintingMarks(inkex.EffectExtension):
g_pag_info.label = 'PageInformation'
g_pag_info.set('id', 'PageInformation')
y_margin = max(bmb + offset, self.min_mark_margin)
+ font_size = self.svg.viewport_to_unit("9pt")
txt_attribs = {
- 'style': 'font-size:12px;font-style:normal;font-weight:normal;fill:#000000;font-family:Bitstream Vera Sans,sans-serif;text-anchor:middle;text-align:center',
+ 'style': f'font-size:{font_size}px;font-style:normal;font-weight:normal;fill:#000000;font-family:Bitstream Vera Sans,sans-serif;text-anchor:middle;text-align:center',
'x': str(middle_horizontal),
'y': str(bbox.bottom + y_margin + self.mark_size + 20)
}
txt = g_pag_info.add(TextElement(**txt_attribs))
txt.text = 'Page size: ' + \
- str(round(self.svg.uutounit(bbox.width, self.options.unit), 2)) + \
+ str(round(self.svg.unit_to_viewport(bbox.width, self.options.unit), 2)) + \
'x' + \
- str(round(self.svg.uutounit(bbox.height, self.options.unit), 2)) + \
+ str(round(self.svg.unit_to_viewport(bbox.height, self.options.unit), 2)) + \
' ' + self.options.unit
diff --git a/tests/data/refs/dimension__--id__circle1.out b/tests/data/refs/dimension__--id__circle1.out
new file mode 100644
index 0000000000000000000000000000000000000000..fc681d0cb6836ced10cfc96a9cd5f5d843d805da
--- /dev/null
+++ b/tests/data/refs/dimension__--id__circle1.out
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/data/refs/guides_creator__--vertical_guides__6__--horizontal_guides__8.out b/tests/data/refs/guides_creator__--vertical_guides__6__--horizontal_guides__8.out
new file mode 100644
index 0000000000000000000000000000000000000000..3a61a207f157e49ee0d6155f4be52dda237297c0
--- /dev/null
+++ b/tests/data/refs/guides_creator__--vertical_guides__6__--horizontal_guides__8.out
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/data/refs/hpgl_output__--force__24__--speed__20__--orientation__90__hpgl_multipen__svg.out b/tests/data/refs/hpgl_output__--force__24__--speed__20__--orientation__90__hpgl_multipen__svg.out
index c56e69cf627062fb3713e54858a889271f0ca60a..d0f4221372838f62cd35adb9b7d723920dcd4c9b 100644
--- a/tests/data/refs/hpgl_output__--force__24__--speed__20__--orientation__90__hpgl_multipen__svg.out
+++ b/tests/data/refs/hpgl_output__--force__24__--speed__20__--orientation__90__hpgl_multipen__svg.out
@@ -1 +1 @@
-IN;PU;SP1;VS20;FS24;PU0,0;PD0,90;PU3203,7456;PD3208,7455,3212,7451,3213,7446,3211,7441,3207,7437,3203,7436,3203,4422,3198,4423,3194,4427,3193,4432,0,4432,1,4437,5,4441,10,4442,10,7456,15,7455,19,7451,20,7446,3213,7446,3211,7441,3208,7438,3203,7436,3203,7396;PU;SP2;PU3203,3015;PD3203,3015,3203,0,3198,2,3194,6,3193,10,0,10,1,16,5,19,10,20,10,3035,15,3033,19,3030,20,3025,3213,3025,3211,3020,3208,3016,3203,3015,3203,2975;PU;SP3;PU3203,11744;PD3203,11744,3203,8730,3198,8731,3194,8735,3193,8740,0,8740,1,8745,5,8748,10,8750,10,11764,15,11763,19,11759,20,11754,3213,11754,3211,11749,3208,11745,3203,11744,3203,11704;SP0;PU0,0;IN;
\ No newline at end of file
+IN;PU;SP1;VS20;FS24;PU0,0;PD0,90;PU855,1988;PD860,1986,864,1983,865,1977,863,1972,859,1969,855,1968,855,1170,850,1172,846,1176,845,1180,0,1180,2,1185,5,1189,10,1190,10,1988,15,1986,19,1983,20,1978,865,1978,863,1973,860,1969,855,1968,855,1928;PU;SP2;PU855,798;PD855,798,855,0,850,2,846,6,845,10,0,10,2,16,5,19,10,20,10,818,15,817,19,813,20,808,865,808,863,803,860,799,855,798,855,758;PU;SP3;PU855,3108;PD855,3108,855,2310,850,2311,846,2315,845,2320,0,2320,2,2325,5,2329,10,2330,10,3128,15,3126,19,3122,20,3118,865,3118,863,3112,860,3109,855,3108,855,3068;SP0;PU0,0;IN;
\ No newline at end of file
diff --git a/tests/data/refs/lorem_ipsum__--svg2__true.out b/tests/data/refs/lorem_ipsum__--svg2__true.out
new file mode 100644
index 0000000000000000000000000000000000000000..e47873a1ecb802ba9c6a6142b9bbcbbf0e1a5179
--- /dev/null
+++ b/tests/data/refs/lorem_ipsum__--svg2__true.out
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nam molestie nisl at metus. Mauris ac massa vestibulum nisl facilisis viverra. Aliquam justo lectus, iaculis a, auctor sed, congue in, nisl. Sed quis elit. Mauris sed nulla quis nisi interdum tempor. Proin dolor sapien, adipiscing id, sagittis eu, molestie viverra, mauris. Integer tempus malesuada pede. Maecenas rhoncus rhoncus ipsum. Mauris ac massa vestibulum nisl facilisis viverra. Suspendisse fermentum. Phasellus magna sem, vulputate eget, ornare sed, dignissim sit amet, pede. Nam id neque. Nulla blandit justo a metus. Nam laoreet dui sed magna. Vivamus posuere, ante eu tempor dictum, felis nibh facilisis sem, eu auctor metus nulla non lorem. Phasellus at purus sed purus cursus iaculis. Vivamus feugiat. Integer fringilla. Praesent a eros. Aliquam sed erat. Nam a nunc. Maecenas rhoncus rhoncus ipsum.
+
+Praesent scelerisque. Proin lectus orci, venenatis pharetra, egestas id, tincidunt vel, eros. Etiam cursus purus interdum libero. Nam massa turpis, nonummy et, consectetuer id, placerat ac, ante. Suspendisse fermentum. Sed at turpis vitae velit euismod aliquet. Aliquam justo lectus, iaculis a, auctor sed, congue in, nisl. Nam massa turpis, nonummy et, consectetuer id, placerat ac, ante. Quisque arcu ante, cursus in, ornare quis, viverra ut, justo. Nam a nunc. Duis sem velit, ultrices et, fermentum auctor, rhoncus ut, ligula. Aliquam metus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse potenti. Mauris tincidunt aliquam ante. Mauris et pede. In consectetuer, lorem eu lobortis egestas, velit odio imperdiet eros, sit amet sagittis nunc mi ac neque. Curabitur risus urna, placerat et, luctus pulvinar, auctor vel, orci. Donec ut purus. Integer accumsan. Morbi volutpat. Suspendisse potenti. Vivamus posuere, ante eu tempor dictum, felis nibh facilisis sem, eu auctor metus nulla non lorem.
+
+Aenean luctus vulputate turpis. Pellentesque convallis dolor vel libero. Quisque arcu ante, cursus in, ornare quis, viverra ut, justo. Etiam non neque ac mi vestibulum placerat. Mauris urna sem, suscipit vitae, dignissim id, ultrices sed, nunc. Phasellus at purus sed purus cursus iaculis. Phasellus lacinia iaculis mi. Mauris tempor ultrices justo. Nam massa turpis, nonummy et, consectetuer id, placerat ac, ante. Nam id neque. Fusce venenatis ligula in pede. Nullam libero nunc, tristique eget, laoreet eu, sagittis id, ante. Suspendisse lectus. Phasellus nisi metus, tempus sit amet, ultrices ac, porta nec, felis. Pellentesque suscipit accumsan massa. Aliquam metus.
+
+Cras ac enim vel dui vestibulum suscipit. In hac habitasse platea dictumst. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Vivamus eu orci. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse potenti. Integer accumsan. Nam id neque. Donec at diam a tellus dignissim vestibulum. Donec ut urna. Pellentesque ac turpis. Phasellus hendrerit. Nulla sed lacus. Aenean justo ipsum, luctus ut, volutpat laoreet, vehicula in, libero. Morbi turpis arcu, egestas congue, condimentum quis, tristique cursus, leo. Nam pharetra. Nulla sagittis condimentum ligula. Nam malesuada sapien eu nibh. Sed a lorem ut est tincidunt consectetuer. Sed dolor. Donec ut purus. Phasellus hendrerit. Mauris et pede. Donec diam eros, tristique sit amet, pretium vel, pellentesque ut, neque. Aenean turpis ipsum, rhoncus vitae, posuere vitae, euismod sed, ligula. Nulla sagittis condimentum ligula. Aliquam imperdiet lobortis metus. Integer accumsan. Donec interdum vestibulum libero. Curabitur risus urna, placerat et, luctus pulvinar, auctor vel, orci.
+
+Donec interdum vestibulum libero. Nam molestie nisl at metus. In consectetuer, lorem eu lobortis egestas, velit odio imperdiet eros, sit amet sagittis nunc mi ac neque. Phasellus magna sem, vulputate eget, ornare sed, dignissim sit amet, pede. Aliquam imperdiet lobortis metus. Nam consectetuer mollis dolor. Curabitur lorem risus, sagittis vitae, accumsan a, iaculis id, metus. Integer tempus malesuada pede. Nam laoreet dui sed magna. Donec sit amet enim. Mauris et dolor. Ut eu metus id lectus vestibulum ultrices. Quisque aliquam, nulla ac scelerisque convallis, nisi ligula sagittis risus, at nonummy arcu urna pulvinar nibh. Pellentesque sit amet dui vel justo gravida auctor. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. Fusce venenatis ligula in pede. Suspendisse potenti. Quisque vehicula porttitor odio. Praesent a lacus vitae turpis consequat semper. Praesent scelerisque. Pellentesque viverra dolor non nunc. Aliquam justo lectus, iaculis a, auctor sed, congue in, nisl. Mauris sed nulla quis nisi interdum tempor. Aliquam velit dui, commodo quis, porttitor eget, convallis et, nisi. Donec ut urna. Morbi turpis arcu, egestas congue, condimentum quis, tristique cursus, leo.
\ No newline at end of file
diff --git a/tests/data/refs/restack__--id__g20858__--id__g21085__--id__g20940__--id__g26580__--id__g21081__--id__g20854.out b/tests/data/refs/restack__--id__g20858__--id__g21085__--id__g20940__--id__g26580__--id__g21081__--id__g20854.out
new file mode 100644
index 0000000000000000000000000000000000000000..799e354a83fc76b1a0b3f9d31f4a60d001e7aecb
--- /dev/null
+++ b/tests/data/refs/restack__--id__g20858__--id__g21085__--id__g20940__--id__g26580__--id__g21081__--id__g20854.out
@@ -0,0 +1,38 @@
+
+
+
+
+ Ungrouped objects
+ Grouped objects
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/data/svg/restack_grouped.svg b/tests/data/svg/restack_grouped.svg
new file mode 100644
index 0000000000000000000000000000000000000000..aee5629df9e8aed295ae48ff40d264de6e92a849
--- /dev/null
+++ b/tests/data/svg/restack_grouped.svg
@@ -0,0 +1,215 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ungrouped objects
+ Grouped objects
+
+
+
+
+
+
+
+
diff --git a/tests/test_dimension.py b/tests/test_dimension.py
index 1514589296428474104d871777ec5ac47c576645..818f7178735ea1e1e149b291e1994834d0f2b5b4 100644
--- a/tests/test_dimension.py
+++ b/tests/test_dimension.py
@@ -8,3 +8,8 @@ class TestDimensionBasic(ComparisonMixin, TestCase):
('--id=p1', '--id=r3', '--xoffset=100.0', '--yoffset=100.0'),
('--id=p1', '--id=r3', '--type=visual', '--xoffset=100.0', '--yoffset=100.0'),
]
+
+class TestDimensionMillimeters(ComparisonMixin, TestCase):
+ effect_class = Dimension
+ compare_file = 'svg/css.svg'
+ comparisons = [('--id=circle1',)]
\ No newline at end of file
diff --git a/tests/test_guides_creator.py b/tests/test_guides_creator.py
index 33264dd89b338ca32e0ad9b6eee27bdec189f624..043aa10dbbfde3e0135a3796bc351394a13da68a 100644
--- a/tests/test_guides_creator.py
+++ b/tests/test_guides_creator.py
@@ -19,3 +19,10 @@ class GuidesCreatorBasicTest(ComparisonMixin, InkscapeExtensionTestMixin, TestCa
old_defaults + ('--tab=margins', '--start_from_edges=True', '--margins_preset=book_left'),
old_defaults + ('--tab=margins', '--start_from_edges=True', '--margins_preset=book_right'),
]
+
+class GuidesCreatorMillimeterTest(ComparisonMixin, TestCase):
+ effect_class = GuidesCreator
+ compare_file = 'svg/complextransform.test.svg'
+ compare_filters = [CompareNumericFuzzy()]
+ comparisons = [("--vertical_guides=6", "--horizontal_guides=8")]
+
diff --git a/tests/test_hershey.py b/tests/test_hershey.py
index f76eac8f19621ef2de939430eb9de107f66af96a..451c4e8998e3df1543659bf464c4e690ad245a5f 100644
--- a/tests/test_hershey.py
+++ b/tests/test_hershey.py
@@ -4,7 +4,6 @@ from lxml import etree
from inkex.tester import ComparisonMixin, InkscapeExtensionTestMixin, TestCase
from inkex.tester.filters import CompareNumericFuzzy, CompareOrderIndependentStyle
-from inkex.tester.svg import svg, svg_file, uu_svg
from hershey import Hershey
diff --git a/tests/test_inkex_svg.py b/tests/test_inkex_svg.py
index 43b4666c0406620c57f49fd86bdb50eb07124c66..a6dbbb6ba0cf2415def067ce0995ab4739083c43 100644
--- a/tests/test_inkex_svg.py
+++ b/tests/test_inkex_svg.py
@@ -23,7 +23,7 @@ Test the svg interface for inkscape extensions.
from inkex.transforms import Vector2d
from inkex import Guide
from inkex.tester import TestCase
-from inkex.tester.svg import svg, svg_file, uu_svg
+from inkex.tester.svg import svg, svg_file, svg_unit_scaled
from inkex import addNS
class BasicSvgTest(TestCase):
@@ -144,9 +144,9 @@ class BasicSvgTest(TestCase):
def test_scale(self):
"""Scale of a document"""
doc = svg('id="empty" viewBox="0 0 100 100" width="200" height="200"')
- self.assertEqual(doc.width, 200.0)
+ self.assertEqual(doc.viewport_width, 200.0)
self.assertEqual(doc.get_viewbox()[2], 100.0)
- self.assertEqual(doc.scale, 2.0)
+ self.assertEqual(doc.scale, 0.5)
doc = svg('id="empty" viewBox="0 0 0 0" width="200" height="200"')
self.assertEqual(doc.scale, 1.0)
@@ -167,83 +167,94 @@ class NamedViewTest(TestCase):
class GetDocumentWidthTest(TestCase):
- """Tests for Effect.width."""
+ """Tests for SvgDocumentElement.viewport_width and viewbox_width."""
+ def assert_svg_sizes(self, creation_string, viewport_width, viewbox_width):
+ """Check viewport and viewbox width"""
+ doc = svg(creation_string)
+ self.assertAlmostEqual(doc.viewbox_width, viewbox_width)
+ self.assertAlmostEqual(doc.viewport_width, viewport_width)
def test_no_dimensions(self):
"""An empty width value should be default zero width"""
- self.assertEqual(svg().width, 0)
+ self.assert_svg_sizes("", 0, 0)
def test_empty_width(self):
"""An empty width value should be the same as a missing width."""
- self.assertEqual(svg('width=""').width, 0)
+ self.assert_svg_sizes('width=""', 0, 0)
def test_empty_viewbox(self):
"""An empty viewBox value should be the same as a missing viewBox."""
- self.assertEqual(svg('viewBox=""').width, 0)
+ self.assert_svg_sizes('viewBox=""', 0, 0)
def test_empty_width_and_viewbox(self):
"""Empty values for both should be the same as both missing."""
- self.assertEqual(svg('width="" viewBox=""').width, 0)
+ self.assert_svg_sizes('width="" viewBox=""', 0, 0)
def test_width_only(self):
"""Test a fixed width"""
- self.assertAlmostEqual(svg('width="120mm"').width, 453.5433071)
+ self.assert_svg_sizes('width="120mm"', 453.5433071, 453.5433071)
def test_width_and_viewbox(self):
"""If both are present, width overrides viewBox."""
- self.assertAlmostEqual(svg('width="120mm" viewBox="0 0 22 99"').width, 453.5433071)
+ self.assert_svg_sizes('width="120mm" viewBox="0 0 22 99"', 453.5433071, 22)
def test_viewbox_only(self):
"""IF only the viewBox is present"""
- self.assertEqual(svg('viewBox="0 0 22 99"').width, 22.0)
+ self.assert_svg_sizes('viewBox="0 0 22 99"', 22, 22)
def test_only_valid_viewbox(self):
"""An empty width value should be the same as a missing width."""
- self.assertEqual(svg('width="" viewBox="0 0 22 99"').width, 22.0)
+ self.assert_svg_sizes('width="" viewBox="0 0 22 99"', 22, 22)
def test_non_zero_viewbox_x(self):
"""Demonstrate that a non-zero x value (viewbox[0]) does not affect the width value."""
- self.assertEqual(svg('width="" viewBox="5 7 22 99"').width, 22.0)
+ self.assert_svg_sizes('width="" viewBox="5 7 22 99"', 22, 22)
class GetDocumentHeightTest(TestCase):
"""Tests for Effect.height."""
+ def assert_svg_sizes(self, creation_string, viewport_height, viewbox_height):
+ """Check viewport and viewbox height"""
+ doc = svg(creation_string)
+ self.assertAlmostEqual(doc.viewbox_height, viewbox_height)
+ self.assertAlmostEqual(doc.viewport_height, viewport_height)
+
def test_no_dimensions(self):
"""Test height from blank svg"""
- self.assertEqual(svg().height, 0)
+ self.assert_svg_sizes("", 0, 0)
def test_empty_height(self):
"""An empty height value should be the same as a missing height."""
- self.assertEqual(svg('height=""').height, 0)
+ self.assert_svg_sizes('height=""', 0, 0)
def test_empty_viewbox(self):
"""An empty viewBox value should be the same as a missing viewBox."""
- self.assertEqual(svg('viewBox=""').height, 0)
+ self.assert_svg_sizes('viewBox=""', 0, 0)
def test_empty_height_viewbox(self):
"""Empty values for both should be the same as both missing."""
- self.assertEqual(svg('height="" viewBox=""').height, 0)
+ self.assert_svg_sizes('height="" viewBox=""', 0, 0)
def test_height_only(self):
"""A simple height only in px"""
- self.assertEqual(svg('height="330px"').height, 330)
+ self.assert_svg_sizes('height="330px"', 330, 330)
def test_height_and_viewbox(self):
"""If both are present, height overrides viewBox."""
- self.assertEqual(svg('height="330px" viewBox="0 0 22 99"').height, 330)
+ self.assert_svg_sizes('height="330px" viewBox="0 0 22 99"', 330, 99)
def test_viewbox_only(self):
"""Height from viewBox only"""
- self.assertEqual(svg('viewBox="0 0 22 99"').height, 99.0)
+ self.assert_svg_sizes('viewBox="0 0 22 99"', 99, 99)
def test_no_height_valid_viewbox(self):
"""An empty height value should be the same as a missing height."""
- self.assertEqual(svg('height="" viewBox="0 0 22 99"').height, 99.0)
+ self.assert_svg_sizes('height="" viewBox="0 0 22 99"', 99, 99)
def test_non_zero_viewbox_y(self):
"""Demonstrate that a non-zero y value (viewbox[1]) does not affect the height value."""
- self.assertEqual(svg('height="" viewBox="5 7 22 99"').height, 99.0)
+ self.assert_svg_sizes('height="" viewBox="5 7 22 99"', 99, 99)
class GetDocumentUnitTest(TestCase):
@@ -333,33 +344,36 @@ class UserUnitTest(TestCase):
def assertToUserUnit(self, user_unit, test_value, expected): # pylint: disable=invalid-name
"""Checks a user unit and a test_value against the expected result"""
- doc = uu_svg(user_unit)
+ doc = svg_unit_scaled(user_unit)
self.assertEqual(doc.unit, user_unit, msg=svg)
self.assertAlmostEqual(doc.unittouu(test_value), expected)
def assertFromUserUnit(self, user_unit, value, unit, expected): # pylint: disable=invalid-name
"""Check converting from a user unity for the test_value"""
- self.assertAlmostEqual(uu_svg(user_unit).uutounit(value, unit), expected)
+ self.assertAlmostEqual(svg_unit_scaled(user_unit).uutounit(value, unit), expected)
# Unit-ratio tests. Don't exhaustively test every unit conversion, just
# demonstrate that the logic works.
def test_unittouu_in_to_cm(self):
- """1in is ~2.54cm"""
- self.assertToUserUnit('cm', '1in', 2.54)
+ """1in is 96px in a cm based document"""
+ self.assertToUserUnit('cm', '1in', 96.0)
- def test_unittouu_yd_to_m(self):
- """1yd is ~0.9144m"""
- self.assertToUserUnit('m', '1yd', 0.9144)
+ def test_yd_to_m(self):
+ """1yd is 3456px"""
+ self.assertToUserUnit('m', '1yd', 3456.0)
+
+ def test_unittouu_no_unit(self):
+ """If no unit is given, the value must not be changed in mm based documents."""
+ self.assertToUserUnit('mm', '9.87654321', 9.87654321)
def test_unittouu_identity(self):
- """If the input and output units are the same, the input and output
- values should exactly be the same, too."""
- self.assertToUserUnit('pc', '9.87654321pc', 9.87654321)
+ """User units are px. If a value is given in px, the value must not change"""
+ self.assertToUserUnit('px', '9.87654321px', 9.87654321)
def test_unittouu_unitless_input(self):
"""Passing a unitless value to unittouu() should treat the units as 'px'."""
- self.assertToUserUnit('in', '96', 1) # 1in == 96px
+ self.assertToUserUnit('in', '96', 96) # user unit = px
def test_unittouu_empty_input(self):
"""Passing an empty string to unittouu() should treat the value as zero."""
@@ -386,8 +400,8 @@ class UserUnitTest(TestCase):
def test_unittouu_bad_input_number(self):
"""Bad input number"""
- self.assertToUserUnit('cm', '1in', 2.54)
- # Demonstrate that 1in is ~2.54cm.
+ self.assertToUserUnit('cm', '1in', 96.0)
+ # Demonstrate that 1in is ~96px, also in a "cm based" document.
# Corrupt the input to contain an invalid number component; note that
# the result changes to zero.
@@ -395,39 +409,38 @@ class UserUnitTest(TestCase):
def test_unittouu_bad_input_unit(self):
"""Bad input unit"""
- # Demonstrate that 1.0in passes through without change.
- self.assertToUserUnit('in', '1.0in', 1.0)
+ # Demonstrate that 1.0px passes through without change.
+ self.assertToUserUnit('mm', '1.0px', 1.0)
# Corrupt the input to contain an invalid unit component; note that the
# result changes to 0.0, because corrupt parsing is zero px.
# it used to be the ratio between inches and pixels. This was
# because unittouu() treats unknown units as 'px'.
- self.assertToUserUnit('in', '1.0ABCD', 0)
+ self.assertToUserUnit('mm', '1.0ABCD', 0)
# Unit-ratio tests. Don't exhaustively test every unit conversion, just
# demonstrate that the logic works.
def test_uutounit_cm_to_in(self):
- """Convert 1 user unit ('in') to 'cm'."""
- self.assertFromUserUnit('in', 1, 'cm', 2.54) # 1in is ~2.54cm
+ """Convert 1 user unit (px) to 'cm' in a in-based document"""
+ self.assertFromUserUnit('in', 1, 'cm', 2.54/96) # 1in is ~2.54cm
def test_uutounit_m_to_yd(self):
- """Convert 1 user unit ('yd') to 'm'."""
- self.assertFromUserUnit('yd', 1, 'm', 0.9144) # 1yd is ~0.9144m
+ """Convert 1 user unit (px) to 'm' in a yd-based document"""
+ self.assertFromUserUnit('yd', 1, 'm', 1/100*2.54/96)
def test_uutounit_identity(self):
- """If the input and output units are the same, the input and output
- values should exactly be the same, too."""
- self.assertFromUserUnit('pc', 9.87654321, 'pc', 9.87654321)
+ """If the input unit is px, output value should be identical"""
+ self.assertFromUserUnit('pc', 9.87654321, 'px', 9.87654321)
def test_uutounit_unknown_unit(self):
"""Demonstrate that passing an unknown unit string to uutounit()"""
- self.assertEqual(uu_svg('in').uutounit(1, 'px'), 96.0)
+ self.assertEqual(svg_unit_scaled('in').uutounit(1, 'px'), 1)
def test_adddocumentunit_common(self):
"""Test common add_unit results"""
# For valid float inputs, the output should be the input with the user unit appended.
- doc = uu_svg('pt')
+ doc = svg_unit_scaled('pt')
cases = (
# Input, expected output
(100, '100pt'),
@@ -450,7 +463,7 @@ class UserUnitTest(TestCase):
def test_adddocumentunit_non_float(self):
"""Strings that are invalid floats should pass through unchanged."""
- doc = uu_svg('pt')
+ doc = svg_unit_scaled('pt')
inputs = (
'',
'ABCD',
@@ -459,3 +472,30 @@ class UserUnitTest(TestCase):
)
for value in inputs:
self.assertEqual(doc.add_unit(value), '')
+
+class ViewportUnitTestCase(TestCase):
+ def assertFromVPUnit(self, width_unit, test_value, unit, expected): # pylint: disable=invalid-name
+ """Checks a viewport unit and a test_value against the expected result"""
+ doc = svg_unit_scaled(width_unit)
+ self.assertEqual(doc.unit, width_unit, msg=svg)
+ self.assertAlmostEqual(doc.viewport_to_unit(test_value, unit), expected)
+
+ def assertToVPUnit(self, user_unit, value, unit, expected): # pylint: disable=invalid-name
+ """Check converting from a user unity for the test_value"""
+ self.assertAlmostEqual(svg_unit_scaled(user_unit).unit_to_viewport(value, unit), expected)
+
+ def test_unittovp(self):
+ """1in is ~2.54cm"""
+ self.assertToVPUnit('px', '1in', "px", 96)
+ self.assertToVPUnit('px', '1', "px", 1)
+
+ # 1 in = 96 px = 96 * 96 px/in / 2.54 cm/in on the viewport
+ self.assertToVPUnit('cm', '1in', "px", 96 * 96 / 2.54,)
+ self.assertToVPUnit('cm', '1in', "in", 96 / 2.54, )
+ self.assertToVPUnit('mm', '4', "mm", 4)
+ self.assertToVPUnit('mm', '4', "px", 4 * 96 / 25.4)
+
+ def test_vptounit(self):
+ self.assertFromVPUnit('mm', "1m", "px", 1000)
+ self.assertFromVPUnit('mm', "4mm", 'px', 4)
+ self.assertFromVPUnit('mm', "4mm", 'mm', 4 * 25.4/96)
diff --git a/tests/test_lorem_ipsum.py b/tests/test_lorem_ipsum.py
index bbcad68c2561648f872a25ba87dde8a6bb1130de..b99932a91a73d1b934f523938d50d05954e05832 100644
--- a/tests/test_lorem_ipsum.py
+++ b/tests/test_lorem_ipsum.py
@@ -6,3 +6,8 @@ class LorumIpsumBasicTest(ComparisonMixin, TestCase):
effect_class = LoremIpsum
compare_file = "svg/shapes.svg"
comparisons = [(), ["--svg2=false"], ["--id=r1"], ["--id=r1", "--svg2=false"], ["--id=t4"]]
+
+class LoremIpsumMillimeters(ComparisonMixin, TestCase):
+ effect_class = LoremIpsum
+ compare_file = "svg/complextransform.test.svg"
+ comparisons = [("--svg2=true",)]
diff --git a/tests/test_restack.py b/tests/test_restack.py
index 738eb92b60d8f2a1d18b79c4fe6a2e25a4d5817d..3129ec5f05a6ced9f3df9a65defff65da0c64f18 100644
--- a/tests/test_restack.py
+++ b/tests/test_restack.py
@@ -13,3 +13,10 @@ class RestackBasicTest(ComparisonMixin, TestCase):
('--nb_direction=custom', '--angle=50.0', '--id=s1', '--id=p1', '--id=c3',
'--id=slicerect1') + old_defaults,
]
+
+class RestackMillimeterGrouped(ComparisonMixin, TestCase):
+ """Test for https://gitlab.com/inkscape/extensions/-/issues/372"""
+ effect_class = Restack
+ compare_file = "svg/restack_grouped.svg"
+ comparisons = [('--id=g20858', '--id=g21085', '--id=g20940', '--id=g26580', '--id=g21081',
+ '--id=g20854'),]
\ No newline at end of file