Commit 9e1302af authored by Jonathan Neuhauser's avatar Jonathan Neuhauser
Browse files

Fix unit conversions for uutounit and unittouu

parent 98f333de
......@@ -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'))
......@@ -11,6 +11,7 @@ Welcome to inkex's documentation!
:caption: Quickstart
.. toctree::
:maxdepth: 4
This diff is collapsed.
<svg xmlns=''
width="200" height="100">
<rect x="0" y="0" width="200" height="100" fill="#aaa"/>
\ No newline at end of file
<svg xmlns=''
width="84mm" height="56mm" viewBox="0 0 84 56" fill="none">
<rect x="0" y="0" width="84" height="56" stroke="orange" stroke-width="2px"/>
<path d="M 0, 14 H 84" stroke="black" stroke-width="0.2px"/>
<path d="M 21, 0 V 56" stroke="black" stroke-width="0.2px"/>
<circle id="c1" r="2" cx="21" cy="14" stroke="red" stroke-width="0.5"/>
<circle id="c2" r="4px" cx="21px" cy="14px" stroke="green" stroke-width="0.5px"/>
<circle id="c3" r="0.5mm" cx="21mm" cy="14mm" stroke="blue" stroke-width="0.5mm"/>
\ No newline at end of file
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 `<svg>` 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
<svg xmlns='' width="200" height="100">
<rect x="0" y="0" width="200" height="100" fill="#aaa"/>
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
<svg xmlns='' width="84mm" height="56mm" viewBox="0 0 84 56" fill="none">
<rect x="0" y="0" width="84" height="56" stroke="orange" stroke-width="2px"/>
<path d="M 0, 14 H 84" stroke="black" stroke-width="0.2px"/>
<path d="M 21, 0 V 56" stroke="black" stroke-width="0.2px"/>
<circle id="c1" r="2" cx="21" cy="14" stroke="red" stroke-width="0.5"/>
<circle id="c2" r="4px" cx="21px" cy="14px" stroke="green" stroke-width="0.5px"/>
<circle id="c3" r="0.5mm" cx="21mm" cy="14mm" stroke="blue" stroke-width="0.5mm"/>
.. 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 <inkex.elements._base.BaseElement>` has a method
:meth:`unittouu <inkex.elements._base.BaseElement.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"))
>>> svg.unittouu(svg.getElementById("c2").get("cx"))
>>> svg.unittouu(svg.getElementById("c3").get("cx"))
For some classes, e.g. :class:`Rectangle <inkex.elements._polygons.Rectangle>`, convenience
properties are available which do the conversion for you, e.g.
:attr:`Rectangle.left <inkex.elements._polygons.RectangleBase.left>`. Similarly there are some
properties for circles:
>>> svg.getElementById("c3").center
Vector2d(79.3701, 52.9134)
>>> svg.getElementById("c2").radius
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 <inkex.elements._base.BaseElement.uutounit>`.
>>> svg.uutounit(svg.getElementById("c2").radius, "px")
>>> svg.uutounit(svg.getElementById("c2").radius, "mm")
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 <inkex.elements._base.BaseElement.unit_to_viewport>`.
>>> svg.unit_to_viewport(svg.getElementById("c2").radius)
>>> svg.unit_to_viewport(svg.getElementById("c2").radius, "mm")
>>> svg.unit_to_viewport("4", "mm")
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 <inkex.elements._base.BaseElement.viewport to unit>`.
>>> svg.viewport_to_unit("4mm", "px")
>>> svg.viewport_to_unit("4mm", "mm")
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
>>>["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 <inkex.elements._svg.SvgDocumentElement.viewport_width>`
:attr:`SvgDocumentElement.viewport_height <inkex.elements._svg.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 <inkex.elements._svg.SvgDocumentElement.viewbox_height>`
:attr:`SvgDocumentElement.viewbox_height <inkex.elements._svg.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 <inkex.elements._base.BaseElement>`
because they use properties of the root SVG. For an unrooted SVG fragment,
:meth:`BaseElement.unittouu <inkex.elements._base.BaseElement.unittouu>`.
:meth:`BaseElement.uutounit <inkex.elements._base.BaseElement.uutounit>` work as well.
If you want to convert between arbitrary units, you can do so using the
:meth:`convert_unit <inkex.units.convert_units>` method:
>>> inkex.units.convert_unit("4mm", "px")
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]
.. [2]
.. [3]
.. [4]
.. [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
......@@ -112,7 +112,7 @@ class DxfTwelve(inkex.OutputExtension):
scale = 25.4 / 90.0
h = self.svg.height
h = self.svg.viewport_height
path = '//svg:path'
for node in self.svg.xpath(path):
......@@ -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')
......@@ -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
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:
return ('h', ' '.join(horz_guides)), ('v', ' '.join(vert_guides))
......@@ -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))
......@@ -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:
......@@ -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:
......@@ -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):
coords = ":".join([self.get_localised_string(dim) for dim in sli])
inkscape(self.options.input_file, export_area=coords, export_filename=filename)
def export_slices(self, slices):
......@@ -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"""
......@@ -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
......@@ -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)
def uutounit(value, to_unit='px'):
"""Convert a value given in user units (px) the given unit type"""
return convert_unit(value, to_unit)
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 """
......@@ -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)
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)
class PathEffect(BaseElement):
"""Inkscape LPE element"""
......@@ -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.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):
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')))
def center(self, value):
......@@ -201,7 +201,7 @@ class Circle(EllipseBase):
def radius(self):
return self.uutounit(self.get('r', '0'), 'px')
return self.unittouu(self.get('r', '0'))
def radius(self, value):
......@@ -218,7 +218,7 @@ class Ellipse(EllipseBase):
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')))
def radius(self, value):
......@@ -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
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