summaryrefslogtreecommitdiffstats
path: root/share/extensions/inkex/units.py
blob: fec0e87e2badb92e5a46b8c427964317dc96ec74 (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
# -*- coding: utf-8 -*-
#
# Copyright (c) Aaron Spike <aaron@ekips.org>
#               Aurélio A. Heckert <aurium(a)gmail.com>
#               Bulia Byak <buliabyak@users.sf.net>
#               Nicolas Dufour, nicoduf@yahoo.fr
#               Peter J. R. Moulder <pjrm@users.sourceforge.net>
#               Martin Owens <doctormo@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
"""
Convert to and from various units and find the closest matching unit.
"""

import re

# a dictionary of unit to user unit conversion factors
CONVERSIONS = {
    "in": 96.0,
    "pt": 1.3333333333333333,
    "px": 1.0,
    "mm": 3.779527559055118,
    "cm": 37.79527559055118,
    "m": 3779.527559055118,
    "km": 3779527.559055118,
    "Q": 0.94488188976378,
    "pc": 16.0,
    "yd": 3456.0,
    "ft": 1152.0,
    "": 1.0,  # Default px
}

# allowed unit types, including percentages, relative units, and others
# that are not suitable for direct conversion to a length.
# Note that this is _not_ an exhaustive list of allowed unit types.
UNITS = [
    "in",
    "pt",
    "px",
    "mm",
    "cm",
    "m",
    "km",
    "Q",
    "pc",
    "yd",
    "ft",
    "",
    "%",
    "em",
    "ex",
    "ch",
    "rem",
    "vw",
    "vh",
    "vmin",
    "vmax",
    "deg",
    "grad",
    "rad",
    "turn",
    "s",
    "ms",
    "Hz",
    "kHz",
    "dpi",
    "dpcm",
    "dppx",
]

UNIT_MATCH = re.compile(rf"({'|'.join(UNITS)})")
NUMBER_MATCH = re.compile(r"(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)")
BOTH_MATCH = re.compile(rf"^\s*{NUMBER_MATCH.pattern}\s*{UNIT_MATCH.pattern}\s*$")


def parse_unit(value, default_unit="px", default_value=None):
    """
    Takes a value such as 55.32px and returns (55.32, 'px')
    Returns default (None) if no match can be found
    """
    ret = BOTH_MATCH.match(str(value))
    if ret:
        return float(ret.groups()[0]), ret.groups()[-1] or default_unit
    return (default_value, default_unit) if default_value is not None else None


def are_near_relative(point_a, point_b, eps=0.01):
    """Return true if the points are near to eps"""
    return (point_a - point_b <= point_a * eps) and (
        point_a - point_b >= -point_a * eps
    )


def discover_unit(value, viewbox, default="px"):
    """Attempt to detect the unit being used based on the viewbox"""
    # Default 100px when width can't be parsed
    (value, unit) = parse_unit(value, default_value=100.0)
    if unit not in CONVERSIONS:
        return default
    this_factor = CONVERSIONS[unit] * value / viewbox

    # try to find the svgunitfactor in the list of units known. If we don't find
    # something, ...
    for unit, unit_factor in CONVERSIONS.items():
        if unit != "":
            # allow 1% error in factor
            if are_near_relative(this_factor, unit_factor, eps=0.01):
                return unit
    return default


def convert_unit(value, to_unit, default="px"):
    """Returns userunits given a string representation of units in another system

    Args:
        value: <length> string
        to_unit: unit to convert to
        default: if ``value`` contains no unit, what unit should be assumed.

            .. versionadded:: 1.1
    """
    value, from_unit = parse_unit(value, default_unit=default, default_value=0.0)
    if from_unit in CONVERSIONS and to_unit in CONVERSIONS:
        return (
            value * CONVERSIONS[from_unit] / CONVERSIONS.get(to_unit, CONVERSIONS["px"])
        )
    return 0.0


def render_unit(value, unit):
    """Checks and then renders a number with its unit"""
    try:
        if isinstance(value, str):
            (value, unit) = parse_unit(value, default_unit=unit)
        return f"{value:.6g}{ unit:s}"
    except TypeError:
        return ""