summaryrefslogtreecommitdiffstats
path: root/share/extensions/docs/authors/units.rst
blob: 055fbfd9784bb72015e4dfbf187e4d590a4bd3f9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
.. _units:

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 `<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 which explains the **viewport coordinate system**, 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='http://www.w3.org/2000/svg' width="200" height="100">
        <rect x="0" y="0" width="200" height="100" fill="#aaa"/>
    </svg>

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 within the area with the 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
an 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='http://www.w3.org/2000/svg' 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"/>
    </svg>

.. 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 didn't change the core statement "no unit = user unit = 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 convenient 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:`to_dimensionless <inkex.elements._base.BaseElement.to_dimensionless>`. This method parses a 
``length`` value and returns it, converted to px (user units). 

:meth:`~inkex.elements._base.BaseElement.to_dimensionless` fulfils the following task:
**Convert this string from the XML into a number, while processing the unit.
When using this function on any SVG attribute and replacing the
original value with the result, the output doesn't change visually.**

In these and the following examples, the above "business card" SVG will be used.

>>> svg = inkex.load_svg("docs/samples/units2.svg").getroot()
>>> svg.to_dimensionless(svg.getElementById("c1").get("cx"))
21.0
>>> svg.to_dimensionless(svg.getElementById("c2").get("cx")) 
21.0
>>> svg.to_dimensionless(svg.getElementById("c3").get("cx"))
79.370078

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
4.0

What is the dimension of an object in a specified unit in the user coordinate system?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

You can also convert from user units to any unit. This is done using 
:meth:`BaseElement.to_dimensional <inkex.elements._base.BaseElement.to_dimensional>`. 

>>> svg.to_dimensional(svg.getElementById("c2").radius, "px")
4.0
>>> svg.to_dimensional(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 <inkex.elements._base.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

In other words, ``unit_to_viewport(value, unit="px")`` answers the following
question: **What does the the width/height widget of the selection
tool (set to** ``unit`` **) show when selecting an element with width**
``value`` **as defined in the SVG?** Consider again 
``<svg width="210mm" viewBox="0 0 105 147.5"><rect width="100" height="100"/></svg>``
, i.e. a "mm-based" document with scale=2. When selecting this rectangle, the rectangle tool 
shows ``viewport_to_unit("100", unit="mm") = 200``, if the rectangle tool is set to 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.viewport_to_unit <inkex.elements._base.BaseElement.viewport_to_unit>`.

>>> 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 will raise an error if the element is unrooted.

In other words, ``viewport_to_unit(value, target_unit="px")`` answers the following question: 
**What is the SVG representation of entering** ``value`` **in the width/height widget of the 
selection tool (set to the unit of value)?** Consider 
``<svg width="210mm" viewBox="0 0 105 147.5"><rect width="?" height="?"/></svg>``,
i.e. a "mm-based" SVG with scale=2. When typing ``200`` in the
rectangle tool, set to mm, the XML editor shows ``100`` =
``100px``. That's what ``viewport_to_unit("200mm") = 100`` does.

Note that this is different than
``viewport_to_unit("200", "mm")``, which would be for a rectangle
with a width (in the width/height widget of the rectangle tool) of
200 (px), while writing the width in ``mm`` *in the SVG*:
``<rect width="7.00043mm" height="7.00043mm"/>``.

Document dimensions
^^^^^^^^^^^^^^^^^^^^^^^^

* :attr:`SvgDocumentElement.viewport_width <inkex.elements._svg.SvgDocumentElement.viewport_width>`
  and  
  :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)``

.. code-block: 
    84mm          *   96px/in / (25.4mm/in) = 317.480314
    [output size]     [resolution 96dpi]      [output size in pixels]

* :attr:`SvgDocumentElement.viewbox_width <inkex.elements._svg.SvgDocumentElement.viewbox_height>`
  and  
  :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.to_dimensionless <inkex.elements._base.BaseElement.to_dimensionless>`. 
:meth:`BaseElement.to_dimensional <inkex.elements._base.BaseElement.to_dimensional>` 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")  
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.