├── .github ├── FUNDING.yml └── workflows │ ├── codeql-analysis.yml │ └── unittests.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── pyproject.toml ├── requirements.txt ├── setup.cfg ├── setup.py ├── svgelements ├── __init__.py └── svgelements.py ├── test ├── __init__.py ├── test_angle.py ├── test_approximate.py ├── test_arc_length.py ├── test_bbox.py ├── test_clippath.py ├── test_color.py ├── test_copy.py ├── test_css.py ├── test_cubic_bezier.py ├── test_descriptive_elements.py ├── test_element.py ├── test_generation.py ├── test_group.py ├── test_image.py ├── test_intersections.py ├── test_length.py ├── test_matrix.py ├── test_parsing.py ├── test_path.py ├── test_path_dunder.py ├── test_path_segments.py ├── test_paths.py ├── test_point.py ├── test_quadratic_bezier.py ├── test_repr.py ├── test_shape.py ├── test_stroke_width.py ├── test_text.py ├── test_use.py ├── test_viewbox.py └── test_write.py └── tools └── build_pypi.cmd /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [tatarize] 2 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | paths: 7 | - '**.py' 8 | - '.github/workflows/codeql*.yml' 9 | pull_request: 10 | # The branches below must be a subset of the branches above 11 | branches: [master] 12 | paths: 13 | - '**.py' 14 | - '.github/workflows/codeql*.yml' 15 | schedule: 16 | - cron: '0 23 * * 5' 17 | 18 | concurrency: 19 | group: codeql-${{ github.ref }} 20 | cancel-in-progress: true 21 | 22 | defaults: 23 | run: 24 | shell: bash 25 | 26 | jobs: 27 | analyze: 28 | name: CodeQL 29 | runs-on: ubuntu-latest 30 | timeout-minutes: 10 31 | 32 | steps: 33 | - name: Checkout ${{ github.ref }} 34 | uses: actions/checkout@v2 35 | 36 | - name: Set up Python 3.9 37 | uses: actions/setup-python@v2 38 | with: 39 | python-version: '3.9' 40 | 41 | - name: Get detailed Python version 42 | id: full-python-version 43 | run: echo ::set-output name=version::$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))") 44 | 45 | - name: Python Cache - ${{ matrix.os }}-python-${{ steps.full-python-version.outputs.version }} 46 | uses: actions/cache@v2 47 | with: 48 | path: ${{ env.pythonLocation }} 49 | key: ${{ matrix.os }}-python-${{ steps.full-python-version.outputs.version }} 50 | 51 | - name: Install Python dependencies 52 | run: | 53 | python3 -m pip install --upgrade --upgrade-strategy eager pip setuptools wheel babel 54 | pip3 install --upgrade --upgrade-strategy eager pillow scipy numpy 55 | 56 | - name: List environment 57 | env: 58 | GITHUB_CONTEXT: ${{ toJSON(github) }} 59 | JOB_CONTEXT: ${{ toJSON(job) }} 60 | STEPS_CONTEXT: ${{ toJSON(steps) }} 61 | RUNNER_CONTEXT: ${{ toJSON(runner) }} 62 | STRATEGY_CONTEXT: ${{ toJSON(strategy) }} 63 | MATRIX_CONTEXT: ${{ toJSON(matrix) }} 64 | run: | 65 | pip3 list 66 | env 67 | 68 | # Initializes the CodeQL tools for scanning. 69 | - name: Initialize CodeQL 70 | uses: github/codeql-action/init@v1 71 | with: 72 | languages: python 73 | 74 | - name: Perform CodeQL Analysis 75 | uses: github/codeql-action/analyze@v1 -------------------------------------------------------------------------------- /.github/workflows/unittests.yml: -------------------------------------------------------------------------------- 1 | name: Unittest 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | paths: 7 | - '**.py' 8 | - '.github/workflows/unittests.yml' 9 | pull_request: 10 | branches: [master] 11 | paths: 12 | - '**.py' 13 | - '.github/workflows/unittests.yml' 14 | 15 | concurrency: 16 | group: unittests-${{ github.ref }} 17 | cancel-in-progress: true 18 | 19 | defaults: 20 | run: 21 | shell: bash 22 | 23 | jobs: 24 | unittests: 25 | 26 | name: ${{ matrix.os }}+py${{ matrix.python-version }} 27 | runs-on: ${{ matrix.os }} 28 | timeout-minutes: 10 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | os: [ubuntu-20.04, ubuntu-latest, macos-11] 33 | python-version: ['3.9', '3.11'] 34 | experimental: [false] 35 | include: 36 | - os: ubuntu-20.04 37 | python-version: 3.6 38 | - os: macos-11 39 | python-version: 3.6 40 | 41 | steps: 42 | 43 | - name: Checkout ${{ github.ref }} 44 | uses: actions/checkout@v2 45 | 46 | - name: Set up Python ${{ matrix.python-version }} 47 | uses: actions/setup-python@v4 48 | with: 49 | python-version: ${{ matrix.python-version }} 50 | 51 | - name: Get detailed Python version 52 | id: full-python-version 53 | run: echo ::set-output name=version::$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))") 54 | 55 | - name: Python Cache - ${{ matrix.os }}-python-${{ steps.full-python-version.outputs.version }} 56 | uses: actions/cache@v2 57 | with: 58 | path: ${{ env.pythonLocation }} 59 | key: new-${{ matrix.os }}-python-${{ steps.full-python-version.outputs.version }} 60 | 61 | - name: Install Python dependencies 62 | run: | 63 | python3 -m pip install --upgrade --upgrade-strategy eager pip setuptools wheel babel 64 | pip3 install --upgrade --upgrade-strategy eager pillow scipy numpy 65 | 66 | - name: List environment 67 | env: 68 | GITHUB_CONTEXT: ${{ toJSON(github) }} 69 | JOB_CONTEXT: ${{ toJSON(job) }} 70 | STEPS_CONTEXT: ${{ toJSON(steps) }} 71 | RUNNER_CONTEXT: ${{ toJSON(runner) }} 72 | STRATEGY_CONTEXT: ${{ toJSON(strategy) }} 73 | MATRIX_CONTEXT: ${{ toJSON(matrix) }} 74 | run: | 75 | pip3 list 76 | env 77 | 78 | - name: Run Unittests 79 | run: | 80 | python -m unittest discover test -v 81 | if ${{ matrix.experimental }} == true; then 82 | exit 0 83 | fi 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 meerk40t 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md 2 | include *.txt 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.mypy] 2 | color_output = true 3 | error_summary = true 4 | pretty = true 5 | show_error_context = true 6 | show_column_numbers = true 7 | soft_error_limit = 20 8 | warn_redundant_casts = true 9 | warn_return_any = true 10 | warn_unreachable = true 11 | warn_unused_configs = true 12 | warn_unused_ignores = true 13 | 14 | 15 | [tool.isort] 16 | profile = "black" 17 | line_length = 88 18 | src_paths = ["svgelements"] 19 | 20 | 21 | [tool.black] 22 | line-length = 88 23 | target-version = ['py36'] 24 | include = '\.pyi?$' 25 | 26 | 27 | [tool.flake8] 28 | filename = "*.py" 29 | count = "true" 30 | exclude = [ 31 | "*.pyc", 32 | "__pycache__" 33 | ] 34 | indent-size = 4 35 | max-complexity = 10 36 | max-line-length = 88 37 | show-source = "true" 38 | statistics = "true" 39 | 40 | 41 | [tool.pylint.master] 42 | # A comma-separated list of package or module names from where C extensions may 43 | # be loaded. Extensions are loading into the active Python interpreter and may 44 | # run arbitrary code. 45 | extension-pkg-whitelist = "" 46 | 47 | # Add files or directories to the blacklist. They should be base names, not 48 | # paths. 49 | ignore = "CVS" 50 | 51 | # Add files or directories matching the regex patterns to the blacklist. The 52 | # regex matches against base names, not paths. 53 | ignore-patterns = "" 54 | 55 | # Python code to execute, usually for sys.path manipulation such as 56 | # pygtk.require(). 57 | #init-hook = "" 58 | 59 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 60 | # number of processors available to use. 61 | jobs = 0 62 | 63 | # Control the amount of potential inferred values when inferring a single 64 | # object. This can help the performance when dealing with large functions or 65 | # complex, nested conditions. 66 | limit-inference-results = 100 67 | 68 | # List of plugins (as comma separated values of python modules names) to load, 69 | # usually to register additional checkers. 70 | load-plugins = "" 71 | 72 | # Pickle collected data for later comparisons. 73 | persistent = true 74 | 75 | # Specify a configuration file. 76 | #rcfile = "" 77 | 78 | # When enabled, pylint would attempt to guess common misconfiguration and emit 79 | # user-friendly hints instead of false-positive error messages. 80 | suggestion-mode = true 81 | 82 | # Allow loading of arbitrary C extensions. Extensions are imported into the 83 | # active Python interpreter and may run arbitrary code. 84 | unsafe-load-any-extension = false 85 | 86 | 87 | [tool.pylint.'messages control'] 88 | # Only show warnings with the listed confidence levels. Leave empty to show 89 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 90 | confidence = "" 91 | 92 | # Disable the message, report, category or checker with the given id(s). You 93 | # can either give multiple identifiers separated by comma (,) or put this 94 | # option multiple times (only on the command line, not in the configuration 95 | # file where it should appear only once). You can also use "--disable = all" to 96 | # disable everything first and then reenable specific checks. For example, if 97 | # you want to run only the similarities checker, you can use "--disable = all 98 | # --enable = similarities". If you want to run only the classes checker, but have 99 | # no Warning level messages displayed, use "--disable = all --enable = classes 100 | # --disable = W". 101 | #disable = "all" 102 | enable = "all" 103 | 104 | # Enable the message, report, category or checker with the given id(s). You can 105 | # either give multiple identifier separated by comma (,) or put this option 106 | # multiple time (only on the command line, not in the configuration file where 107 | # it should appear only once). See also the "--disable" option for examples. 108 | #enable = [ 109 | # consider-using-enumerate, 110 | # format-combined-specification, 111 | # return-in-init, 112 | # catching-non-exception, 113 | # bad-except-order, 114 | # unexpected-special-method-signature, 115 | # # Enforce list comprehensions 116 | # # Newline at EOF 117 | # raising-bad-type, 118 | # raising-non-exception, 119 | # format-needs-mapping, 120 | # invalid-all-object, 121 | # bad-super-call, 122 | # nonexistent-operator, 123 | # missing-kwoa, 124 | # missing-format-argument-key, 125 | # init-is-generator, 126 | # access-member-before-definition, 127 | # used-before-assignment, 128 | # redundant-keyword-arg, 129 | # assert-on-tuple, 130 | # assignment-from-no-return, 131 | # expression-not-assigned, 132 | # misplaced-bare-raise, 133 | # redefined-argument-from-local, 134 | # not-in-loop, 135 | # bad-exception-context, 136 | # unidiomatic-typecheck, 137 | # no-staticmethod-decorator, 138 | # nonlocal-and-global, 139 | # confusing-with-statement, 140 | # global-variable-undefined, 141 | # global-variable-not-assigned, 142 | # inconsistent-mro, 143 | # no-classmethod-decorator, 144 | # nonlocal-without-binding, 145 | # duplicate-bases, 146 | # duplicate-argument-name, 147 | # duplicate-key, 148 | # useless-else-on-loop, 149 | # arguments-differ, 150 | # logging-too-many-args, 151 | # too-few-format-args, 152 | # bad-format-string-key, 153 | # invalid-sequence-index, 154 | # inherit-non-class, 155 | # bad-format-string, 156 | # invalid-format-index, 157 | # invalid-star-assignment-target, 158 | # no-method-argument, 159 | # no-value-for-parameter, 160 | # missing-format-attribute, 161 | # logging-too-few-args, 162 | # too-few-format-args, 163 | # mixed-format-string, 164 | # # Old style class 165 | # logging-format-truncated, 166 | # truncated-format-string, 167 | # notimplemented-raised, 168 | # # Builtin redefined 169 | # function-redefined, 170 | # reimported, 171 | # repeated-keyword, 172 | # lost-exception, 173 | # return-outside-function, 174 | # return-arg-in-generator, 175 | # non-iterator-returned, 176 | # method-hidden, 177 | # too-many-star-expressions, 178 | # trailing-whitespace, 179 | # unexpected-keyword-arg, 180 | # missing-format-string-key, 181 | # unnecessary-lambda, 182 | # unnecessary-pass, 183 | # unreachable, 184 | # logging-unsupported-format, 185 | # bad-format-character, 186 | # unused-import, 187 | # exec-used, 188 | # pointless-statement, 189 | # pointless-string-statement, 190 | # undefined-all-variable, 191 | # misplaced-future, 192 | # continue-in-finally, 193 | # invalid-slots, 194 | # invalid-slice-index, 195 | # invalid-slots-object, 196 | # star-needs-assignment-target, 197 | # global-at-module-level, 198 | # yield-outside-function, 199 | # mixed-indentation, 200 | # non-parent-init-called, 201 | # bare-except, 202 | # no-self-use, 203 | # dangerous-default-value, 204 | # arguments-differ, 205 | # signature-differs, 206 | # duplicate-except, 207 | # abstract-class-instantiated, 208 | # binary-op-exception, 209 | # undefined-variable 210 | #] 211 | 212 | 213 | [tool.pylint.reports] 214 | # Python expression which should return a note less than 10 (10 is the highest 215 | # note). You have access to the variables errors warning, statement which 216 | # respectively contain the number of errors / warnings messages and the total 217 | # number of statements analyzed. This is used by the global evaluation report 218 | # (RP0004). Default is: 219 | # evaluation = 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 220 | # evaluation = 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 221 | 222 | # Template used to display messages. This is a python new-style format string 223 | # used to format the message information. See doc for all details. 224 | #msg-template = "" 225 | 226 | # Set the output format. Available formats are text, parseable, colorized, json 227 | # and msvs (visual studio). You can also give a reporter class, e.g. 228 | # mypackage.mymodule.MyReporterClass. 229 | #output-format = text 230 | output-format = "colorized" 231 | 232 | # Tells whether to display a full report or only the messages. 233 | #reports = false 234 | reports = true 235 | 236 | # Activate the evaluation score. 237 | score = true 238 | 239 | 240 | [tool.pylint.refactoring] 241 | # Maximum number of nested blocks for function / method body 242 | max-nested-blocks = 5 243 | 244 | # Complete name of functions that never returns. When checking for 245 | # inconsistent-return-statements if a never returning function is called then 246 | # it will be considered as an explicit return statement and no message will be 247 | # printed. 248 | never-returning-functions = ["sys.exit"] 249 | 250 | 251 | [tool.pylint.format] 252 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 253 | expected-line-ending-format = "LF" 254 | 255 | # Regexp for a line that is allowed to be longer than the limit. 256 | #ignore-long-lines = "^\s*(# )??$" 257 | 258 | # Number of spaces of indent required inside a hanging or continued line. 259 | indent-after-paren = 4 260 | 261 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 262 | # tab). 263 | indent-string = " " 264 | 265 | # Maximum number of characters on a single line. 266 | max-line-length = 88 267 | 268 | # Maximum number of lines in a module. 269 | max-module-lines = 1000 270 | 271 | # List of optional constructs for which whitespace checking is disabled. 272 | # `dict-separator` is used to allow tabulation in dicts, etc. e.g. 273 | # {1 : 1, 274 | # 222: 2}. 275 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 276 | # `empty-line` allows space-only lines. 277 | no-space-check = [ 278 | "trailing-comma", 279 | "dict-separator" 280 | ] 281 | 282 | # Allow the body of a class to be on the same line as the declaration if body 283 | # contains single statement. 284 | single-line-class-stmt = false 285 | 286 | # Allow the body of an if to be on the same line as the test if there is no 287 | # else. 288 | single-line-if-stmt = false 289 | 290 | 291 | [tool.pylint.spelling] 292 | # Limits count of emitted suggestions for spelling mistakes. 293 | max-spelling-suggestions = 4 294 | 295 | # Spelling dictionary name. Available dictionaries: none. To make it working 296 | # install python-enchant package.. 297 | spelling-dict = "" 298 | 299 | # List of comma separated words that should not be checked. 300 | spelling-ignore-words = "" 301 | 302 | # A path to a file that contains private dictionary; one word per line. 303 | spelling-private-dict-file = "" 304 | 305 | # Tells whether to store unknown words to indicated private dictionary in 306 | # --spelling-private-dict-file option instead of raising a message. 307 | spelling-store-unknown-words = false 308 | 309 | 310 | [tool.pylint.similarities] 311 | # Ignore comments when computing similarities. 312 | ignore-comments = true 313 | 314 | # Ignore docstrings when computing similarities. 315 | ignore-docstrings = true 316 | 317 | # Ignore imports when computing similarities. 318 | ignore-imports = false 319 | 320 | # Minimum lines number of a similarity. 321 | min-similarity-lines = 4 322 | 323 | 324 | [tool.pylint.variables] 325 | # List of additional names supposed to be defined in builtins. Remember that 326 | # you should avoid defining new builtins when possible. 327 | additional-builtins = [ 328 | "_", 329 | "N_", 330 | "ngettext", 331 | "gettext_countries", 332 | "gettext_attributes" 333 | ] 334 | 335 | # Tells whether unused global variables should be treated as a violation. 336 | allow-global-unused-variables = true 337 | 338 | # List of strings which can identify a callback function by name. A callback 339 | # name must start or end with one of those strings. 340 | callbacks = [ 341 | "cb_", 342 | "_cb" 343 | ] 344 | 345 | # A regular expression matching the name of dummy variables (i.e. expected to 346 | # not be used). 347 | dummy-variables-rgx = "_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_" 348 | 349 | # Argument names that match this expression will be ignored. Default to name 350 | # with leading underscore. 351 | ignored-argument-names = "_.*|^ignored_|^unused_" 352 | 353 | # Tells whether we should check for unused import in __init__ files. 354 | init-import = true 355 | 356 | # List of qualified module names which can have objects that can redefine 357 | # builtins. 358 | redefining-builtins-modules = [ 359 | "six.moves", 360 | "past.builtins", 361 | "future.builtins", 362 | "builtins", 363 | "io" 364 | ] 365 | 366 | 367 | [tool.pylint.miscellaneous] 368 | # List of note tags to take in consideration, separated by a comma. 369 | notes = [ 370 | "FIXME", 371 | "XXX", 372 | "TODO" 373 | ] 374 | 375 | 376 | [tool.pylint.logging] 377 | # Format style used to check logging format string. `old` means using % 378 | # formatting, while `new` is for `{}` formatting. 379 | logging-format-style = "old" 380 | 381 | # Logging modules to check that the string format arguments are in logging 382 | # function parameter format. 383 | #logging-modules = "logging" 384 | logging-modules = "" 385 | 386 | 387 | [tool.pylint.basic] 388 | # Naming style matching correct argument names. 389 | argument-naming-style = "snake_case" 390 | 391 | # Regular expression matching correct argument names. Overrides argument- 392 | # naming-style. 393 | #argument-rgx = "" 394 | 395 | # Naming style matching correct attribute names. 396 | attr-naming-style = "snake_case" 397 | 398 | # Regular expression matching correct attribute names. Overrides attr-naming- 399 | # style. 400 | #attr-rgx = "" 401 | 402 | # Bad variable names which should always be refused, separated by a comma. 403 | bad-names = [ 404 | "foo", 405 | "bar", 406 | "baz", 407 | "toto", 408 | "tutu", 409 | "tata" 410 | ] 411 | 412 | # Naming style matching correct class attribute names. 413 | class-attribute-naming-style = "any" 414 | 415 | # Regular expression matching correct class attribute names. Overrides class- 416 | # attribute-naming-style. 417 | #class-attribute-rgx = "" 418 | 419 | # Naming style matching correct class names. 420 | class-naming-style = "PascalCase" 421 | 422 | # Regular expression matching correct class names. Overrides class-naming- 423 | # style. 424 | #class-rgx = "" 425 | 426 | # Naming style matching correct constant names. 427 | const-naming-style = "UPPER_CASE" 428 | 429 | # Regular expression matching correct constant names. Overrides const-naming- 430 | # style. 431 | #const-rgx = "" 432 | 433 | # Minimum line length for functions/classes that require docstrings, shorter 434 | # ones are exempt. 435 | docstring-min-length = -1 436 | 437 | # Naming style matching correct function names. 438 | function-naming-style = "snake_case" 439 | 440 | # Regular expression matching correct function names. Overrides function- 441 | # naming-style. 442 | #function-rgx = "" 443 | 444 | # Good variable names which should always be accepted, separated by a comma. 445 | good-names = [ 446 | "i", 447 | "j", 448 | "k", 449 | "_" 450 | ] 451 | 452 | # Include a hint for the correct naming format with invalid-name. 453 | include-naming-hint = false 454 | 455 | # Naming style matching correct inline iteration names. 456 | inlinevar-naming-style = "any" 457 | 458 | # Regular expression matching correct inline iteration names. Overrides 459 | # inlinevar-naming-style. 460 | #inlinevar-rgx = "" 461 | 462 | # Naming style matching correct method names. 463 | method-naming-style = "snake_case" 464 | 465 | # Regular expression matching correct method names. Overrides method-naming- 466 | # style. 467 | #method-rgx = "" 468 | 469 | # Naming style matching correct module names. 470 | module-naming-style = "snake_case" 471 | 472 | # Regular expression matching correct module names. Overrides module-naming- 473 | # style. 474 | #module-rgx = "" 475 | 476 | # Colon-delimited sets of names that determine each other's naming style when 477 | # the name regexes allow several styles. 478 | name-group = "" 479 | 480 | # Regular expression which should only match function or class names that do 481 | # not require a docstring. 482 | no-docstring-rgx = "^_" 483 | 484 | # List of decorators that produce properties, such as abc.abstractproperty. Add 485 | # to this list to register other decorators that produce valid properties. 486 | # These decorators are taken in consideration only for invalid-name. 487 | property-classes = "abc.abstractproperty" 488 | 489 | # Naming style matching correct variable names. 490 | variable-naming-style = "snake_case" 491 | 492 | # Regular expression matching correct variable names. Overrides variable- 493 | # naming-style. 494 | #variable-rgx = "" 495 | 496 | 497 | [tool.pylint.typecheck] 498 | # List of decorators that produce context managers, such as 499 | # contextlib.contextmanager. Add to this list to register other decorators that 500 | # produce valid context managers. 501 | contextmanager-decorators = ["contextlib.contextmanager"] 502 | 503 | # List of members which are set dynamically and missed by pylint inference 504 | # system, and so shouldn't trigger E1101 when accessed. Python regular 505 | # expressions are accepted. 506 | generated-members = "" 507 | 508 | # Tells whether missing members accessed in mixin class should be ignored. A 509 | # mixin class is detected if its name ends with "mixin" (case insensitive). 510 | ignore-mixin-members = true 511 | 512 | # Tells whether to warn about missing members when the owner of the attribute 513 | # is inferred to be None. 514 | ignore-none = true 515 | 516 | # This flag controls whether pylint should warn about no-member and similar 517 | # checks whenever an opaque object is returned when inferring. The inference 518 | # can return multiple potential results while evaluating a Python object, but 519 | # some branches might not be evaluated, which results in partial inference. In 520 | # that case, it might be useful to still emit no-member and other checks for 521 | # the rest of the inferred objects. 522 | ignore-on-opaque-inference = true 523 | 524 | # List of class names for which member attributes should not be checked (useful 525 | # for classes with dynamically set attributes). This supports the use of 526 | # qualified names. 527 | ignored-classes = [ 528 | "optparse.Values", 529 | "thread._local", 530 | "_thread._local" 531 | ] 532 | 533 | # List of module names for which member attributes should not be checked 534 | # (useful for modules/projects where namespaces are manipulated during runtime 535 | # and thus existing member attributes cannot be deduced by static analysis. It 536 | # supports qualified module names, as well as Unix pattern matching. 537 | ignored-modules = "" 538 | 539 | # Show a hint with possible names when a member name was not found. The aspect 540 | # of finding the hint is based on edit distance. 541 | missing-member-hint = true 542 | 543 | # The minimum edit distance a name should have in order to be considered a 544 | # similar match for a missing member name. 545 | missing-member-hint-distance = 1 546 | 547 | # The total number of similar names that should be taken in consideration when 548 | # showing a hint for a missing member. 549 | missing-member-max-choices = 1 550 | 551 | 552 | [tool.pylint.classes] 553 | # List of method names used to declare (i.e. assign) instance attributes. 554 | defining-attr-methods = [ 555 | "__init__", 556 | "__new__" 557 | ] 558 | 559 | # List of member names, which should be excluded from the protected access 560 | # warning. 561 | #exclude-protected = [ 562 | # _asdict, 563 | # _fields, 564 | # _replace, 565 | # _source, 566 | # _make 567 | #] 568 | exclude-protected = "" 569 | 570 | # List of valid names for the first argument in a class method. 571 | valid-classmethod-first-arg = "cls" 572 | 573 | # List of valid names for the first argument in a metaclass class method. 574 | valid-metaclass-classmethod-first-arg = "cls" 575 | 576 | 577 | [tool.pylint.imports] 578 | # Allow wildcard imports from modules that define __all__. 579 | allow-wildcard-with-all = true 580 | 581 | # Analyse import fallback blocks. This can be used to support both Python 2 and 582 | # 3 compatible code, which means that the block might have code that exists 583 | # only in one or another interpreter, leading to false positives when analysed. 584 | analyse-fallback-blocks = false 585 | 586 | # Deprecated modules which should not be used, separated by a comma. 587 | deprecated-modules = [ 588 | "optparse", 589 | "tkinter.tix" 590 | ] 591 | 592 | # Create a graph of external dependencies in the given file (report RP0402 must 593 | # not be disabled). 594 | ext-import-graph = "" 595 | 596 | # Create a graph of every (i.e. internal and external) dependencies in the 597 | # given file (report RP0402 must not be disabled). 598 | import-graph = "" 599 | 600 | # Create a graph of internal dependencies in the given file (report RP0402 must 601 | # not be disabled). 602 | int-import-graph = "" 603 | 604 | # Force import order to recognize a module as part of the standard 605 | # compatibility libraries. 606 | known-standard-library = "" 607 | 608 | # Force import order to recognize a module as part of a third party library. 609 | known-third-party = "" 610 | 611 | 612 | [tool.pylint.design] 613 | # Maximum number of arguments for function / method. 614 | max-args = 5 615 | 616 | # Maximum number of attributes for a class (see R0902). 617 | max-attributes = 7 618 | 619 | # Maximum number of boolean expressions in an if statement. 620 | max-bool-expr = 5 621 | 622 | # Maximum number of branch for function / method body. 623 | max-branches = 12 624 | 625 | # Maximum number of locals for function / method body. 626 | max-locals = 15 627 | 628 | # Maximum number of parents for a class (see R0901). 629 | max-parents = 7 630 | 631 | # Maximum number of public methods for a class (see R0904). 632 | max-public-methods = 20 633 | 634 | # Maximum number of return / yield for function / method body. 635 | max-returns = 6 636 | 637 | # Maximum number of statements in function / method body. 638 | max-statements = 50 639 | 640 | # Minimum number of public methods for a class (see R0903). 641 | min-public-methods = 2 642 | 643 | 644 | [tool.pylint.exceptions] 645 | # Exceptions that will emit a warning when being caught. Defaults to 646 | # "Exception". 647 | #overgeneral-exceptions = ["Exception"] 648 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = svgelements 3 | version = 1.9.6 4 | description = Svg Elements Parsing 5 | long_description_content_type=text/markdown 6 | long_description = file: README.md 7 | classifiers = 8 | Development Status :: 5 - Production/Stable 9 | Intended Audience :: Developers 10 | License :: OSI Approved :: MIT License 11 | Operating System :: OS Independent 12 | Programming Language :: Python 13 | Programming Language :: Python :: 3.6 14 | Programming Language :: Python :: 3.7 15 | Programming Language :: Python :: 3.8 16 | Programming Language :: Python :: 3.9 17 | Programming Language :: Python :: 3.10 18 | Programming Language :: Python :: 3.11 19 | Topic :: Multimedia :: Graphics 20 | Topic :: Multimedia :: Graphics :: Editors :: Vector-Based 21 | Topic :: Software Development :: Libraries :: Python Modules 22 | Topic :: Utilities 23 | keywords = svg, path, elements, matrix, vector, parser 24 | author = Tatarize 25 | author_email = tatarize@gmail.com 26 | url = https://github.com/meerk40t/svgelements 27 | license = MIT 28 | 29 | [options] 30 | zip_safe = True 31 | include_package_data = True 32 | packages = find: 33 | package_dir = 34 | = . 35 | test_suite = test 36 | 37 | [pep8] 38 | max-line-length=100 39 | 40 | [bdist_wheel] 41 | universal=1 42 | 43 | [options.packages.find] 44 | exclude = test 45 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() -------------------------------------------------------------------------------- /svgelements/__init__.py: -------------------------------------------------------------------------------- 1 | from .svgelements import * 2 | 3 | name = "svgelements" 4 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | name = "tests" 2 | -------------------------------------------------------------------------------- /test/test_angle.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from svgelements import * 4 | 5 | 6 | class TestElementAngle(unittest.TestCase): 7 | """These tests ensure the basic functions of the Angle element.""" 8 | 9 | def test_angle_init(self): 10 | self.assertEqual(Angle.degrees(90).as_turns, 0.25) 11 | self.assertEqual(Angle.degrees(180).as_turns, 0.50) 12 | self.assertEqual(Angle.degrees(360).as_turns, 1.0) 13 | self.assertEqual(Angle.degrees(720).as_turns, 2.0) 14 | self.assertEqual(Angle.radians(tau).as_turns, 1.0) 15 | self.assertEqual(Angle.radians(tau / 50.0).as_turns, 1.0 / 50.0) 16 | self.assertEqual(Angle.gradians(100).as_turns, 0.25) 17 | self.assertEqual(Angle.turns(100).as_turns, 100) 18 | self.assertEqual(Angle.gradians(100).as_gradians, 100) 19 | self.assertEqual(Angle.degrees(100).as_degrees, 100) 20 | self.assertEqual(Angle.radians(100).as_radians, 100) 21 | self.assertEqual(Angle.parse("90deg").as_radians, tau / 4.0) 22 | self.assertEqual(Angle.parse("90turn").as_radians, tau * 90) 23 | 24 | def test_angle_equal(self): 25 | self.assertEqual(Angle.degrees(0), Angle.degrees(-360)) 26 | self.assertEqual(Angle.degrees(0), Angle.degrees(360)) 27 | self.assertEqual(Angle.degrees(0), Angle.degrees(1080)) 28 | self.assertNotEqual(Angle.degrees(0), Angle.degrees(180)) 29 | self.assertEqual(Angle.degrees(0), Angle.turns(5)) 30 | 31 | def test_orth(self): 32 | self.assertTrue(Angle.degrees(0).is_orthogonal()) 33 | self.assertTrue(Angle.degrees(90).is_orthogonal()) 34 | self.assertTrue(Angle.degrees(180).is_orthogonal()) 35 | self.assertTrue(Angle.degrees(270).is_orthogonal()) 36 | self.assertTrue(Angle.degrees(360).is_orthogonal()) 37 | 38 | self.assertFalse(Angle.degrees(1).is_orthogonal()) 39 | self.assertFalse(Angle.degrees(91).is_orthogonal()) 40 | self.assertFalse(Angle.degrees(181).is_orthogonal()) 41 | self.assertFalse(Angle.degrees(271).is_orthogonal()) 42 | self.assertFalse(Angle.degrees(361).is_orthogonal()) 43 | -------------------------------------------------------------------------------- /test/test_approximate.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from random import * 3 | 4 | from svgelements import * 5 | 6 | 7 | def get_random_cubic_bezier(): 8 | return CubicBezier((random() * 50, random() * 50), (random() * 50, random() * 50), 9 | (random() * 50, random() * 50), (random() * 50, random() * 50)) 10 | 11 | 12 | class TestElementApproximation(unittest.TestCase): 13 | 14 | def test_cubic_bezier_arc_approximation(self): 15 | n = 50 16 | for _ in range(n): 17 | b = get_random_cubic_bezier() 18 | path = Move(b.start) + Path([b]) 19 | path2 = Path(path) 20 | path2.approximate_bezier_with_circular_arcs(error=0.001) 21 | path2.approximate_arcs_with_cubics(error=0.001) 22 | -------------------------------------------------------------------------------- /test/test_arc_length.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from random import * 3 | 4 | from svgelements import * 5 | 6 | 7 | def get_random_arc(): 8 | return Arc((random() * 50, random() * 50), 9 | random() * 48 + 2, random() * 48 + 2, 10 | int(random() * 180), 11 | int(random() * 2), int(random() * 2), 12 | (random() * 50, random() * 50)) 13 | 14 | 15 | def get_random_circle_arc(): 16 | r = random() * 48 + 2 17 | return Arc((random() * 50, random() * 50), 18 | r, r, 19 | int(random() * 180), 20 | int(random() * 2), int(random() * 2), 21 | (random() * 50, random() * 50)) 22 | 23 | 24 | class TestElementArcLength(unittest.TestCase): 25 | 26 | def test_arc_angle_point(self): 27 | for i in range(1000): 28 | ellipse = Ellipse((0, 0), 2, 1) 29 | angle = random() * tau / 2 - tau / 4 30 | 31 | p = ellipse.point_at_angle(angle) 32 | a = ellipse.angle_at_point(p) 33 | self.assertAlmostEqual(angle, a) 34 | 35 | def test_arc_angle_point_rotated(self): 36 | for i in range(1000): 37 | ellipse = Ellipse((0, 0), 2, 1, "rotate(45deg)") 38 | angle = random() * tau / 2 - tau / 4 39 | 40 | p = ellipse.point_at_angle(angle) 41 | a = ellipse.angle_at_point(p) 42 | self.assertAlmostEqual(angle, a) 43 | 44 | def test_arc_angles(self): 45 | for i in range(1000): 46 | ellipse = Ellipse((0, 0), 2, 1) 47 | start = random() * tau / 2 - tau / 4 48 | end = random() * tau / 2 - tau / 4 49 | 50 | p = ellipse.point_at_angle(start) 51 | a = ellipse.angle_at_point(p) 52 | self.assertAlmostEqual(start, a) 53 | 54 | p = ellipse.point_at_angle(end) 55 | a = ellipse.angle_at_point(p) 56 | self.assertAlmostEqual(end, a) 57 | 58 | arc = ellipse.arc_angle(start, end) 59 | self.assertAlmostEqual(arc.get_start_angle(), start) 60 | self.assertAlmostEqual(arc.get_end_angle(), end) 61 | 62 | def test_arc_t(self): 63 | for i in range(1000): 64 | ellipse = Ellipse((0, 0), 2, 1) 65 | start = random() * tau / 2 - tau / 4 66 | end = random() * tau / 2 - tau / 4 67 | 68 | p = ellipse.point_at_t(start) 69 | a = ellipse.t_at_point(p) 70 | self.assertAlmostEqual(start, a) 71 | 72 | p = ellipse.point_at_t(end) 73 | a = ellipse.t_at_point(p) 74 | self.assertAlmostEqual(end, a) 75 | 76 | arc = ellipse.arc_t(start, end) 77 | self.assertAlmostEqual(arc.get_start_t(), start) 78 | self.assertAlmostEqual(arc.get_end_t(), end) 79 | 80 | def test_arc_angles_rotated(self): 81 | for i in range(1000): 82 | ellipse = Ellipse((0, 0), 2, 1, "rotate(90deg)") 83 | start = random() * tau / 2 - tau / 4 84 | end = random() * tau / 2 - tau / 4 85 | 86 | p = ellipse.point_at_angle(start) 87 | a = ellipse.angle_at_point(p) 88 | self.assertAlmostEqual(start, a) 89 | 90 | p = ellipse.point_at_t(end) 91 | a = ellipse.t_at_point(p) 92 | self.assertAlmostEqual(end, a) 93 | 94 | p = ellipse.point_at_angle(end) 95 | a = ellipse.angle_at_point(p) 96 | self.assertAlmostEqual(end, a) 97 | 98 | arc = ellipse.arc_angle(start, end) 99 | self.assertAlmostEqual(arc.get_start_angle(), start) 100 | self.assertAlmostEqual(arc.get_end_angle(), end) 101 | 102 | def test_arc_t_rotated(self): 103 | for i in range(1000): 104 | ellipse = Ellipse((0, 0), 2, 1, "rotate(90deg)") 105 | start = random() * tau / 2 - tau / 4 106 | end = random() * tau / 2 - tau / 4 107 | 108 | p = ellipse.point_at_t(start) 109 | a = ellipse.t_at_point(p) 110 | self.assertAlmostEqual(start, a) 111 | 112 | arc = ellipse.arc_t(start, end) 113 | self.assertAlmostEqual(arc.get_start_t(), start) 114 | self.assertAlmostEqual(arc.get_end_t(), end) 115 | 116 | def test_arc_solve_produced(self): 117 | a = 3.05 118 | b = 2.23 119 | angle = atan(a * tan(radians(50)) / b) 120 | x = cos(angle) * a 121 | y = sin(angle) * b 122 | arc0 = Arc(start=3.05 + 0j, radius=3.05 + 2.23j, rotation=0, sweep_flag=1, arc_flag=0, end=x + 1j * y) 123 | 124 | ellipse = Ellipse(0, 0, 3.05, 2.23) 125 | arc1 = ellipse.arc_angle(0, Angle.degrees(50)) 126 | 127 | self.assertEqual(arc0, arc1) 128 | 129 | def test_arc_solved_exact(self): 130 | ellipse = Ellipse(0.0, 0.0, 3.05, 2.23) 131 | arc = ellipse.arc_angle(0, Angle.degrees(50)) 132 | arc *= "rotate(1)" 133 | exact = arc._exact_length() 134 | self.assertAlmostEqual(exact, 2.5314195265536624417, delta=1e-10) 135 | 136 | def test_arc_solved_integrated(self): 137 | ellipse = Ellipse(0, 0, 3.05, 2.23) 138 | arc = ellipse.arc_angle(0, Angle.degrees(50)) 139 | length_calculated = arc._integral_length() 140 | self.assertAlmostEqual(length_calculated, 2.5314195265536624417, delta=1e-4) 141 | 142 | def test_arc_solved_lines(self): 143 | ellipse = Ellipse(0, 0, 3.05, 2.23) 144 | arc = ellipse.arc_angle(0, Angle.degrees(50)) 145 | length_calculated = arc._line_length() 146 | self.assertAlmostEqual(length_calculated, 2.5314195265536624417, delta=1e-9) 147 | 148 | def test_arc_rotated_solved_exact(self): 149 | ellipse = Ellipse(0, 0, 3.05, 2.23) 150 | arc = ellipse.arc_angle(Angle.degrees(180), Angle.degrees(180 - 50)) 151 | exact = arc._exact_length() 152 | self.assertAlmostEqual(exact, 2.5314195265536624417) 153 | 154 | arc = ellipse.arc_angle(Angle.degrees(360 + 180 - 50), Angle.degrees(180)) 155 | exact = arc._exact_length() 156 | self.assertAlmostEqual(exact, 14.156360641292059) 157 | 158 | def test_arc_position_0_ortho(self): 159 | arc = Ellipse(0, 0, 3, 5).arc_angle(0, Angle.degrees(90)) 160 | self.assertEqual(arc.point(0), (3, 0)) 161 | 162 | def test_arc_position_0_rotate(self): 163 | arc = Ellipse(0, 0, 3, 5).arc_angle(0, Angle.degrees(90)) 164 | arc *= "rotate(90deg)" 165 | p = arc.point(0) 166 | self.assertEqual(p, (0, 3)) 167 | p = arc.point(1) 168 | self.assertEqual(p, (-5, 0)) 169 | 170 | def test_arc_position_0_angle(self): 171 | arc = Ellipse("0,0", 3, 5).arc_angle(0, Angle.degrees(90)) 172 | arc *= "rotate(-33deg)" 173 | self.assertEqual(arc.get_start_angle(), Angle.degrees(-33)) 174 | 175 | def test_arc_position_0(self): 176 | start = Point(13.152548373912, 38.873772319489) 177 | arc = Arc(start, 178 | Point(14.324014604836, 24.436855715076), 179 | Point(-14.750000067599, 25.169681093411), 180 | Point(-43.558410063178, 28.706909065029), 181 | Point(-19.42967575562, -12.943218880396), 182 | 5.89788464227) 183 | point_0 = arc.point(0) 184 | self.assertAlmostEqual(start, point_0) 185 | 186 | def test_arc_len_r0_default(self): 187 | """Test error vs. random arc""" 188 | arc = Arc(Point(13.152548373912, 38.873772319489), 189 | Point(14.324014604836, 24.436855715076), 190 | Point(-14.750000067599, 25.169681093411), 191 | Point(-43.558410063178, 28.706909065029), 192 | Point(-19.42967575562, -12.943218880396), 193 | 5.89788464227) 194 | length = arc.length() 195 | self.assertAlmostEqual(198.3041678406902, length, places=3) 196 | 197 | def test_arc_len_r0_lines(self): 198 | """Test error vs. random arc""" 199 | arc = Arc(Point(13.152548373912, 38.873772319489), 200 | Point(14.324014604836, 24.436855715076), 201 | Point(-14.750000067599, 25.169681093411), 202 | Point(-43.558410063178, 28.706909065029), 203 | Point(-19.42967575562, -12.943218880396), 204 | 5.89788464227) 205 | length = arc._line_length() 206 | self.assertAlmostEqual(198.3041678406902, length, places=3) 207 | 208 | def test_arc_len_r0_exact(self): 209 | """Test error vs. random arc""" 210 | arc = Arc(Point(13.152548373912, 38.873772319489), 211 | Point(14.324014604836, 24.436855715076), 212 | Point(-14.750000067599, 25.169681093411), 213 | Point(-43.558410063178, 28.706909065029), 214 | Point(-19.42967575562, -12.943218880396), 215 | 5.89788464227) 216 | length = arc._exact_length() 217 | self.assertAlmostEqual(198.3041678406902, length, places=3) 218 | 219 | def test_arc_len_r0_integral(self): 220 | """Test error vs. random arc""" 221 | arc = Arc(Point(13.152548373912, 38.873772319489), 222 | Point(14.324014604836, 24.436855715076), 223 | Point(-14.750000067599, 25.169681093411), 224 | Point(-43.558410063178, 28.706909065029), 225 | Point(-19.42967575562, -12.943218880396), 226 | 5.89788464227) 227 | length = arc._integral_length() 228 | self.assertAlmostEqual(198.3041678406902, length, places=3) 229 | 230 | def test_arc_len_straight(self): 231 | """Test error at extreme eccentricities""" 232 | self.assertAlmostEqual(Arc(0, 1, 1e-10, 0, 1, 0, (0, 2e-10))._line_length(), 2, places=15) 233 | self.assertAlmostEqual(Arc(0, 1, 1e-10, 0, 1, 0, (0, 2e-10))._integral_length(), 2, places=5) 234 | self.assertEqual(Arc(0, 1, 1e-10, 0, 1, 0, (0, 2e-10))._exact_length(), 2) 235 | 236 | def test_unit_matrix(self): 237 | ellipse = Ellipse("20", "20", 4, 8, "rotate(45deg)") 238 | matrix = ellipse.unit_matrix() 239 | ellipse2 = Circle() 240 | ellipse2.values[SVG_ATTR_VECTOR_EFFECT] = SVG_VALUE_NON_SCALING_STROKE 241 | ellipse2 *= matrix 242 | p1 = ellipse.point_at_t(1) 243 | p2 = ellipse2.point_at_t(1) 244 | self.assertAlmostEqual(p1, p2) 245 | self.assertEqual(ellipse, ellipse2) 246 | 247 | def test_arc_len_circle_shortcut(self): 248 | """Known chord vs. shortcut""" 249 | error = 0 250 | for i in range(1000): 251 | arc = get_random_circle_arc() 252 | chord = abs(arc.sweep * arc.rx) 253 | length = arc.length() 254 | c = abs(length - chord) 255 | error += c 256 | self.assertAlmostEqual(chord, length) 257 | print("Average chord vs shortcut-length: %g" % (error / 1000)) 258 | 259 | def test_arc_len_circle_int(self): 260 | """Known chord vs integral""" 261 | n = 10 262 | error = 0 263 | for i in range(n): 264 | arc = get_random_circle_arc() 265 | chord = abs(arc.sweep * arc.rx) 266 | length = arc._integral_length() 267 | c = abs(length - chord) 268 | error += c 269 | self.assertAlmostEqual(chord, length) 270 | print("Average chord vs integral: %g" % (error / n)) 271 | 272 | def test_arc_len_circle_exact(self): 273 | """Known chord vs exact""" 274 | n = 1000 275 | error = 0 276 | for i in range(n): 277 | arc = get_random_circle_arc() 278 | chord = abs(arc.sweep * arc.rx) 279 | length = arc._exact_length() 280 | c = abs(length - chord) 281 | error += c 282 | self.assertAlmostEqual(chord, length) 283 | print("Average chord vs exact: %g" % (error / n)) 284 | 285 | def test_arc_len_circle_line(self): 286 | """Known chord vs line""" 287 | n = 1 288 | error = 0 289 | for i in range(n): 290 | arc = get_random_circle_arc() 291 | chord = abs(arc.sweep * arc.rx) 292 | length = arc._line_length() 293 | c = abs(length - chord) 294 | error += c 295 | self.assertAlmostEqual(chord, length, places=6) 296 | print("Average chord vs line: %g" % (error / n)) 297 | 298 | def test_arc_len_flat_line(self): 299 | """Known flat vs line""" 300 | n = 100 301 | error = 0 302 | for i in range(n): 303 | flat = 1 + random() * 50 304 | arc = Arc(0, flat, 1e-10, 0, 1, 0, (0, 2e-10)) 305 | flat = 2*flat 306 | length = arc._line_length() 307 | c = abs(length - flat) 308 | error += c 309 | self.assertAlmostEqual(flat, length) 310 | print("Average flat vs line: %g" % (error / n)) 311 | 312 | def test_arc_len_flat_integral(self): 313 | """Known flat vs integral""" 314 | n = 10 315 | error = 0 316 | for i in range(n): 317 | flat = 1 + random() * 50 318 | arc = Arc(0, flat, 1e-10, 0, 1, 0, (0, 2e-10)) 319 | flat = 2*flat 320 | length = arc._integral_length() 321 | c = abs(length - flat) 322 | error += c 323 | self.assertAlmostEqual(flat, length) 324 | print("Average flat vs integral: %g" % (error / n)) 325 | 326 | def test_arc_len_flat_exact(self): 327 | """Known flat vs exact""" 328 | n = 1000 329 | error = 0 330 | for i in range(n): 331 | flat = 1 + random() * 50 332 | arc = Arc(0, flat, 1e-10, 0, 1, 0, (0, 2e-10)) 333 | flat = 2*flat 334 | length = arc._exact_length() 335 | c = abs(length - flat) 336 | error += c 337 | self.assertAlmostEqual(flat, length) 338 | print("Average flat vs line: %g" % (error / n)) 339 | 340 | def test_arc_len_random_int(self): 341 | """Test error vs. random arc""" 342 | n = 5 343 | error = 0 344 | for i in range(n): 345 | arc = get_random_arc() 346 | length = arc._integral_length() 347 | exact = arc._exact_length() 348 | c = abs(length - exact) 349 | error += c 350 | self.assertAlmostEqual(exact, length, places=1) 351 | print("Average arc-integral error: %g" % (error / n)) 352 | 353 | def test_arc_len_random_lines(self): 354 | """Test error vs. random arc""" 355 | n = 2 356 | error = 0 357 | for i in range(n): 358 | arc = get_random_arc() 359 | length = arc._line_length() 360 | exact = arc._exact_length() 361 | c = abs(length - exact) 362 | error += c 363 | self.assertAlmostEqual(exact, length, places=1) 364 | print("Average arc-line error: %g" % (error / n)) 365 | 366 | def test_arc_issue_126(self): 367 | """ 368 | Numerical Instability within arc bulge code. 369 | """ 370 | arc = Arc( 371 | start=(-35.61856796405604, -3.1190066784519077), 372 | end=(-37.881309663852996, -5.381748378248861), 373 | bulge=0.9999999999999999 374 | ) 375 | self.assertLessEqual(arc.sweep, tau/2) 376 | 377 | 378 | class TestElementArcPoint(unittest.TestCase): 379 | 380 | def test_arc_point_start_stop(self): 381 | import numpy as np 382 | for _ in range(1000): 383 | arc = get_random_arc() 384 | self.assertEqual(arc.start, arc.point(0)) 385 | self.assertEqual(arc.end, arc.point(1)) 386 | self.assertTrue(np.all(np.array([list(arc.start), list(arc.end)]) 387 | == arc.npoint([0, 1]))) 388 | 389 | def test_arc_point_implementations_match(self): 390 | import numpy as np 391 | for _ in range(1000): 392 | arc = get_random_arc() 393 | 394 | pos = np.linspace(0, 1, 100) 395 | 396 | v1 = arc.npoint(pos) 397 | v2 = [] 398 | for i in range(len(pos)): 399 | v2.append(arc.point(pos[i])) 400 | 401 | for p, p1, p2 in zip(pos, v1, v2): 402 | self.assertEqual(arc.point(p), Point(p1)) 403 | self.assertEqual(Point(p1), Point(p2)) 404 | 405 | 406 | class TestElementArcApproximation(unittest.TestCase): 407 | 408 | def test_approx_quad(self): 409 | n = 2 410 | for i in range(n): 411 | arc = get_random_arc() 412 | path1 = Path([Move(), arc]) 413 | path2 = Path(path1) 414 | path2.approximate_arcs_with_quads(error=0.05) 415 | d = abs(path1.length() - path2.length()) 416 | # Error less than 1% typically less than 0.5% 417 | if d > 10: 418 | print(arc) 419 | self.assertAlmostEqual(d, 0.0, delta=20) 420 | 421 | def test_approx_cubic(self): 422 | n = 2 423 | for i in range(n): 424 | arc = get_random_arc() 425 | path1 = Path([Move(), arc]) 426 | path2 = Path(path1) 427 | path2.approximate_arcs_with_cubics(error=0.1) 428 | d = abs(path1.length() - path2.length()) 429 | # Error less than 0.1% typically less than 0.001% 430 | if d > 1: 431 | print(arc) 432 | self.assertAlmostEqual(d, 0.0, delta=2) 433 | 434 | def test_approx_quad_degenerate(self): 435 | arc = Arc(start=(0,0),end=(0,0), control=(0,0)) 436 | path1 = Path([Move(), arc]) 437 | path2 = Path(path1) 438 | path2.approximate_arcs_with_quads(error=0.05) 439 | d = abs(path1.length() - path2.length()) 440 | # Error less than 1% typically less than 0.5% 441 | if d > 10: 442 | print(arc) 443 | self.assertAlmostEqual(d, 0.0, delta=20) 444 | 445 | def test_approx_cubic_degenerate(self): 446 | arc = Arc(start=(0,0),end=(0,0), control=(0,0)) 447 | path1 = Path([Move(), arc]) 448 | path2 = Path(path1) 449 | path2.approximate_arcs_with_cubics(error=0.1) 450 | d = abs(path1.length() - path2.length()) 451 | # Error less than 0.1% typically less than 0.001% 452 | if d > 1: 453 | print(arc) 454 | self.assertAlmostEqual(d, 0.0, delta=2) -------------------------------------------------------------------------------- /test/test_bbox.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import io 3 | 4 | from svgelements import * 5 | 6 | 7 | class TestElementBbox(unittest.TestCase): 8 | 9 | def test_bbox_rect(self): 10 | values = { 11 | 'tag': 'rect', 12 | 'rx': "4", 13 | 'ry': "2", 14 | 'x': "50", 15 | 'y': "51", 16 | 'width': "20", 17 | 'height': "10" 18 | } 19 | e = Rect(values) 20 | self.assertEqual(e.bbox(), (50,51,70,61)) 21 | e *= "translate(2)" 22 | self.assertEqual(e.bbox(), (52, 51, 72, 61)) 23 | 24 | def test_bbox_rect_stroke(self): 25 | values = { 26 | 'tag': 'rect', 27 | 'rx': "4", 28 | 'ry': "2", 29 | 'x': "50", 30 | 'y': "51", 31 | 'width': "20", 32 | 'height': "10", 33 | 'stroke-width': "5", 34 | 'stroke': 'red', 35 | } 36 | e = Rect(values) 37 | self.assertEqual(e.bbox(), (50, 51, 70, 61)) 38 | self.assertEqual(e.bbox(with_stroke=True), ( 39 | 50-(5./2.), 40 | 51-(5./2.), 41 | 70+(5./2.), 42 | 61+(5./2.) 43 | )) 44 | e *= "translate(2)" 45 | self.assertEqual(e.bbox(), (52, 51, 72, 61)) 46 | self.assertEqual(e.bbox(with_stroke=True), ( 47 | 52 - (5. / 2.), 48 | 51 - (5. / 2.), 49 | 72 + (5. / 2.), 50 | 61 + (5. / 2.) 51 | )) 52 | e *= "scale(2)" 53 | self.assertEqual(e.bbox(), (52 * 2, 51 * 2, 72 * 2, 61 * 2)) 54 | self.assertEqual(e.bbox(with_stroke=True), ( 55 | 52 * 2 - 5., 56 | 51 * 2 - 5., 57 | 72 * 2 + 5., 58 | 61 * 2 + 5. 59 | )) 60 | self.assertEqual(e.bbox(transformed=False), (50, 51, 70, 61)) 61 | self.assertEqual(e.bbox(transformed=False, with_stroke=True), ( 62 | 50 - (5. / 2.), 63 | 51 - (5. / 2.), 64 | 70 + (5. / 2.), 65 | 61 + (5. / 2.) 66 | )) 67 | 68 | def test_bbox_path(self): 69 | values = { 70 | 'tag': 'rect', 71 | 'rx': "4", 72 | 'ry': "2", 73 | 'x': "50", 74 | 'y': "51", 75 | 'width': "20", 76 | 'height': "10" 77 | } 78 | e = Path(Rect(values)) 79 | self.assertEqual(e.bbox(), (50,51,70,61)) 80 | e *= "translate(2)" 81 | self.assertEqual(e.bbox(), (52, 51, 72, 61)) 82 | 83 | def test_bbox_path_stroke(self): 84 | values = { 85 | 'tag': 'rect', 86 | 'rx': "4", 87 | 'ry': "2", 88 | 'x': "50", 89 | 'y': "51", 90 | 'width': "20", 91 | 'height': "10", 92 | 'stroke-width': "5", 93 | 'stroke': 'red', 94 | } 95 | e = Path(Rect(values)) 96 | self.assertEqual(e.bbox(), (50, 51, 70, 61)) 97 | self.assertEqual(e.bbox(with_stroke=True), ( 98 | 50-(5./2.), 99 | 51-(5./2.), 100 | 70+(5./2.), 101 | 61+(5./2.) 102 | )) 103 | e *= "translate(2)" 104 | self.assertEqual(e.bbox(), (52, 51, 72, 61)) 105 | self.assertEqual(e.bbox(with_stroke=True), ( 106 | 52 - (5. / 2.), 107 | 51 - (5. / 2.), 108 | 72 + (5. / 2.), 109 | 61 + (5. / 2.) 110 | )) 111 | e *= "scale(2)" 112 | self.assertEqual(e.bbox(), (52 * 2, 51 * 2, 72 * 2, 61 * 2)) 113 | self.assertEqual(e.bbox(with_stroke=True), ( 114 | 52 * 2 - 5., 115 | 51 * 2 - 5., 116 | 72 * 2 + 5., 117 | 61 * 2 + 5. 118 | )) 119 | self.assertEqual(e.bbox(transformed=False), (50, 51, 70, 61)) 120 | self.assertEqual(e.bbox(transformed=False, with_stroke=True), ( 121 | 50 - (5. / 2.), 122 | 51 - (5. / 2.), 123 | 70 + (5. / 2.), 124 | 61 + (5. / 2.) 125 | )) 126 | 127 | def test_bbox_path_stroke_none(self): 128 | """ 129 | Same as test_bbox_path_stroke but stroke is set to none, so the bbox should not change. 130 | """ 131 | values = { 132 | 'tag': 'rect', 133 | 'rx': "4", 134 | 'ry': "2", 135 | 'x': "50", 136 | 'y': "51", 137 | 'width': "20", 138 | 'height': "10", 139 | 'stroke-width': "5", 140 | 'stroke': "none", 141 | } 142 | e = Path(Rect(values)) 143 | self.assertEqual(e.bbox(), (50, 51, 70, 61)) 144 | self.assertEqual(e.bbox(with_stroke=True), ( 145 | 50, 146 | 51, 147 | 70, 148 | 61 149 | )) 150 | e *= "translate(2)" 151 | self.assertEqual(e.bbox(), (52, 51, 72, 61)) 152 | self.assertEqual(e.bbox(with_stroke=True), ( 153 | 52, 154 | 51, 155 | 72, 156 | 61 157 | )) 158 | e *= "scale(2)" 159 | self.assertEqual(e.bbox(), (52 * 2, 51 * 2, 72 * 2, 61 * 2)) 160 | self.assertEqual(e.bbox(with_stroke=True), ( 161 | 52 * 2, 162 | 51 * 2, 163 | 72 * 2, 164 | 61 * 2 165 | )) 166 | self.assertEqual(e.bbox(transformed=False), (50, 51, 70, 61)) 167 | self.assertEqual(e.bbox(transformed=False, with_stroke=True), ( 168 | 50, 169 | 51, 170 | 70, 171 | 61 172 | )) 173 | 174 | def test_bbox_path_stroke_unset(self): 175 | """ 176 | Same as test_bbox_path_stroke but the stroke is unset and thus shouldn't contribute to the bbox even if 177 | with_stroke is set. 178 | """ 179 | values = { 180 | 'tag': 'rect', 181 | 'rx': "4", 182 | 'ry': "2", 183 | 'x': "50", 184 | 'y': "51", 185 | 'width': "20", 186 | 'height': "10", 187 | 'stroke-width': "5", 188 | } 189 | e = Path(Rect(values)) 190 | self.assertEqual(e.bbox(), (50, 51, 70, 61)) 191 | self.assertEqual(e.bbox(with_stroke=True), ( 192 | 50, 193 | 51, 194 | 70, 195 | 61 196 | )) 197 | e *= "translate(2)" 198 | self.assertEqual(e.bbox(), (52, 51, 72, 61)) 199 | self.assertEqual(e.bbox(with_stroke=True), ( 200 | 52, 201 | 51, 202 | 72, 203 | 61 204 | )) 205 | e *= "scale(2)" 206 | self.assertEqual(e.bbox(), (52 * 2, 51 * 2, 72 * 2, 61 * 2)) 207 | self.assertEqual(e.bbox(with_stroke=True), ( 208 | 52 * 2, 209 | 51 * 2, 210 | 72 * 2, 211 | 61 * 2 212 | )) 213 | self.assertEqual(e.bbox(transformed=False), (50, 51, 70, 61)) 214 | self.assertEqual(e.bbox(transformed=False, with_stroke=True), ( 215 | 50, 216 | 51, 217 | 70, 218 | 61 219 | )) 220 | 221 | def test_bbox_subpath(self): 222 | p = Path("M 10,100 H 20 V 80 H 10 Z m 10,-90 H 60 V 70 H 20 Z") 223 | e = p.subpath(1) 224 | self.assertEqual(e.bbox(), (20, 10, 60, 70)) 225 | e *= "translate(5)" 226 | self.assertEqual(e.bbox(), (25, 10, 65, 70)) 227 | 228 | def test_bbox_move_subpath2(self): 229 | p = Path("M 0,0 Z m 100,100 h 20 v 20 h -20 Z") 230 | e = p.subpath(1) 231 | self.assertEqual(e.bbox(), (100, 100, 120, 120)) 232 | 233 | def test_bbox_subpath_stroke(self): 234 | values = { 235 | 'tag': 'rect', 236 | 'rx': "4", 237 | 'ry': "2", 238 | 'x': "50", 239 | 'y': "51", 240 | 'width': "20", 241 | 'height': "10", 242 | 'stroke-width': "5", 243 | 'stroke': 'red', 244 | } 245 | p = Path(Rect(values)) 246 | e = p.subpath(0) 247 | self.assertEqual(e.bbox(), (50, 51, 70, 61)) 248 | self.assertEqual(e.bbox(with_stroke=True), ( 249 | 50-(5./2.), 250 | 51-(5./2.), 251 | 70+(5./2.), 252 | 61+(5./2.) 253 | )) 254 | p *= "translate(2)" 255 | self.assertEqual(e.bbox(), (52, 51, 72, 61)) 256 | self.assertEqual(e.bbox(with_stroke=True), ( 257 | 52 - (5. / 2.), 258 | 51 - (5. / 2.), 259 | 72 + (5. / 2.), 260 | 61 + (5. / 2.) 261 | )) 262 | p *= "scale(2)" 263 | self.assertEqual(e.bbox(), (52 * 2, 51 * 2, 72 * 2, 61 * 2)) 264 | self.assertEqual(e.bbox(with_stroke=True), ( 265 | 52 * 2 - 5., 266 | 51 * 2 - 5., 267 | 72 * 2 + 5., 268 | 61 * 2 + 5. 269 | )) 270 | self.assertEqual(e.bbox(transformed=False), (50, 51, 70, 61)) 271 | self.assertEqual(e.bbox(transformed=False, with_stroke=True), ( 272 | 50 - (5. / 2.), 273 | 51 - (5. / 2.), 274 | 70 + (5. / 2.), 275 | 61 + (5. / 2.) 276 | )) 277 | 278 | def test_bbox_rotated_circle(self): 279 | # Rotation of circle must not affect it's bounding box 280 | c = Circle(cx=0, cy=0, r=1, transform="rotate(45)") 281 | (xmin, ymin, xmax, ymax) = c.bbox() 282 | self.assertAlmostEqual(-1, xmin) 283 | self.assertAlmostEqual(-1, ymin) 284 | self.assertAlmostEqual( 1, xmax) 285 | self.assertAlmostEqual( 1, ymax) 286 | 287 | def test_bbox_svg_with_rotated_circle(self): 288 | # Rotation of circle within group must not affect it's bounding box 289 | q = io.StringIO( 290 | u''' 291 | 292 | 293 | 294 | ''' 295 | ) 296 | svg = SVG.parse(q) 297 | (xmin, ymin, xmax, ymax) = svg.bbox() 298 | self.assertAlmostEqual(-1, xmin) 299 | self.assertAlmostEqual(-1, ymin) 300 | self.assertAlmostEqual( 1, xmax) 301 | self.assertAlmostEqual( 1, ymax) 302 | 303 | def test_bbox_translated_circle(self): 304 | c = Circle(cx=0, cy=0, r=1, transform="translate(-1,-1)") 305 | (xmin, ymin, xmax, ymax) = c.bbox() 306 | self.assertAlmostEqual(-2, xmin) 307 | self.assertAlmostEqual(-2, ymin) 308 | self.assertAlmostEqual( 0, xmax) 309 | self.assertAlmostEqual( 0, ymax) 310 | 311 | def test_bbox_svg_with_translated_group_with_circle(self): 312 | # Translation of nested group must be applied correctly 313 | q = io.StringIO( 314 | u''' 315 | 316 | 317 | 318 | 319 | 320 | ''' 321 | ) 322 | svg = SVG.parse(q) 323 | (xmin, ymin, xmax, ymax) = svg.bbox() 324 | self.assertAlmostEqual(-2, xmin) 325 | self.assertAlmostEqual(-2, ymin) 326 | self.assertAlmostEqual( 0, xmax) 327 | self.assertAlmostEqual( 0, ymax) 328 | 329 | 330 | def test_issue_104(self): 331 | """Testing Issue 104 rotated bbox""" 332 | rect = Rect(10,10,10,10) 333 | rect *= "rotate(45deg)" 334 | self.assertEqual(rect.bbox(), abs(rect).bbox()) 335 | 336 | circ = Circle(5,5,10) 337 | circ *= "rotate(45deg)" 338 | self.assertEqual(circ.bbox(), abs(circ).bbox()) 339 | 340 | path = Path("M0 0 100,100") 341 | path *= "rotate(45deg)" 342 | self.assertEqual(path.bbox(), abs(path).bbox()) 343 | 344 | path = Path("M0 0q100,100 200,200z") 345 | path *= "rotate(45deg)" 346 | self.assertEqual(path.bbox(), abs(path).bbox()) 347 | 348 | path = Path("M0 20c0,128 94,94 200,200z") 349 | path *= "rotate(45deg)" 350 | self.assertEqual(path.bbox(), abs(path).bbox()) 351 | 352 | q = io.StringIO(u''' 353 | 354 | 355 | ''') 356 | m = SVG.parse(q, reify=False) 357 | p0 = m[0] 358 | p1 = abs(p0) 359 | self.assertEqual(p0, p1) 360 | self.assertEqual(p0.bbox(), p1.bbox()) 361 | -------------------------------------------------------------------------------- /test/test_clippath.py: -------------------------------------------------------------------------------- 1 | import io 2 | import unittest 3 | 4 | from svgelements import * 5 | 6 | 7 | class TestElementClipPath(unittest.TestCase): 8 | """These tests test the existence and functionality of clipPaths""" 9 | 10 | def test_parse_clippath(self): 11 | q = io.StringIO(u''' 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ''') 21 | svg = SVG.parse(q) 22 | m = list(svg.elements()) 23 | a = m[1] 24 | self.assertEqual(type(a), Circle) 25 | self.assertNotEqual(a.clip_path, None) 26 | self.assertEqual(type(a.clip_path), ClipPath) 27 | self.assertEqual(a.clip_path[0].clip_rule, SVG_RULE_NONZERO) 28 | self.assertRaises(AttributeError, lambda: a.clip_rule) 29 | self.assertEqual(type(a.clip_path[0]), Rect) 30 | 31 | def test_nested_clippath(self): 32 | q = io.StringIO( 33 | u''' 34 | ' 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ''') 52 | svg = SVG.parse(q) 53 | m = list(svg.elements()) 54 | a = m[4] 55 | self.assertEqual(type(a), Path) 56 | 57 | clip_path = m[2].clip_path 58 | self.assertNotEqual(clip_path, None) 59 | self.assertEqual(type(clip_path), ClipPath) 60 | self.assertEqual(clip_path[0].clip_rule, SVG_RULE_NONZERO) 61 | self.assertEqual(type(clip_path[0]), Circle) 62 | 63 | clip_path = m[3].clip_path 64 | self.assertNotEqual(clip_path, None) 65 | self.assertEqual(type(clip_path), ClipPath) 66 | self.assertEqual(clip_path[0].clip_rule, SVG_RULE_EVENODD) 67 | self.assertEqual(type(clip_path[0]), Rect) 68 | -------------------------------------------------------------------------------- /test/test_color.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | import io 4 | 5 | from svgelements import * 6 | 7 | 8 | class TestElementColor(unittest.TestCase): 9 | """These tests test the basic functions of the Color element.""" 10 | 11 | def test_color_red(self): 12 | reference = Color('red') 13 | self.assertEqual(reference, 'red') 14 | self.assertEqual(reference, Color('#F00')) 15 | self.assertEqual(reference, Color('#FF0000')) 16 | self.assertEqual(reference, Color("rgb(255, 0, 0)")) 17 | self.assertEqual(reference, Color("rgb(100%, 0%, 0%)")) 18 | self.assertEqual(reference, Color("rgb(300, 0, 0)")) 19 | self.assertEqual(reference, Color("rgb(255, -10, 0)")) 20 | self.assertEqual(reference, Color("rgb(110%, 0%, 0%)")) 21 | self.assertEqual(reference, Color("rgba(255, 0, 0, 1)")) 22 | self.assertEqual(reference, Color("rgba(100%, 0%, 0%, 1)")) 23 | self.assertEqual(reference, Color("hsl(0, 100%, 50%)")) 24 | self.assertEqual(reference, Color("hsla(0, 100%, 50%, 1.0)")) 25 | self.assertEqual(reference, Color(0xFF0000)) 26 | color = Color() 27 | color.rgb = 0xFF0000 28 | self.assertEqual(reference, color) 29 | self.assertEqual(reference, Color(rgb=0xFF0000)) 30 | self.assertEqual(reference, Color(bgr=0x0000FF)) 31 | self.assertEqual(reference, Color(argb=0xFFFF0000)) 32 | self.assertEqual(reference, Color(rgba=0xFF0000FF)) 33 | self.assertEqual(reference, Color(0xFF0000, 1.0)) 34 | 35 | def test_color_green(self): 36 | reference = Color('lime') # Lime is 255 green, green is 128 green. 37 | self.assertEqual(reference, 'lime') 38 | self.assertEqual(reference, Color('#0F0')) 39 | self.assertEqual(reference, Color('#00FF00')) 40 | self.assertEqual(reference, Color("rgb(0, 255, 0)")) 41 | self.assertEqual(reference, Color("rgb(0%, 100%, 0%)")) 42 | self.assertEqual(reference, Color("rgb(0, 300, 0)")) 43 | self.assertEqual(reference, Color("rgb(-10, 255, 0)")) 44 | self.assertEqual(reference, Color("rgb(0%, 110%, 0%)")) 45 | self.assertEqual(reference, Color("rgba(0, 255, 0, 1)")) 46 | self.assertEqual(reference, Color("rgba(0%, 100%, 0%, 1)")) 47 | self.assertEqual(reference, Color("hsl(120, 100%, 50%)")) 48 | self.assertEqual(reference, Color("hsla(120, 100%, 50%, 1.0)")) 49 | self.assertEqual(reference, Color(0x00FF00)) 50 | color = Color() 51 | color.rgb = 0x00FF00 52 | self.assertEqual(reference, color) 53 | self.assertEqual(reference, Color(rgb=0x00FF00)) 54 | self.assertEqual(reference, Color(bgr=0x00FF00)) 55 | self.assertEqual(reference, Color(argb=0xFF00FF00)) 56 | self.assertEqual(reference, Color(rgba=0x00FF00FF)) 57 | self.assertEqual(reference, Color(0x00FF00, 1.0)) 58 | 59 | def test_color_blue(self): 60 | reference = Color('blue') 61 | self.assertEqual(reference, 'blue') 62 | self.assertEqual(reference, Color('#00F')) 63 | self.assertEqual(reference, Color('#0000FF')) 64 | self.assertEqual(reference, Color("rgb(0, 0, 255)")) 65 | self.assertEqual(reference, Color("rgb(0%, 0%, 100%)")) 66 | self.assertEqual(reference, Color("rgb(0, 0, 300)")) 67 | self.assertEqual(reference, Color("rgb(0, -10, 255)")) 68 | self.assertEqual(reference, Color("rgb(0%, 0%, 110%)")) 69 | self.assertEqual(reference, Color("rgba(0, 0, 255, 1)")) 70 | self.assertEqual(reference, Color("rgb(0%, 0%, 100%)")) 71 | self.assertEqual(reference, Color("rgba(0%, 0%, 100%, 1)")) 72 | self.assertEqual(reference, Color("hsl(240, 100%, 50%)")) 73 | self.assertEqual(reference, Color("hsla(240, 100%, 50%, 1.0)")) 74 | self.assertEqual(reference, Color(0x0000FF)) 75 | color = Color() 76 | color.rgb = 0x0000FF 77 | self.assertEqual(reference, color) 78 | self.assertEqual(reference, Color(rgb=0x0000FF)) 79 | self.assertEqual(reference, Color(bgr=0xFF0000)) 80 | self.assertEqual(reference, Color(argb=0xFF0000FF)) 81 | self.assertEqual(reference, Color(rgba=0x0000FFFF)) 82 | self.assertEqual(reference, Color(0x0000FF, 1.0)) 83 | 84 | def test_color_bgr(self): 85 | reference = Color("#26A") 86 | self.assertEqual(reference, Color(bgr=0xAA6622)) 87 | reference.bgr = 0x2468AC 88 | self.assertEqual(reference, Color(rgb=0xAC6824)) 89 | self.assertEqual(reference.alpha, 0xFF) 90 | 91 | def test_color_red_half(self): 92 | half_ref = Color("rgba(100%, 0%, 0%, 0.5)") 93 | self.assertNotEqual(half_ref, 'red') 94 | 95 | color = Color('red', opacity=0.5) 96 | self.assertEqual(color, half_ref) 97 | 98 | def test_color_transparent(self): 99 | t0 = Color('transparent') 100 | t1 = Color('rgba(0,0,0,0)') 101 | self.assertEqual(t0, t1) 102 | self.assertNotEqual(t0, "black") 103 | 104 | def test_color_hsl(self): 105 | c0 = Color("hsl(0, 100%, 50%)") # red 106 | self.assertAlmostEqual(c0.hue, 0) 107 | self.assertAlmostEqual(c0.saturation, 1, places=2) 108 | self.assertAlmostEqual(c0.lightness, 0.5, places=2) 109 | self.assertEqual(c0, "red") 110 | c1 = Color("hsl(120, 100%, 50%)") # lime 111 | self.assertAlmostEqual(c1.hue, 120) 112 | self.assertAlmostEqual(c1.saturation, 1, places=2) 113 | self.assertAlmostEqual(c1.lightness, 0.5, places=2) 114 | self.assertEqual(c1, "lime") 115 | c2 = Color("hsl(120, 100%, 19.62%)") # dark green 116 | self.assertAlmostEqual(c2.hue, 120) 117 | self.assertAlmostEqual(c2.saturation, 1, places=2) 118 | self.assertAlmostEqual(c2.lightness, 0.1962, places=2) 119 | self.assertEqual(c2, "dark green") 120 | c3 = Color("hsl(120, 73.4%, 75%)") # light green 121 | self.assertAlmostEqual(c3.hue, 120) 122 | self.assertAlmostEqual(c3.saturation, 0.734, places=2) 123 | self.assertAlmostEqual(c3.lightness, 0.75, places=2) 124 | self.assertEqual(c3, "light green") 125 | c4 = Color("hsl(120, 60%, 66.67%)") # pastel green 126 | self.assertAlmostEqual(c4.hue, 120) 127 | self.assertAlmostEqual(c4.saturation, 0.6, places=2) 128 | self.assertAlmostEqual(c4.lightness, 0.6667, places=2) 129 | self.assertEqual(c4, "#77dd77") 130 | 131 | def test_color_hsla(self): 132 | c0 = Color.parse("hsl(120, 100%, 50%)") 133 | c1 = Color.parse("hsla(120, 100%, 50%, 1)") 134 | self.assertEqual(c0, c1) 135 | self.assertNotEqual(c0, "black") 136 | t1 = Color.parse("hsla(240, 100%, 50%, 0.5)") # semi - transparent solid blue 137 | t2 = Color.parse("hsla(30, 100%, 50%, 0.1)") # very transparent solid orange 138 | self.assertNotEqual(t1,t2) 139 | 140 | def test_parse_fill_opacity(self): 141 | q = io.StringIO(u'''\n 142 | 143 | 144 | ''' 145 | ) 146 | m = list(SVG.parse(q).elements()) 147 | r = m[1] 148 | self.assertAlmostEqual(r.fill.opacity, 0.5, delta=1.0/255.0) 149 | 150 | def test_parse_stroke_opacity(self): 151 | q = io.StringIO(u''' 152 | 153 | 154 | 155 | ''') 156 | m = list(SVG.parse(q).elements()) 157 | r = m[1] 158 | self.assertAlmostEqual(r.stroke.opacity, 0.2, delta=1.0/255.0) 159 | 160 | def test_color_none(self): 161 | color = Color(None) 162 | self.assertEqual(color, SVG_VALUE_NONE) 163 | self.assertEqual(color.red, None) 164 | self.assertEqual(color.green, None) 165 | self.assertEqual(color.blue, None) 166 | self.assertEqual(color.alpha, None) 167 | self.assertEqual(color.opacity, None) 168 | self.assertEqual(color.hexa, None) 169 | self.assertEqual(color.hex, None) 170 | self.assertEqual(color.blackness, None) 171 | self.assertEqual(color.brightness, None) 172 | self.assertEqual(color.hsl, None) 173 | self.assertEqual(color.hue, None) 174 | self.assertEqual(color.saturation, None) 175 | self.assertEqual(color.lightness, None) 176 | self.assertEqual(color.luma, None) 177 | self.assertEqual(color.luminance, None) 178 | self.assertEqual(color.intensity, None) 179 | 180 | def set_red(): 181 | color.red = 0 182 | self.assertRaises(ValueError, set_red) 183 | 184 | def set_green(): 185 | color.green = 0 186 | self.assertRaises(ValueError, set_green) 187 | 188 | def set_blue(): 189 | color.blue = 0 190 | self.assertRaises(ValueError, set_blue) 191 | 192 | def set_alpha(): 193 | color.alpha = 0 194 | self.assertRaises(ValueError, set_alpha) 195 | 196 | def set_opacity(): 197 | color.opacity = 1 198 | self.assertRaises(ValueError, set_opacity) 199 | 200 | def test_color_hexa(self): 201 | for r in range(0,255,17): 202 | for g in range(0, 255, 17): 203 | for b in range(0, 255, 17): 204 | for a in range(0, 255, 17): 205 | c = Color() 206 | c.red = r 207 | c.green = g 208 | c.blue = b 209 | c.alpha = a 210 | hexa = c.hexa 211 | c2 = Color(hexa) 212 | self.assertEqual(c,c2) 213 | 214 | def test_color_hex(self): 215 | for r in range(0,255,17): 216 | for g in range(0, 255, 17): 217 | for b in range(0, 255, 17): 218 | c = Color() 219 | c.red = r 220 | c.green = g 221 | c.blue = b 222 | c.alpha = 255 223 | hex = c.hex 224 | c2 = Color(hex) 225 | self.assertEqual(c,c2) 226 | 227 | def test_color_components(self): 228 | color = Color("#E9967A80") 229 | color2 = Color("darksalmon", .5) 230 | self.assertEqual(color, color2) 231 | self.assertEqual(color.red, 0xE9) 232 | self.assertEqual(color.green, 0x96) 233 | self.assertEqual(color.blue, 0x7A) 234 | self.assertEqual(color.alpha, 0x80) 235 | self.assertAlmostEqual(color.opacity, 0.50196078) 236 | self.assertEqual(color.hex, "#e9967a80") 237 | self.assertAlmostEqual(color.blackness, 0.0862745098) 238 | 239 | color.red = 0 240 | self.assertEqual(color.red, 0x0) 241 | color.green = 0 242 | self.assertEqual(color.green, 0x0) 243 | color.blue = 0 244 | self.assertEqual(color.blue, 0x0) 245 | color.alpha = 0 246 | self.assertEqual(color.alpha, 0x0) 247 | color.opacity = 1 248 | self.assertEqual(color.alpha, 0xFF) 249 | 250 | color.lightness = .5 251 | self.assertEqual(color.red, 0x7F) 252 | self.assertEqual(color.green, 0x7F) 253 | self.assertEqual(color.blue, 0x7F) 254 | self.assertEqual(color.alpha, 0xFF) 255 | 256 | def test_color_distinct(self): 257 | c0 = Color.distinct(0) 258 | self.assertEqual(c0, "white") 259 | c1 = Color.distinct(1) 260 | self.assertEqual(c1, "black") 261 | c2 = Color.distinct(2) 262 | self.assertEqual(c2, "red") 263 | c3 = Color.distinct(3) 264 | self.assertEqual(c3, "lime") 265 | c4 = Color.distinct(4) 266 | self.assertEqual(c4, "blue") 267 | c5 = Color.distinct(5) 268 | self.assertEqual(c5, "yellow") 269 | c6 = Color.distinct(6) 270 | self.assertEqual(c6, "cyan") 271 | c7 = Color.distinct(7) 272 | self.assertEqual(c7, "magenta") 273 | c8 = Color.distinct(6767890) 274 | self.assertEqual(c8, "#d85c3d") 275 | -------------------------------------------------------------------------------- /test/test_copy.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import io 3 | 4 | from svgelements import * 5 | 6 | 7 | class TestElementCopy(unittest.TestCase): 8 | """These tests test the validity of object copy.""" 9 | 10 | def test_copy_objects(self): 11 | # CSS OBJECTS 12 | 13 | length = Length('1in') 14 | length_copy = copy(length) 15 | self.assertEqual(length, length_copy) 16 | 17 | color = Color('red') 18 | color_copy = copy(color) 19 | self.assertEqual(color, color_copy) 20 | 21 | point = Point("2,4.58") 22 | point_copy = copy(point) 23 | self.assertEqual(point, point_copy) 24 | 25 | angle = Angle.parse('1.2grad') 26 | angle_copy = copy(angle) 27 | self.assertEqual(angle, angle_copy) 28 | 29 | matrix = Matrix("scale(4.5) translate(2,2.4) rotate(40grad)") 30 | matrix_copy = copy(matrix) 31 | self.assertEqual(matrix, matrix_copy) 32 | 33 | # SVG OBJECTS 34 | viewbox = Viewbox('0 0 103 109', preserveAspectRatio="xMaxyMin slice") 35 | viewbox_copy = copy(viewbox) 36 | # self.assertEqual(viewbox, viewbox_copy) 37 | 38 | svgelement = SVGElement({'tag': "element", 'id': 'testelement1234'}) 39 | svgelement_copy = copy(svgelement) 40 | self.assertIsNotNone(svgelement_copy.values) 41 | # self.assertEqual(svgelement, svgelement_copy) 42 | 43 | # PATH SEGMENTS 44 | move = Move((8,8.78)) 45 | move_copy = copy(move) 46 | self.assertEqual(move, move_copy) 47 | 48 | close = Close() 49 | close_copy = copy(close) 50 | self.assertEqual(close, close_copy) 51 | 52 | line = Line((8, 8.78)) 53 | line_copy = copy(line) 54 | self.assertEqual(line, line_copy) 55 | 56 | quad = QuadraticBezier((8, 8.78), (50, 50.78), (50, 5)) 57 | quad_copy = copy(quad) 58 | self.assertEqual(quad, quad_copy) 59 | 60 | cubic = CubicBezier((8, 8.78), (1, 6.78), (8, 9.78), (50, 5)) 61 | cubic_copy = copy(cubic) 62 | self.assertEqual(cubic, cubic_copy) 63 | 64 | arc = Arc(start=(0,0), end=(25,0), control=(10,10)) 65 | arc_copy = copy(arc) 66 | self.assertEqual(arc, arc_copy) 67 | 68 | # SHAPES 69 | path = Path("M5,5V10Z") 70 | path_copy = copy(path) 71 | self.assertEqual(path, path_copy) 72 | self.assertIsNotNone(path_copy.values) 73 | 74 | rect = Rect(0, 0, 1000, 1000, ry=20) 75 | rect_copy = copy(rect) 76 | self.assertEqual(rect, rect_copy) 77 | self.assertIsNotNone(rect_copy.values) 78 | 79 | ellipse = Ellipse(0, 0, 1000, 1000) 80 | ellipse_copy = copy(ellipse) 81 | self.assertEqual(ellipse, ellipse_copy) 82 | self.assertIsNotNone(ellipse_copy.values) 83 | 84 | circle = Circle(x=0, y=0, r=1000) 85 | circle_copy = copy(circle) 86 | self.assertEqual(circle, circle_copy) 87 | self.assertIsNotNone(circle_copy.values) 88 | 89 | sline = SimpleLine((0, 0), (1000, 1000)) 90 | sline_copy = copy(sline) 91 | self.assertEqual(sline, sline_copy) 92 | self.assertIsNotNone(sline_copy.values) 93 | 94 | rect = Rect(0, 0, 1000, 1000, ry=20) 95 | rect_copy = copy(rect) 96 | self.assertEqual(rect, rect_copy) 97 | self.assertIsNotNone(rect_copy.values) 98 | 99 | pline = Polyline((0, 0), (1000, 1000), (0,1000), (0,0)) 100 | pline_copy = copy(pline) 101 | self.assertEqual(pline, pline_copy) 102 | self.assertIsNotNone(pline_copy.values) 103 | 104 | pgon = Polygon((0, 0), (1000, 1000), (0,1000)) 105 | pgon_copy = copy(pgon) 106 | self.assertEqual(pgon, pgon_copy) 107 | self.assertIsNotNone(pgon_copy.values) 108 | 109 | group = Group(stroke="cornflower") 110 | group_copy = copy(group) 111 | self.assertEqual(group, group_copy) 112 | self.assertIsNotNone(group_copy.values) 113 | 114 | cpath = ClipPath(stroke="blue") 115 | cpath_copy = copy(cpath) 116 | self.assertEqual(cpath, cpath_copy) 117 | self.assertIsNotNone(cpath_copy.values) 118 | 119 | text = SVGText("HelloWorld") 120 | text_copy = copy(text) 121 | # self.assertEqual(text,text_copy) 122 | self.assertIsNotNone(text_copy.values) 123 | 124 | image = SVGImage(viewbox=viewbox) 125 | image_copy = copy(image) 126 | # self.assertEqual(image,image_copy) 127 | self.assertIsNotNone(image_copy.values) 128 | 129 | desc = Desc({"apple": 7}, desc="Some description") 130 | desc_copy = copy(desc) 131 | # self.assertEqual(desc, desc_copy) 132 | self.assertIsNotNone(desc_copy.values) 133 | 134 | title = Title({"apple": 3}, title="Some Title") 135 | title_copy = copy(title) 136 | # self.assertEqual(title, title_copy) 137 | self.assertIsNotNone(title_copy.values) 138 | 139 | q = io.StringIO(u''' 140 | 141 | 142 | ''') 143 | svg = SVG.parse(q) 144 | svg_copy = copy(svg) 145 | self.assertEqual(svg, svg_copy) 146 | self.assertIsNotNone(svg_copy.values) 147 | -------------------------------------------------------------------------------- /test/test_css.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import io 3 | 4 | from svgelements import * 5 | 6 | 7 | class TestSVGCSS(unittest.TestCase): 8 | 9 | def test_issue_103(self): 10 | """Testing Issue 103 css class parsing 11 | This test is based on an Illustrator file, where the styling relies more on CSS. 12 | """ 13 | 14 | q = io.StringIO(u''' 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ''') 26 | m = SVG.parse(q) 27 | poly = m[0][0][0] 28 | circ1 = m[0][0][1] 29 | circ2 = m[0][0][2] 30 | 31 | self.assertEqual(poly.fill, "black") 32 | self.assertEqual(poly.stroke, "none") 33 | 34 | self.assertEqual(circ1.fill, "none") 35 | self.assertEqual(circ1.stroke, "blue") 36 | 37 | self.assertEqual(circ2.fill, "none") 38 | self.assertEqual(circ2.stroke, "red") 39 | 40 | def test_issue_178(self): 41 | """Testing Issue 178 css comment parsing 42 | """ 43 | 44 | q = io.StringIO(u''' 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | ''') 61 | m = SVG.parse(q) 62 | poly = m[0][0][0] 63 | circ1 = m[0][0][1] 64 | circ2 = m[0][0][2] 65 | 66 | self.assertEqual(poly.fill, "black") 67 | self.assertEqual(poly.stroke, "none") 68 | 69 | self.assertEqual(circ1.fill, "none") 70 | self.assertEqual(circ1.stroke, "blue") 71 | 72 | self.assertEqual(circ2.fill, "none") 73 | self.assertEqual(circ2.stroke, "red") 74 | 75 | 76 | def test_issue_174b(self): 77 | """ 78 | aliflux-omo noted a crash parsing https://upload.wikimedia.org/wikipedia/commons/5/58/Axis_Occupation_of_Europe_%281942%29.svg 79 | Which contained a pointless blank style comment. 80 | """ 81 | 82 | q = io.StringIO(u''' 83 | 84 | ''') 85 | SVG.parse(q) 86 | -------------------------------------------------------------------------------- /test/test_cubic_bezier.py: -------------------------------------------------------------------------------- 1 | import random 2 | import unittest 3 | from random import * 4 | 5 | from svgelements import * 6 | 7 | 8 | def get_random_cubic_bezier(): 9 | return CubicBezier( 10 | (random() * 50, random() * 50), 11 | (random() * 50, random() * 50), 12 | (random() * 50, random() * 50), 13 | (random() * 50, random() * 50), 14 | ) 15 | 16 | 17 | class TestElementCubicBezierLength(unittest.TestCase): 18 | def test_cubic_bezier_length(self): 19 | n = 100 20 | error = 0 21 | for _ in range(n): 22 | b = get_random_cubic_bezier() 23 | l1 = b._length_scipy() 24 | l2 = b._length_default(error=1e-6) 25 | c = abs(l1 - l2) 26 | error += c 27 | self.assertAlmostEqual(l1, l2, places=1) 28 | print("Average cubic-line error: %g" % (error / n)) 29 | 30 | 31 | class TestElementCubicBezierPoint(unittest.TestCase): 32 | def test_cubic_bezier_point_start_stop(self): 33 | import numpy as np 34 | 35 | for _ in range(1000): 36 | b = get_random_cubic_bezier() 37 | self.assertEqual(b.start, b.point(0)) 38 | self.assertEqual(b.end, b.point(1)) 39 | self.assertTrue( 40 | np.all(np.array([list(b.start), list(b.end)]) == b.npoint([0, 1])) 41 | ) 42 | 43 | def test_cubic_bezier_point_implementations_match(self): 44 | import numpy as np 45 | 46 | for _ in range(1000): 47 | b = get_random_cubic_bezier() 48 | 49 | pos = np.linspace(0, 1, 100) 50 | 51 | v1 = b.npoint(pos) 52 | v2 = [] 53 | for i in range(len(pos)): 54 | v2.append(b.point(pos[i])) 55 | 56 | for p, p1, p2 in zip(pos, v1, v2): 57 | self.assertEqual(b.point(p), Point(p1)) 58 | self.assertEqual(Point(p1), Point(p2)) 59 | 60 | def test_cubic_bounds_issue_214(self): 61 | cubic = CubicBezier(0, -2 - 3j, -1 - 4j, -3j) 62 | bbox = cubic.bbox() 63 | self.assertLess(bbox[1], -3) 64 | 65 | def test_cubic_bounds_issue_214_random(self): 66 | for i in range(100): 67 | a = random() * 5 68 | b = random() * 5 69 | c = random() * 5 70 | d = a - 3 * b + 3 * c 71 | cubic1 = CubicBezier(a, b, c, d) 72 | bbox1 = cubic1.bbox() 73 | cubic2 = CubicBezier(a, b, c, d + 1e-11) 74 | bbox2 = cubic2.bbox() 75 | for a, b in zip(bbox1, bbox2): 76 | self.assertAlmostEqual(a, b, delta=1e-5) 77 | 78 | def test_cubic_bounds_issue_220(self): 79 | p = Path(transform=Matrix(682.657124793113, 0.000000000003, -0.000000000003, 682.657124793113, 257913.248909660178, -507946.354527872754)) 80 | p += CubicBezier(start=Point(-117.139521365,1480.99923469), control1=Point(-41.342266634,1505.62725567), control2=Point(40.3422666342,1505.62725567), end=Point(116.139521365,1480.99923469)) 81 | bounds = p.bbox() 82 | self.assertNotAlmostEqual(bounds[1], bounds[3], delta=100) 83 | -------------------------------------------------------------------------------- /test/test_descriptive_elements.py: -------------------------------------------------------------------------------- 1 | import io 2 | import unittest 3 | 4 | from svgelements import * 5 | 6 | 7 | class TestDescriptiveElements(unittest.TestCase): 8 | 9 | def test_descriptive_element(self): 10 | q = io.StringIO(u'''\n 11 | 12 | Who? 13 | My Friend. 14 | ''') 15 | m = SVG.parse(q) 16 | q = list(m.elements()) 17 | self.assertEqual(len(q), 3) 18 | self.assertEqual(q[1].title, "Who?") 19 | self.assertEqual(q[2].desc, "My Friend.") 20 | 21 | -------------------------------------------------------------------------------- /test/test_element.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from svgelements import * 4 | 5 | 6 | class TestElementElement(unittest.TestCase): 7 | """These tests ensure the performance of the SVGElement basecase.""" 8 | 9 | def test_element_id(self): 10 | values = {'id': 'my_id', 'random': True} 11 | r = Rect(values) 12 | self.assertEqual(values['id'], r.values['id']) 13 | self.assertEqual(values['random'], r.values['random']) 14 | self.assertRaises(KeyError, lambda: r.values['not_there']) 15 | r = Circle(values) 16 | self.assertEqual(values['id'], r.values['id']) 17 | self.assertEqual(values['random'], r.values['random']) 18 | self.assertRaises(KeyError, lambda: r.values['not_there']) 19 | r = Ellipse(values) 20 | self.assertEqual(values['id'], r.values['id']) 21 | self.assertEqual(values['random'], r.values['random']) 22 | self.assertRaises(KeyError, lambda: r.values['not_there']) 23 | r = Polygon(values) 24 | self.assertEqual(values['id'], r.values['id']) 25 | self.assertEqual(values['random'], r.values['random']) 26 | self.assertRaises(KeyError, lambda: r.values['not_there']) 27 | r = Polyline(values) 28 | self.assertEqual(values['id'], r.values['id']) 29 | self.assertEqual(values['random'], r.values['random']) 30 | self.assertRaises(KeyError, lambda: r.values['not_there']) 31 | r = SimpleLine(values) 32 | self.assertEqual(values['id'], r.values['id']) 33 | self.assertEqual(values['random'], r.values['random']) 34 | self.assertRaises(KeyError, lambda: r.values['not_there']) 35 | r = Path(values) 36 | self.assertEqual(values['id'], r.values['id']) 37 | self.assertEqual(values['random'], r.values['random']) 38 | self.assertRaises(KeyError, lambda: r.values['not_there']) 39 | r = SVGImage(values) 40 | self.assertEqual(values['id'], r.values['id']) 41 | self.assertEqual(values['random'], r.values['random']) 42 | self.assertRaises(KeyError, lambda: r.values['not_there']) 43 | r = SVGText(values) 44 | self.assertEqual(values['id'], r.values['id']) 45 | self.assertEqual(values['random'], r.values['random']) 46 | self.assertRaises(KeyError, lambda: r.values['not_there']) 47 | 48 | def test_element_merge(self): 49 | values = {'id': 'my_id', 'random': True} 50 | r = Rect(values, random=False, tat='awesome') 51 | self.assertEqual(r.values['id'], values['id']) 52 | self.assertNotEqual(r.values['random'], values['random']) 53 | self.assertEqual(r.values['tat'], 'awesome') 54 | 55 | r = Rect(fill='red') 56 | self.assertEqual(r.fill, '#f00') 57 | 58 | def test_element_propagate(self): 59 | 60 | values = {'id': 'my_id', 'random': True} 61 | r = Rect(values, random=False, tat='awesome') 62 | r = Rect(r) 63 | self.assertEqual(r.values['id'], values['id']) 64 | self.assertNotEqual(r.values['random'], values['random']) 65 | self.assertEqual(r.values['tat'], 'awesome') 66 | 67 | r = Rect(fill='red') 68 | r = Rect(r) 69 | self.assertEqual(r.fill, '#f00') 70 | r = Rect(stroke='red') 71 | r = Rect(r) 72 | self.assertEqual(r.stroke, '#f00') 73 | 74 | r = Rect(width=20) 75 | r = Rect(r) 76 | self.assertEqual(r.width, 20) 77 | 78 | p = Path('M0,0 20,0 0,20z M20,20 40,20 20,40z', fill='red') 79 | p2 = Path(p.subpath(1)) 80 | p2[0].start = None 81 | self.assertEqual(p2, 'M20,20 40,20 20,40z') 82 | self.assertEqual(p2.fill, 'red') 83 | 84 | -------------------------------------------------------------------------------- /test/test_generation.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from svgelements import * 4 | 5 | paths = [ 6 | 'M 100,100 L 300,100 L 200,300 Z', 7 | 'M 0,0 L 50,20 M 100,100 L 300,100 L 200,300 Z', 8 | 'M 100,100 L 200,200', 9 | 'M 100,200 L 200,100 L -100,-200', 10 | 'M 100,200 C 100,100 250,100 250,200 S 400,300 400,200', 11 | 'M 100,200 C 100,100 400,100 400,200', 12 | 'M 100,500 C 25,400 475,400 400,500', 13 | 'M 100,800 C 175,700 325,700 400,800', 14 | 'M 600,200 C 675,100 975,100 900,200', 15 | 'M 600,500 C 600,350 900,650 900,500', 16 | 'M 600,800 C 625,700 725,700 750,800 S 875,900 900,800', 17 | 'M 200,300 Q 400,50 600,300 T 1000,300', 18 | 'M -3.4E+38,3.4E+38 L -3.4E-38,3.4E-38', 19 | 'M 0,0 L 50,20 M 50,20 L 200,100 Z', 20 | 'M 600,350 L 650,325 A 25,25 -30 0,1 700,300 L 750,275', 21 | ] 22 | 23 | 24 | class TestGeneration(unittest.TestCase): 25 | """Examples from the SVG spec""" 26 | 27 | def test_svg_examples(self): 28 | for path in paths[15:]: 29 | self.assertEqual(Path(path).d(), path) 30 | 31 | def test_svg_example0(self): 32 | path = paths[0] 33 | self.assertEqual(Path(path).d(), path) 34 | 35 | def test_svg_example1(self): 36 | path = paths[1] 37 | self.assertEqual(Path(path).d(), path) 38 | 39 | def test_svg_example2(self): 40 | path = paths[2] 41 | self.assertEqual(Path(path).d(), path) 42 | 43 | def test_svg_example3(self): 44 | path = paths[3] 45 | self.assertEqual(Path(path).d(), path) 46 | 47 | def test_svg_example4(self): 48 | path = paths[4] 49 | self.assertEqual(Path(path).d(), path) 50 | 51 | def test_svg_example5(self): 52 | path = paths[5] 53 | self.assertEqual(Path(path).d(), path) 54 | 55 | def test_svg_example6(self): 56 | path = paths[6] 57 | self.assertEqual(Path(path).d(), path) 58 | 59 | def test_svg_example7(self): 60 | path = paths[7] 61 | self.assertEqual(Path(path).d(), path) 62 | 63 | def test_svg_example8(self): 64 | path = paths[8] 65 | self.assertEqual(Path(path).d(), path) 66 | 67 | def test_svg_example9(self): 68 | path = paths[9] 69 | self.assertEqual(Path(path).d(), path) 70 | 71 | def test_svg_example10(self): 72 | path = paths[10] 73 | self.assertEqual(Path(path).d(), path) 74 | 75 | def test_svg_example11(self): 76 | path = paths[11] 77 | self.assertEqual(Path(path).d(), path) 78 | 79 | def test_svg_example12(self): 80 | path = paths[12] 81 | self.assertEqual(Path(path).d(), path) 82 | 83 | def test_svg_example13(self): 84 | path = paths[13] 85 | self.assertEqual(Path(path).d(), path) 86 | 87 | def test_svg_example14(self): 88 | path = paths[14] 89 | self.assertEqual(Path(path).d(), "M 600,350 L 650,325 A 27.9508,27.9508 -30 0,1 700,300 L 750,275") 90 | # Too small arc forced increase rx,ry 91 | -------------------------------------------------------------------------------- /test/test_group.py: -------------------------------------------------------------------------------- 1 | import io 2 | import unittest 3 | 4 | from svgelements import * 5 | 6 | 7 | class TestElementGroup(unittest.TestCase): 8 | 9 | def test_group_bbox(self): 10 | q = io.StringIO(u''' 11 | 12 | 13 | 14 | 15 | ''') 16 | m = SVG.parse(q, width=500, height=500) 17 | m *= 'scale(2)' 18 | for e in m.select(lambda e: isinstance(e, Rect)): 19 | self.assertEqual(e.x, 0) 20 | self.assertEqual(e.y, 40) 21 | self.assertEqual(e.width, 100) 22 | self.assertEqual(e.height, 100) 23 | self.assertEqual(m.width, 200) 24 | self.assertEqual(m.height, 200) 25 | 26 | def test_group_2rect(self): 27 | q = io.StringIO(u''' 28 | 29 | 30 | 31 | 32 | 33 | ''') 34 | m = SVG.parse(q, width=500, height=500, reify=False) 35 | m *= 'scale(2)' 36 | rects = list(m.select(lambda e: isinstance(e, Rect))) 37 | r0 = rects[0] 38 | self.assertEqual(r0.implicit_x, 0) 39 | self.assertEqual(r0.implicit_y, 80) 40 | self.assertEqual(r0.implicit_width, 200) 41 | self.assertEqual(r0.implicit_height, 200) 42 | self.assertEqual(m.width, 200) 43 | self.assertEqual(m.height, 200) 44 | self.assertEqual(r0.bbox(), (0.0, 80.0, 200.0, 280.0)) 45 | m.reify() 46 | self.assertEqual(m.implicit_width, 400) 47 | self.assertEqual(m.implicit_height, 400) 48 | r1 = rects[1] 49 | self.assertEqual(r1.implicit_x, 0) 50 | self.assertEqual(r1.implicit_y, 0) 51 | self.assertAlmostEqual(r1.implicit_width, 200) 52 | self.assertAlmostEqual(r1.implicit_height, 200) 53 | print(r1.bbox()) 54 | 55 | def test_issue_107(self): 56 | """ 57 | Tests issue 107 inability to multiple group matrix objects while creating new group objects. 58 | 59 | https://github.com/meerk40t/svgelements/issues/107 60 | """ 61 | q = io.StringIO(u''' 62 | 63 | 64 | 65 | 66 | ''') 67 | m = SVG.parse(q) 68 | m *= "translate(100,100)" # Test __imul__ 69 | n = m * 'scale(2)' # Test __mult__ 70 | self.assertEqual(n[0][0].transform, Matrix("matrix(2,0,0,2,200,200)")) 71 | self.assertEqual(m[0][0].transform, Matrix("matrix(1,0,0,1,100,100)")) 72 | 73 | def test_issue_152(self): 74 | """ 75 | Tests issue 152, closed text objects within a group with style:display=None 76 | This should have the SVG element and nothing else. 77 | 78 | https://github.com/meerk40t/svgelements/issues/152 79 | """ 80 | q = io.StringIO(u''' 81 | 82 | Issue 152 83 | 84 | ''') 85 | elements = list(SVG.parse(q).elements()) 86 | self.assertEqual(len(elements), 1) 87 | -------------------------------------------------------------------------------- /test/test_image.py: -------------------------------------------------------------------------------- 1 | import io 2 | import unittest 3 | 4 | from svgelements import * 5 | 6 | ISSUE_239 = """ 7 | 8 | 9 | 23 | 31 | """ 32 | 33 | 34 | class TestElementImage(unittest.TestCase): 35 | def test_image_preserveaspectratio(self): 36 | """ 37 | "none" stretches to whatever width and height were given. 38 | """ 39 | q = io.StringIO( 40 | u''' 41 | ' 42 | 43 | 44 | ''') 45 | svg = SVG.parse(q) 46 | m = list(svg.elements()) 47 | a = m[1] 48 | a.load() 49 | self.assertEqual(type(a), Image) 50 | self.assertEqual(a.bbox(), (0.0, 0.0, 123, 321)) 51 | 52 | def test_image_preserveaspectratio_default(self): 53 | """ 54 | Square image, at 5/10 it's centered along the width putting it 5x5 image translateX(2.5) 55 | """ 56 | q = io.StringIO( 57 | u''' 58 | ' 59 | 60 | 61 | ''') 62 | svg = SVG.parse(q) 63 | m = list(svg.elements()) 64 | a = m[1] 65 | a.load() 66 | self.assertEqual(type(a), Image) 67 | self.assertEqual((2.5, 0.0, 7.5, 5.0), a.bbox()) 68 | 69 | def test_image_datauri(self): 70 | e = Image(href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAEFklEQVR4nO3dsW7bQBRFwSjw//8yU6UJYDgil9xdnZnalp6LewhBhV/HcRy/gKTfsw8A5hEACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACPuafUDd6/X68WeO43jgEooE4GH/M/iffkcQGEUAHnJm+D+9lhBwlQDcbOTwv3ttIeAsAbjJncP/7r2EgHf5FuAGT45/hfdlXwIw2OwRzn5/9iIAA60yvlXuYH0CMMhqo1vtHtYkAAOsOrZV72IdAnDR6iNb/T7mEoALdhnXLnfyPAGAMAE4aben6m738gwBgDABOGHXp+mud3MfAYAwAXjT7k/R3e9nLAGAMAGAMAGAMAF4w6d8fv6Uv4PrBADCBADCBADCBADCBADCBADCBADCBADCBOANn/Kfdz7l7+A6AYAwAYAwAYAwAXjT7p+fd7+fsQQAwgTghF2forvezX0EAMIE4KTdnqa73cszBADCBOCCXZ6qu9zJ8wTgotXHtfp9zCUAA6w6slXvYh0CMMhqY1vtHtYkAAOtMrpV7mB9AjDY7PHNfn/2IgA3mDVC4+ddX7MP+FR/x/jEf+ExfM4SgJvdGQLD5yoBeMjIEBg+owjAw/4d7/8EweC5iwBMZtzM5FsACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACBMACPsDhkRyDUVGTB8AAAAASUVORK5CYII=") 71 | self.assertEqual(e.data[:6], b"\x89PNG\r\n") 72 | e1 = Image(href="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAEAAQADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiq99f2emWcl5f3cFpax43zTyCNFyQBljwMkgfjXj/iH9ozQtPvBBoemT6vGPvzvIbZDwCNoKlj1IOQuMcZBzQB7RRXzB/w0d4w/6Buh/wDfib/47XV+Hv2kLG5uILfxBoslmhRVku7WXzVD5ALGMgFU+8eCxGAMN1oA90orP0bXNL8Q6cl/pF/Be2rYG+F87SQDtYdVbBGVOCM8itCgAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAqvf31vpmnXN/eSeXa2sTzTPtJ2ooJY4HJwAelWK8X/aM8Q3Gn+F9O0OBcR6pKzzvkcpEUITBHdmU5BGNmOQTQB5J8TPiZfeP9UCIJLbRbdybW0J5J6eZJjguR26KDgdSW4OiigAooooA6jwL461TwHrgv7A+bbyYW6tHbCXCDsfRhk4btnuCQfsPw5r9j4p8P2etaa0htLpCyeYu1lIJVlI9QwI4yOOCRzXwpXvH7N3iG4XUdW8NMu61eL7fGcgeW4KRt2ydwZO/GzpyaAPoeiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACvmD9o7/koen/8AYKj/APRstfT9eF/tIeHprnS9I8QW9tGyWjvb3UioS4V8GMsQPuBgw5PBkGPvGgD50ooooAKKKKACvQPgl/yV7Qv+3j/0nkrz+vbP2cvD01z4l1DxBLbRtaWlubeKSRCSJnIOUOMZCBg2DkCQdmoA+k6KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKz9c0az8Q6He6RfpvtbuJonwASuejLkEBgcEHHBANaFFAHxR468C6p4D1w2F+PNt5Mta3aLhLhB3HowyMr2z3BBPL1916/4c0jxTpbabrVjHd2hcPsYlSrDoVZSCp6jII4JHQmvDPEP7N14t4G8NazA9q3WPUiVePgfxIpD5O7+FccDnrQB4PRXoH/AApL4h/9C9/5O2//AMcrrPD37OWtXNxBL4g1O0s7QoryRWpMs4ORmM5ARTjcNwLAEDhhQB5X4Y8Map4u1yHSNIg824k5Zm4SJB1dz2UZH5gAEkA/Y/gvwnZ+CvC9rotm/m+Vl5pygRppGOWYgfgBnJChRk4zR4T8F6F4K05rPRbTyvN2meZ2LyTMowCzH8TgYUEnAGTXQUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAH//Z") 73 | self.assertEqual(e1.data[:3], b"\xff\xd8\xff") 74 | e2 = Image(href="data:text/plain;base64,c3ZnZWxlbWVudHMgcmVhZHMgc3ZnIGZpbGVz") 75 | self.assertEqual(e2.data, b"svgelements reads svg files") 76 | e3 = Image(href="data:text/vnd-example+xyz;foo=bar;base64,R0lGODdh") 77 | self.assertEqual(e3.data, b"GIF87a") 78 | e4 = Image(href="data:text/plain;charset=UTF-8;page=21,the%20data:1234,5678") 79 | self.assertEqual(e4.data, b"the data:1234,5678") 80 | 81 | def test_image_issue_239(self): 82 | """ 83 | Tests issue 239 newline characters in embedded png data. 84 | """ 85 | q = io.StringIO(ISSUE_239) 86 | svg = SVG.parse(q) 87 | m = list(svg.elements()) 88 | a = m[1] 89 | a.load_data() 90 | self.assertEqual(type(a), Image) 91 | -------------------------------------------------------------------------------- /test/test_intersections.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from operator import itemgetter 3 | 4 | from svgelements import * 5 | 6 | TOL = 1e-4 # default for tests that don't specify a `delta` or `places` 7 | 8 | 9 | class TestElementIntersections(unittest.TestCase): 10 | 11 | def test_intersect(self): 12 | """ 13 | test that `some_seg.intersect(another_seg)` will produce properly 14 | ordered tuples, i.e. the first element in each tuple refers to 15 | `some_seg` and the second element refers to `another_seg`. 16 | Also tests that the correct number of intersections is found. 17 | 18 | * This test adapted from svgpathtools 19 | """ 20 | a = Line(0 + 200j, 300 + 200j) 21 | b = QuadraticBezier(40 + 150j, 70 + 200j, 210 + 300j) 22 | c = CubicBezier(60 + 150j, 40 + 200j, 120 + 250j, 200 + 160j) 23 | d = Arc(70 + 150j, 50 + 100j, 0, 0, 0, 200 + 100j) 24 | segdict = {'line': a, "quadratic": b, 'cubic': c, 'arc': d} 25 | 26 | # test each segment type against each other type 27 | for x, y in [(x, y) for x in segdict for y in segdict]: 28 | if x == y: 29 | continue 30 | x = segdict[x] 31 | y = segdict[y] 32 | xiy = sorted(x.intersect(y)) 33 | yix = sorted(y.intersect(x), key=itemgetter(1)) 34 | for xy, yx in zip(xiy, yix): 35 | self.assertAlmostEqual(xy[0], yx[1], delta=TOL) 36 | self.assertAlmostEqual(xy[1], yx[0], delta=TOL) 37 | self.assertAlmostEqual(x.point(xy[0]), y.point(yx[0]), delta=TOL) 38 | self.assertTrue(len(xiy) == len(yix)) 39 | 40 | # test each segment against another segment of same type 41 | for x in segdict: 42 | count = 1 43 | if x == "arc": 44 | count = 2 45 | x = segdict[x] 46 | mid = x.point(0.5) 47 | y = x * Matrix(f"translate(5,0) rotate(90, {mid.x}, {mid.y})") 48 | xiy = sorted(x.intersect(y)) 49 | yix = sorted(y.intersect(x), key=itemgetter(1)) 50 | for xy, yx in zip(xiy, yix): 51 | self.assertAlmostEqual(xy[0], yx[1], delta=TOL) 52 | self.assertAlmostEqual(xy[1], yx[0], delta=TOL) 53 | self.assertAlmostEqual(x.point(xy[0]), y.point(yx[0]), delta=TOL) 54 | self.assertTrue(len(xiy) == len(yix)) 55 | 56 | self.assertTrue(len(xiy) == count) 57 | self.assertTrue(len(yix) == count) 58 | -------------------------------------------------------------------------------- /test/test_length.py: -------------------------------------------------------------------------------- 1 | import io 2 | import unittest 3 | 4 | from svgelements import * 5 | 6 | 7 | class TestElementLength(unittest.TestCase): 8 | """Tests the functionality of the Length Element.""" 9 | 10 | def test_length_parsing(self): 11 | self.assertAlmostEqual(Length('10cm'), (Length('100mm'))) 12 | self.assertNotEqual(Length("1mm"), 0) 13 | self.assertNotEqual(Length("1cm"), 0) 14 | self.assertNotEqual(Length("1in"), 0) 15 | self.assertNotEqual(Length("1px"), 0) 16 | self.assertNotEqual(Length("1pc"), 0) 17 | self.assertNotEqual(Length("1pt"), 0) 18 | self.assertNotEqual(Length("1%").value(relative_length=100), 0) 19 | self.assertEqual(Length("50%").value(relative_length=100), 50.0) 20 | 21 | def test_distance_matrix(self): 22 | m = Matrix("Translate(20mm,50%)", ppi=1000, width=600, height=800) 23 | self.assertEqual(Matrix(1, 0, 0, 1, 787.402, 400), m) 24 | m = Matrix("Translate(20mm,50%)") 25 | m.render(ppi=1000, width=600, height=800) 26 | self.assertEqual(Matrix(1, 0, 0, 1, 787.402, 400), m) 27 | 28 | def test_rect_distance_percent(self): 29 | rect = Rect("0%", "0%", "100%", "100%") 30 | rect.render(relative_length="1mm", ppi=DEFAULT_PPI) 31 | self.assertEqual(rect, Path("M 0,0 H 3.7795296 V 3.7795296 H 0 z")) 32 | rect = Rect("0%", "0%", "100%", "100%") 33 | rect.render(relative_length="1in", ppi=DEFAULT_PPI) 34 | self.assertEqual(rect, Path("M 0,0 H 96 V 96 H 0 z")) 35 | 36 | def test_circle_distance_percent(self): 37 | shape = Circle(0, 0, "50%") 38 | shape.render(relative_length="1in", ppi=DEFAULT_PPI) 39 | print(shape.d()) 40 | self.assertEqual( 41 | shape, 42 | Path('M48,0A48,48 0 0,1 0,48A48,48 0 0,1-48,0A48,48 0 0,1 0,-48A48,48 0 0,1 48,0Z') 43 | ) 44 | 45 | def test_length_division(self): 46 | self.assertEqual(Length("1mm") // Length('1mm'), 1.0) 47 | self.assertEqual(Length("1mm") / Length('1mm'), 1.0) 48 | self.assertEqual(Length('1in') / '1in', 1.0) 49 | self.assertEqual(Length('1cm') / '1mm', 10.0) 50 | 51 | def test_length_compare(self): 52 | self.assertTrue(Length('1in') < Length('2.6cm')) 53 | self.assertTrue(Length('1in') < '2.6cm') 54 | self.assertFalse(Length('1in') < '2.5cm') 55 | self.assertTrue(Length('10mm') >= '1cm') 56 | self.assertTrue(Length('10mm') <= '1cm') 57 | self.assertTrue(Length('11mm') >= '1cm') 58 | self.assertTrue(Length('10mm') <= '1.1cm') 59 | self.assertFalse(Length('11mm') <= '1cm') 60 | self.assertFalse(Length('10mm') >= '1.1cm') 61 | self.assertTrue(Length('20%') > '10%') 62 | self.assertRaises(ValueError, lambda: Length('20%') > '1in') 63 | self.assertRaises(ValueError, lambda: Length('20px') > '1in') 64 | self.assertRaises(ValueError, lambda: Length('20pc') > '1in') 65 | self.assertRaises(ValueError, lambda: Length('20em') > '1in') 66 | self.assertEqual(max(Length('1in'), Length('2.5cm')), '1in') 67 | 68 | def test_length_parsed(self): 69 | q = io.StringIO(u''' 70 | 71 | 72 | ''') 73 | m = SVG.parse(q, ppi=96.0) 74 | q = list(m.elements()) 75 | self.assertEqual(q[1].x, 96.0) 76 | self.assertEqual(q[1].y, 96.0) 77 | self.assertEqual(q[1].width, 960) 78 | self.assertEqual(q[1].height, 960) 79 | 80 | def test_length_parsed_percent(self): 81 | q = io.StringIO(u''' 82 | 83 | 84 | ''') 85 | m = SVG.parse(q, width=1000, height=1000) 86 | q = list(m.elements()) 87 | self.assertEqual(q[1].x, 250) 88 | self.assertEqual(q[1].y, 250) 89 | self.assertEqual(q[1].width, 500) 90 | self.assertEqual(q[1].height, 500) 91 | 92 | def test_length_parsed_percent2(self): 93 | q = io.StringIO(u'''\n 94 | 95 | 96 | ''') 97 | m = SVG.parse(q, width=1000, height=1000) 98 | q = list(m.elements()) 99 | self.assertEqual(q[1].x, 24) 100 | self.assertEqual(q[1].y, 24) 101 | self.assertEqual(q[1].width, 48) 102 | self.assertEqual(q[1].height, 48) 103 | 104 | def test_length_parsed_percent3(self): 105 | q = io.StringIO(u''' 106 | 107 | 108 | ''') 109 | m = SVG.parse(q, width=500, height=500) 110 | q = list(m.elements()) 111 | self.assertEqual(q[1].x, 24) 112 | self.assertEqual(q[1].y, 24) 113 | self.assertEqual(q[1].width, 48) 114 | self.assertEqual(q[1].height, 48) 115 | 116 | def test_length_parsed_percent4(self): 117 | q = io.StringIO(u''' 118 | 119 | 120 | ''') 121 | m = SVG.parse(q, width="garbage", height=500) 122 | q = list(m.elements()) 123 | self.assertEqual(q[1].x, 24) 124 | self.assertEqual(q[1].y, 24) 125 | self.assertEqual(q[1].width, 48) 126 | self.assertEqual(q[1].height, 48) 127 | 128 | def test_length_parsed_percent5(self): 129 | q = io.StringIO(u''' 130 | 131 | 132 | 133 | ''') 134 | m = SVG.parse(q, width="1in", height="1in") 135 | q = list(m.elements()) 136 | self.assertEqual(q[1].x, 24) 137 | self.assertEqual(q[1].y, 24) 138 | self.assertEqual(q[1].width, 48) 139 | self.assertEqual(q[1].height, 48) 140 | self.assertEqual(q[2].x, 240) 141 | self.assertEqual(q[2].y, 240) 142 | self.assertEqual(q[2].width, 480) 143 | self.assertEqual(q[2].height, 480) 144 | 145 | def test_length_parsed_percent6(self): 146 | q = io.StringIO(u''' 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | ''') 159 | m = SVG.parse(q, width="10000", height="10000") 160 | q = list(m.elements()) 161 | self.assertAlmostEqual(q[2].cx, q[3].cx, delta=1) 162 | self.assertAlmostEqual(q[2].cy, q[3].cy, delta=1) 163 | self.assertAlmostEqual(q[5].rx, q[6].rx, delta=1) 164 | self.assertAlmostEqual(q[6].rx, q[7].rx, delta=1) 165 | 166 | def test_length_viewbox(self): 167 | q = io.StringIO(u'''''') 171 | m = SVG.parse(q, ppi=96.0) 172 | q = list(m.elements()) 173 | self.assertEqual(q[0].width, 750) 174 | self.assertEqual(q[0].height, 950) 175 | -------------------------------------------------------------------------------- /test/test_matrix.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from svgelements import * 4 | 5 | 6 | class TestPathMatrix(unittest.TestCase): 7 | """Tests the functionality of the Matrix element.""" 8 | 9 | def test_rotate_css_angles(self): 10 | matrix = Matrix("rotate(90, 100,100)") 11 | path = Path("M0,0Z") 12 | path *= matrix 13 | self.assertEqual("M 200,0 Z", path.d()) 14 | matrix = Matrix("rotate(90deg, 100,100)") 15 | path = Path("M0,0Z") 16 | path *= matrix 17 | self.assertEqual("M 200,0 Z", path.d()) 18 | matrix = Matrix("rotate(0.25turn, 100,100)") 19 | path = Path("M0,0Z") 20 | path *= matrix 21 | self.assertEqual("M 200,0 Z", path.d()) 22 | matrix = Matrix("rotate(100grad, 100,100)") 23 | path = Path("M0,0Z") 24 | path *= matrix 25 | self.assertEqual("M 200,0 Z", path.d()) 26 | matrix = Matrix("rotate(1.5707963267948966rad, 100,100)") 27 | path = Path("M0,0Z") 28 | path *= matrix 29 | self.assertEqual("M 200,0 Z", path.d()) 30 | 31 | def test_matrix_multiplication(self): 32 | self.assertEqual( 33 | Matrix("scale(0.2) translate(-5,-5)"), 34 | Matrix("translate(-5,-5)") * Matrix("scale(0.2)"), 35 | ) 36 | self.assertEqual( 37 | Matrix("translate(-5,-5) scale(0.2)"), 38 | Matrix("scale(0.2)") * Matrix("translate(-5,-5)"), 39 | ) 40 | 41 | def test_rotate_css_distance(self): 42 | matrix = Matrix("rotate(90deg,100cm,100cm)") 43 | matrix.render(ppi=DEFAULT_PPI) 44 | path = Path("M0,0z") 45 | path *= matrix 46 | d = Length("1cm").value(ppi=DEFAULT_PPI) 47 | p2 = Path("M 200,0 Z") * Matrix("scale(%f)" % d) 48 | p2.values[SVG_ATTR_VECTOR_EFFECT] = SVG_VALUE_NON_SCALING_STROKE 49 | self.assertEqual(p2, path) 50 | 51 | def test_skew_single_value(self): 52 | m0 = Matrix("skew(15deg,0deg)") 53 | m1 = Matrix("skewX(15deg)") 54 | self.assertEqual(m0, m1) 55 | m0 = Matrix("skew(0deg,15deg)") 56 | m1 = Matrix("skewY(15deg)") 57 | self.assertEqual(m0, m1) 58 | 59 | def test_scale_single_value(self): 60 | m0 = Matrix("scale(2,1)") 61 | m1 = Matrix("scaleX(2)") 62 | self.assertEqual(m0, m1) 63 | m0 = Matrix("scale(1,2)") 64 | m1 = Matrix("scaleY(2)") 65 | self.assertEqual(m0, m1) 66 | 67 | def test_translate_single_value(self): 68 | m0 = Matrix("translate(500cm,0)") 69 | m1 = Matrix("translateX(500cm)") 70 | self.assertEqual(m0, m1) 71 | m0 = Matrix("translate(0,500cm)") 72 | m1 = Matrix("translateY(500cm)") 73 | self.assertEqual(m0, m1) 74 | m0 = Matrix("translate(500cm)") 75 | m1 = Matrix("translateX(500cm)") 76 | self.assertEqual(m0, m1) 77 | 78 | def test_translate_css_value(self): 79 | m0 = Matrix("translate(50mm,5cm)") 80 | m1 = Matrix("translate(5cm,5cm)") 81 | self.assertEqual(m0, m1) 82 | 83 | def test_rotate_css_value(self): 84 | m0 = Matrix("rotate(90deg, 50cm,50cm)", ppi=DEFAULT_PPI) 85 | m1 = Matrix("rotate(0.25turn, 500mm,500mm)", ppi=DEFAULT_PPI) 86 | self.assertEqual(m0, m1) 87 | 88 | def test_transform_translate(self): 89 | matrix = Matrix("translate(5,4)") 90 | path = Path() 91 | path.move((0, 0), (0, 100), (100, 100), 100 + 0j, "z").closed() 92 | path *= matrix 93 | self.assertEqual("M 5,4 L 5,104 L 105,104 L 105,4 L 5,4 Z", path.d()) 94 | 95 | def test_transform_scale(self): 96 | matrix = Matrix("scale(2)") 97 | path = Path() 98 | path.move((0, 0), (0, 100), (100, 100), 100 + 0j, "z").closed() 99 | path *= matrix 100 | self.assertEqual("M 0,0 L 0,200 L 200,200 L 200,0 L 0,0 Z", path.d()) 101 | 102 | def test_transform_rotate(self): 103 | matrix = Matrix("rotate(360)") 104 | path = Path() 105 | path.move((0, 0), (0, 100), (100, 100), 100 + 0j, "z") 106 | path *= matrix 107 | self.assertAlmostEqual(path[0][1].x, 0) 108 | self.assertAlmostEqual(path[0][1].y, 0) 109 | 110 | self.assertAlmostEqual(path[1][1].x, 0) 111 | self.assertAlmostEqual(path[1][1].y, 100) 112 | 113 | self.assertAlmostEqual(path[2][1].x, 100) 114 | self.assertAlmostEqual(path[2][1].y, 100) 115 | self.assertAlmostEqual(path[3][1].x, 100) 116 | self.assertAlmostEqual(path[3][1].y, 0) 117 | self.assertAlmostEqual(path[4][1].x, 0) 118 | self.assertAlmostEqual(path[4][1].y, 0) 119 | 120 | def test_transform_value(self): 121 | matrix = Matrix("rotate(360,50,50)") 122 | path = Path() 123 | path.move((0, 0), (0, 100), (100, 100), 100 + 0j, "z") 124 | path *= matrix 125 | self.assertAlmostEqual(path[0][1].x, 0) 126 | self.assertAlmostEqual(path[0][1].y, 0) 127 | 128 | self.assertAlmostEqual(path[1][1].x, 0) 129 | self.assertAlmostEqual(path[1][1].y, 100) 130 | 131 | self.assertAlmostEqual(path[2][1].x, 100) 132 | self.assertAlmostEqual(path[2][1].y, 100) 133 | self.assertAlmostEqual(path[3][1].x, 100) 134 | self.assertAlmostEqual(path[3][1].y, 0) 135 | self.assertAlmostEqual(path[4][1].x, 0) 136 | self.assertAlmostEqual(path[4][1].y, 0) 137 | 138 | def test_transform_skewx(self): 139 | matrix = Matrix("skewX(10,50,50)") 140 | path = Path() 141 | path.move((0, 0), (0, 100), (100, 100), 100 + 0j, "z").closed() 142 | path *= matrix 143 | self.assertEqual( 144 | "M -8.81634903542,0 L 8.81634903542,100 L 108.816349035,100 L 91.1836509646,0 L -8.81634903542,0 Z", 145 | path.d(), 146 | ) 147 | 148 | def test_transform_skewy(self): 149 | matrix = Matrix("skewY(10, 50,50)") 150 | path = Path() 151 | path.move((0, 0), (0, 100), (100, 100), 100 + 0j, "z").closed() 152 | path *= matrix 153 | self.assertEqual( 154 | "M 0,-8.81634903542 L 0,91.1836509646 L 100,108.816349035 L 100,8.81634903542 L 0,-8.81634903542 Z", 155 | path.d(), 156 | ) 157 | 158 | def test_matrix_repr_rotate(self): 159 | """ 160 | [a c e] 161 | [b d f] 162 | """ 163 | self.assertEqual(Matrix(0, 1, -1, 0, 0, 0), Matrix.rotate(radians(90))) 164 | 165 | def test_matrix_repr_scale(self): 166 | """ 167 | [a c e] 168 | [b d f] 169 | """ 170 | self.assertEqual(Matrix(2, 0, 0, 2, 0, 0), Matrix.scale(2)) 171 | 172 | def test_matrix_repr_hflip(self): 173 | """ 174 | [a c e] 175 | [b d f] 176 | """ 177 | self.assertEqual(Matrix(-1, 0, 0, 1, 0, 0), Matrix.scale(-1, 1)) 178 | 179 | def test_matrix_repr_vflip(self): 180 | """ 181 | [a c e] 182 | [b d f] 183 | """ 184 | self.assertEqual(Matrix(1, 0, 0, -1, 0, 0), Matrix.scale(1, -1)) 185 | 186 | def test_matrix_repr_post_cat(self): 187 | """ 188 | [a c e] 189 | [b d f] 190 | """ 191 | m = Matrix.scale(2) 192 | m.post_cat(Matrix.translate(-20, -20)) 193 | self.assertEqual(Matrix(2, 0, 0, 2, -20, -20), m) 194 | 195 | def test_matrix_repr_pre_cat(self): 196 | """ 197 | [a c e] 198 | [b d f] 199 | """ 200 | m = Matrix.translate(-20, -20) 201 | m.pre_cat(Matrix.scale(2)) 202 | self.assertEqual(Matrix(2, 0, 0, 2, -20, -20), m) 203 | 204 | def test_matrix_point_rotated_by_matrix(self): 205 | matrix = Matrix() 206 | matrix.post_rotate(radians(90), 100, 100) 207 | p = matrix.point_in_matrix_space((50, 50)) 208 | self.assertAlmostEqual(p[0], 150) 209 | self.assertAlmostEqual(p[1], 50) 210 | 211 | def test_matrix_point_scaled_in_space(self): 212 | matrix = Matrix() 213 | matrix.post_scale(2, 2, 50, 50) 214 | 215 | p = matrix.point_in_matrix_space((50, 50)) 216 | self.assertAlmostEqual(p[0], 50) 217 | self.assertAlmostEqual(p[1], 50) 218 | 219 | p = matrix.point_in_matrix_space((25, 25)) 220 | self.assertAlmostEqual(p[0], 0) 221 | self.assertAlmostEqual(p[1], 0) 222 | 223 | matrix.post_rotate(radians(45), 50, 50) 224 | p = matrix.point_in_matrix_space((25, 25)) 225 | self.assertAlmostEqual(p[0], 50) 226 | 227 | matrix = Matrix() 228 | matrix.post_scale(0.5, 0.5) 229 | p = matrix.point_in_matrix_space((100, 100)) 230 | self.assertAlmostEqual(p[0], 50) 231 | self.assertAlmostEqual(p[1], 50) 232 | 233 | matrix = Matrix() 234 | matrix.post_scale(2, 2, 100, 100) 235 | p = matrix.point_in_matrix_space((50, 50)) 236 | self.assertAlmostEqual(p[0], 0) 237 | self.assertAlmostEqual(p[1], 0) 238 | 239 | def test_matrix_cat_identity(self): 240 | identity = Matrix() 241 | from random import random 242 | 243 | for i in range(50): 244 | q = Matrix(random(), random(), random(), random(), random(), random()) 245 | p = copy(q) 246 | q.post_cat(identity) 247 | self.assertEqual(q, p) 248 | 249 | def test_matrix_pre_and_post_1(self): 250 | from random import random 251 | 252 | for i in range(50): 253 | tx = random() * 1000 - 500 254 | ty = random() * 1000 - 500 255 | rx = random() * 2 256 | ry = random() * 2 257 | a = random() * tau 258 | q = Matrix() 259 | q.post_translate(tx, ty) 260 | p = Matrix() 261 | p.pre_translate(tx, ty) 262 | self.assertEqual(p, q) 263 | 264 | q = Matrix() 265 | q.post_scale(rx, ry, tx, ty) 266 | p = Matrix() 267 | p.pre_scale(rx, ry, tx, ty) 268 | self.assertEqual(p, q) 269 | 270 | q = Matrix() 271 | q.post_rotate(a, tx, ty) 272 | p = Matrix() 273 | p.pre_rotate(a, tx, ty) 274 | self.assertEqual(p, q) 275 | 276 | q = Matrix() 277 | q.post_skew_x(a, tx, ty) 278 | p = Matrix() 279 | p.pre_skew_x(a, tx, ty) 280 | self.assertEqual(p, q) 281 | 282 | q = Matrix() 283 | q.post_skew_y(a, tx, ty) 284 | p = Matrix() 285 | p.pre_skew_y(a, tx, ty) 286 | self.assertEqual(p, q) 287 | 288 | def test_matrix_eval_repr(self): 289 | self.assertTrue(Matrix("rotate(20)") == eval(repr(Matrix("rotate(20)")))) 290 | self.assertFalse(Matrix("rotate(20)") != eval(repr(Matrix("rotate(20)")))) 291 | 292 | def test_matrix_reverse_scale(self): 293 | m1 = Matrix("scale(2)") 294 | m1.inverse() 295 | m2 = Matrix("scale(0.5)") 296 | self.assertEqual(m1, m2) 297 | m1.inverse() 298 | self.assertEqual(m1, "scale(2)") 299 | 300 | def test_matrix_reverse_translate(self): 301 | m1 = Matrix("translate(20,20)") 302 | m1.inverse() 303 | self.assertEqual(m1, Matrix("translate(-20,-20)")) 304 | 305 | def test_matrix_reverse_rotate(self): 306 | m1 = Matrix("rotate(30)") 307 | m1.inverse() 308 | self.assertEqual(m1, Matrix("rotate(-30)")) 309 | 310 | def test_matrix_reverse_skew(self): 311 | m1 = Matrix("skewX(1)") 312 | m1.inverse() 313 | self.assertEqual(m1, Matrix("skewX(-1)")) 314 | 315 | m1 = Matrix("skewY(1)") 316 | m1.inverse() 317 | self.assertEqual(m1, Matrix("skewY(-1)")) 318 | 319 | def test_matrix_reverse_scale_translate(self): 320 | m1 = Matrix("scale(2) translate(40,40)") 321 | m1.inverse() 322 | self.assertEqual(m1, Matrix("translate(-40,-40) scale(0.5)")) 323 | 324 | def test_matrix_map_identity(self): 325 | """ 326 | Maps one perspective the same perspective. 327 | """ 328 | m1 = Matrix.map( 329 | Point(1, 1), 330 | Point(1, -1), 331 | Point(-1, -1), 332 | Point(-1, 1), 333 | Point(1, 1), 334 | Point(1, -1), 335 | Point(-1, -1), 336 | Point(-1, 1), 337 | ) 338 | self.assertTrue(m1.is_identity()) 339 | 340 | m1 = Matrix.map( 341 | Point(101, 101), 342 | Point(101, 99), 343 | Point(99, 99), 344 | Point(99, 101), 345 | Point(101, 101), 346 | Point(101, 99), 347 | Point(99, 99), 348 | Point(99, 101), 349 | ) 350 | self.assertTrue(m1.is_identity()) 351 | 352 | def test_matrix_map_scale_half(self): 353 | m1 = Matrix.map( 354 | Point(2, 2), 355 | Point(2, -2), 356 | Point(-2, -2), 357 | Point(-2, 2), 358 | Point(1, 1), 359 | Point(1, -1), 360 | Point(-1, -1), 361 | Point(-1, 1), 362 | ) 363 | self.assertEqual(m1, Matrix.scale(0.5)) 364 | 365 | def test_matrix_map_translate(self): 366 | m1 = Matrix.map( 367 | Point(0, 0), 368 | Point(0, 1), 369 | Point(1, 1), 370 | Point(1, 0), 371 | Point(100, 100), 372 | Point(100, 101), 373 | Point(101, 101), 374 | Point(101, 100), 375 | ) 376 | self.assertEqual(m1, Matrix.translate(100, 100)) 377 | 378 | def test_matrix_map_rotate(self): 379 | m1 = Matrix.map( 380 | Point(0, 0), 381 | Point(0, 1), 382 | Point(1, 1), 383 | Point(1, 0), 384 | Point(0, 1), 385 | Point(1, 1), 386 | Point(1, 0), 387 | Point(0, 0), 388 | ) 389 | m2 = Matrix("rotate(-90deg, 0.5, 0.5)") 390 | self.assertEqual(m1, m2) 391 | 392 | def test_matrix_map_translate_scale_y(self): 393 | m1 = Matrix.map( 394 | Point(0, 0), 395 | Point(0, 1), 396 | Point(1, 1), 397 | Point(1, 0), 398 | Point(100, 100), 399 | Point(100, 102), 400 | Point(101, 102), 401 | Point(101, 100), 402 | ) 403 | self.assertEqual(m1, Matrix.scale_y(2) * Matrix.translate(100, 100)) 404 | 405 | def test_matrix_map_map(self): 406 | points = [Point(0, 0), Point(0, 1), Point(1, 1), Point(1, 0)] 407 | 408 | for m in ( 409 | Matrix("skewX(10deg)"), 410 | Matrix("skewX(8deg)"), 411 | Matrix("skewX(5deg)"), 412 | Matrix("skewY(10deg)"), 413 | Matrix("skewX(8deg)"), 414 | Matrix("skewX(5deg)"), 415 | Matrix("scale(1.4235)"), 416 | Matrix("scale(1.4235,4.39392)"), 417 | Matrix("translate(100,200)"), 418 | Matrix("translate(-50,-20.3949)"), 419 | ): 420 | points_to = [m.point_in_matrix_space(p) for p in points] 421 | m1 = Matrix.map(*points, *points_to) 422 | self.assertEqual(m, m1) 423 | 424 | def test_matrix_perspective_ccw_unit_square(self): 425 | """ 426 | This is the unit square ccw. So we mirror it across the x-axis and rotate it back into position. 427 | """ 428 | m1 = Matrix.perspective(Point(0, 0), Point(1, 0), Point(1, 1), Point(0, 1)) 429 | m2 = Matrix.scale(-1, 1) * Matrix("rotate(-90deg)") 430 | self.assertEqual(m1, m2) 431 | 432 | def test_matrix_perspective_unit_square(self): 433 | """ 434 | This is the cw unit square, which is our default perspective, meaning we have the identity matrix 435 | """ 436 | m1 = Matrix.perspective(Point(0, 0), Point(0, 1), Point(1, 1), Point(1, 0)) 437 | m2 = Matrix() 438 | self.assertEqual(m1, m2) 439 | 440 | def test_matrix_perspective_scale_rotate(self): 441 | m1 = Matrix.perspective(Point(-2, -2), Point(-2, 2), Point(2, 2), Point(2, -2)) 442 | m2 = Matrix("scale(4)") * Matrix("translate(-2,-2)") 443 | self.assertEqual(m1, m2) 444 | 445 | def test_matrix_map_map3(self): 446 | points = [Point(0, 0), Point(0, 1), Point(1, 0)] 447 | 448 | for m in ( 449 | Matrix("skewX(10deg)"), 450 | Matrix("skewX(8deg)"), 451 | Matrix("skewX(5deg)"), 452 | Matrix("skewY(10deg)"), 453 | Matrix("skewX(8deg)"), 454 | Matrix("skewX(5deg)"), 455 | Matrix("scale(1.4235)"), 456 | Matrix("scale(1.4235,4.39392)"), 457 | Matrix("translate(100,200)"), 458 | Matrix("translate(-50,-20.3949)"), 459 | ): 460 | points_to = [m.point_in_matrix_space(p) for p in points] 461 | m1 = Matrix.map3(*points, *points_to) 462 | self.assertEqual(m, m1) 463 | 464 | def test_matrix_affine_ccw_unit_square(self): 465 | """ 466 | This is the unit square ccw. So we mirror it across the x-axis and rotate it back into position. 467 | """ 468 | m1 = Matrix.affine(Point(0, 0), Point(1, 0), Point(0, 1)) 469 | m2 = Matrix.scale(-1, 1) * Matrix("rotate(-90deg)") 470 | self.assertEqual(m1, m2) 471 | 472 | def test_matrix_affine_unit_square(self): 473 | """ 474 | This is the cw unit square, which is our default perspective, meaning we have the identity matrix 475 | """ 476 | m1 = Matrix.affine(Point(0, 0), Point(0, 1), Point(1, 0)) 477 | m2 = Matrix() 478 | self.assertEqual(m1, m2) 479 | 480 | def test_matrix_affine_scale_rotate(self): 481 | m1 = Matrix.affine(Point(-2, -2), Point(-2, 2), Point(2, -2)) 482 | m2 = Matrix("scale(4)") * Matrix("translate(-2,-2)") 483 | self.assertEqual(m1, m2) 484 | -------------------------------------------------------------------------------- /test/test_path.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from svgelements import * 4 | 5 | 6 | class TestPath(unittest.TestCase): 7 | """Tests of the SVG Path element.""" 8 | 9 | def test_subpaths(self): 10 | path = Path("M0,0 50,50 100,100Z M0,100 50,50, 100,0") 11 | for i, p in enumerate(path.as_subpaths()): 12 | if i == 0: 13 | self.assertEqual(p.d(), "M 0,0 L 50,50 L 100,100 Z") 14 | elif i == 1: 15 | self.assertEqual(p.d(), "M 0,100 L 50,50 L 100,0") 16 | self.assertLessEqual(i, 1) 17 | 18 | def test_subpath_degenerate(self): 19 | path = Path("") 20 | for i, p in enumerate(path.as_subpaths()): 21 | pass 22 | 23 | def test_subpaths_no_move(self): 24 | path = Path("M0,0 50,0 50,50 0,50 Z L0,100 100,100 100,0") 25 | for i, p in enumerate(path.as_subpaths()): 26 | if i == 0: 27 | self.assertEqual(p.d(), "M 0,0 L 50,0 L 50,50 L 0,50 Z") 28 | elif i == 1: 29 | self.assertEqual(p.d(), "L 0,100 L 100,100 L 100,0") 30 | self.assertLessEqual(i, 1) 31 | path = Path("M0,0ZZZZZ") 32 | subpaths = list(path.as_subpaths()) 33 | self.assertEqual(len(subpaths), 5) 34 | 35 | def test_count_subpaths(self): 36 | path = Path("M0,0 50,50 100,100Z M0,100 50,50, 100,0") 37 | self.assertEqual(path.count_subpaths(), 2) 38 | 39 | def test_subpath(self): 40 | path = Path("M0,0 50,50 100,100Z M0,100 50,50, 100,0") 41 | subpath = path.subpath(0) 42 | self.assertEqual(subpath.d(), "M 0,0 L 50,50 L 100,100 Z") 43 | subpath = path.subpath(1) 44 | self.assertEqual(subpath.d(), "M 0,100 L 50,50 L 100,0") 45 | 46 | def test_move_quad_smooth(self): 47 | path = Path() 48 | path.move((4, 4), (20, 20), (25, 25), 6 + 3j) 49 | path.quad((20, 33), (100, 100)) 50 | path.smooth_quad((13, 45), (16, 16), (34, 56), "z").closed() 51 | self.assertEqual(path.d(), "M 4,4 L 20,20 L 25,25 L 6,3 Q 20,33 100,100 T 13,45 T 16,16 T 34,56 T 4,4 Z") 52 | 53 | def test_move_cubic_smooth(self): 54 | path = Path() 55 | path.move((4, 4), (20, 20), (25, 25), 6 + 3j) 56 | path.cubic((20, 33), (25, 25), (100, 100)) 57 | path.smooth_cubic((13, 45), (16, 16), (34, 56), "z").closed() 58 | self.assertEqual(path.d(), "M 4,4 L 20,20 L 25,25 L 6,3 C 20,33 25,25 100,100 S 13,45 16,16 S 34,56 4,4 Z") 59 | 60 | def test_convex_hull(self): 61 | pts = (3, 4), (4, 6), (18, -2), (9, 0) 62 | hull = [e for e in Point.convex_hull(pts)] 63 | self.assertEqual([(3, 4), (9, 0), (18, -2), (4, 6)], hull) 64 | 65 | # bounding box and a bunch of random numbers that must be inside. 66 | pts = [(100, 100), (100, -100), (-100, -100), (-100, 100)] 67 | from random import randint 68 | for i in range(50): 69 | pts.append((randint(-99, 99), randint(-99, 99))) 70 | hull = [e for e in Point.convex_hull(pts)] 71 | for p in hull: 72 | self.assertEqual(abs(p[0]), 100) 73 | self.assertEqual(abs(p[1]), 100) 74 | 75 | def test_reverse_path_q(self): 76 | path = Path("M1,0 22,7 Q 17,17 91,2") 77 | path.reverse() 78 | self.assertEqual(path, Path("M 91,2 Q 17,17 22,7 L 1,0")) 79 | 80 | def test_reverse_path_multi_move(self): 81 | path = Path("M1,0 M2,0 M3,0") 82 | path.reverse() 83 | self.assertEqual(path, "M3,0 M2,0 M1,0") 84 | path = Path("M1,0z M2,0z M3,0z") 85 | path.reverse() 86 | self.assertEqual(path, "M3,0 Z M2,0 Z M1,0 Z") 87 | 88 | def test_reverse_path_multipath(self): 89 | path = Path("M1,0 22,7 Q 17,17 91,2M0,0zM20,20z") 90 | path.reverse() 91 | self.assertEqual(path, Path("M20,20zM0,0zM 91,2 Q 17,17 22,7 L 1,0")) 92 | 93 | def test_path_mult_sideeffect(self): 94 | path = Path("M1,1 10,10 Q 17,17 91,2 T 9,9 C 40,40 20,0, 9,9 S 60,50 0,0 A 25,25 -30 0,1 30,30 z") 95 | q = path * "scale(2)" 96 | self.assertEqual(path, "M1,1 10,10 Q 17,17 91,2 T 9,9 C 40,40 20,0, 9,9 S 60,50 0,0 A 25,25 -30 0,1 30,30 z") 97 | 98 | def test_subpath_imult_sideeffect(self): 99 | path = Path("M1,1 10,10 Q 17,17 91,2 T 9,9 C 40,40 20,0, 9,9 S 60,50 0,0 A 25,25 -30 0,1 30,30 zM50,50z") 100 | self.assertEqual( 101 | path, 102 | "M1,1 10,10 Q 17,17 91,2 T 9,9 C 40,40 20,0, 9,9 S 60,50 0,0 A 25,25 -30 0,1 30,30 zM50,50z") 103 | for p in path.as_subpaths(): 104 | p *= "scale(2)" 105 | self.assertEqual( 106 | path, 107 | "M 2,2 L 20,20 Q 34,34 182,4 T 18,18 C 80,80 40,0 18,18 S 120,100 0,0 A 50,50 -30 0,1 60,60 ZM100,100z") 108 | 109 | def test_subpath_reverse(self): 110 | #Issue 45 111 | p = Path("M0,0 1,1") 112 | p.reverse() 113 | self.assertEqual(p, "M1,1 0,0") 114 | 115 | p = Path("M0,0 M1,1") 116 | p.reverse() 117 | self.assertEqual(p, "M1,1 M0,0") 118 | 119 | p = Path("M1,1 L5,5M2,1 L6,5M3,1 L7,5") 120 | subpaths = list(p.as_subpaths()) 121 | subpaths[1].reverse() 122 | self.assertEqual("M 1,1 L 5,5 M 6,5 L 2,1 M 3,1 L 7,5", str(p)) 123 | subpaths[1].reverse() 124 | self.assertEqual("M 1,1 L 5,5 M 2,1 L 6,5 M 3,1 L 7,5", str(p)) 125 | 126 | p = Path("M1,1 L5,5M2,1 L6,5ZM3,1 L7,5") 127 | subpaths = list(p.as_subpaths()) 128 | subpaths[1].reverse() 129 | self.assertEqual("M 6,5 L 2,1 Z", str(subpaths[1])) 130 | self.assertEqual("M 1,1 L 5,5 M 6,5 L 2,1 Z M 3,1 L 7,5", str(p)) 131 | 132 | p = Path("M1,1 L5,5M2,1 6,5 100,100 200,200 ZM3,1 L7,5") 133 | subpaths = list(p.as_subpaths()) 134 | subpaths[1].reverse() 135 | self.assertEqual("M 1,1 L 5,5 M 200,200 L 100,100 L 6,5 L 2,1 Z M 3,1 L 7,5", str(p)) 136 | 137 | def test_validation_delete(self): 138 | p = Path("M1,1 M2,2 M3,3 M4,4") 139 | del p[2] 140 | self.assertEqual(p, "M1,1 M2,2 M4,4") 141 | p = Path("M0,0 L 1,1 L 2,2 L 3,3 L 4,4z") 142 | del p[3] 143 | self.assertEqual(p, "M0,0 L 1,1 L 2,2 L 4,4z") 144 | p = Path("M0,0 L 1,1 L 2,2 M 3,3 L 4,4z") 145 | del p[3] 146 | self.assertEqual(p, "M0,0 L 1,1 L 2,2 L 4,4z") 147 | 148 | def test_validation_insert(self): 149 | p = Path("M1,1 M2,2 M4,4") 150 | p.insert(2, "M3,3") 151 | self.assertEqual(p, "M1,1 M2,2 M3,3 M4,4") 152 | p = Path("M0,0 L 1,1 L 2,2 L 4,4") 153 | p.insert(3, "L3,3") 154 | self.assertEqual(p, "M0,0 L 1,1 L 2,2 L 3,3 L 4,4") 155 | 156 | def test_validation_append(self): 157 | p = Path("M1,1 M2,2 M3,3") 158 | p.append("M4,4") 159 | self.assertEqual(p, "M1,1 M2,2 M3,3 M4,4") 160 | p = Path("M0,0 L 1,1 L 2,2 L 3,3") 161 | p.append("L4,4") 162 | self.assertEqual(p, "M0,0 L 1,1 L 2,2 L 3,3 L 4,4") 163 | p.append("Z") 164 | self.assertEqual(p, "M0,0 L 1,1 L 2,2 L 3,3 L 4,4 Z") 165 | 166 | p = Path("M1,1 M2,2") 167 | p.append("M3,3 M4,4") 168 | self.assertEqual(p, "M1,1 M2,2 M3,3 M4,4") 169 | p = Path("M0,0 L 1,1 L 2,2") 170 | p.append("L 3,3 L4,4") 171 | self.assertEqual(p, "M0,0 L 1,1 L 2,2 L 3,3 L 4,4") 172 | p.append("Z") 173 | self.assertEqual(p, "M0,0 L 1,1 L 2,2 L 3,3 L 4,4 Z") 174 | 175 | def test_validation_extend(self): 176 | p = Path("M1,1 M2,2") 177 | p.extend(Path("M3,3 M4,4")) 178 | self.assertEqual(p, "M1,1 M2,2 M3,3 M4,4") 179 | p = Path("M0,0 L 1,1 L 2,2") 180 | p.extend(Path("L 3,3 L4,4")) 181 | self.assertEqual(p, "M0,0 L 1,1 L 2,2 L 3,3 L 4,4") 182 | p.extend(Path("Z")) 183 | self.assertEqual(p, "M0,0 L 1,1 L 2,2 L 3,3 L 4,4 Z") 184 | 185 | p = Path("M1,1 M2,2") 186 | p.extend("M3,3 M4,4") 187 | self.assertEqual(p, "M1,1 M2,2 M3,3 M4,4") 188 | p = Path("M0,0 L 1,1 L 2,2") 189 | p.extend("L 3,3 L4,4") 190 | self.assertEqual(p, "M0,0 L 1,1 L 2,2 L 3,3 L 4,4") 191 | p.extend("Z") 192 | self.assertEqual(p, "M0,0 L 1,1 L 2,2 L 3,3 L 4,4 Z") 193 | 194 | def test_validation_setitem(self): 195 | p = Path("M1,1 M2,2 M3,3 M4,4") 196 | p[2] = Line(None, (3,3)) 197 | self.assertEqual(p, "M1,1 M2,2 L3,3 M4,4") 198 | p = Path("M0,0 L 1,1 L 2,2 L 3,3 L 4,4z") 199 | p[3] = Move(None, (3,3)) 200 | self.assertEqual(p, "M0,0 L 1,1 L 2,2 M3,3 L 4,4z") 201 | 202 | def test_validation_setitem_str(self): 203 | p = Path("M1,1 M2,2 M3,3 M4,4") 204 | p[2] = "L3,3" 205 | self.assertEqual(p, Path("M1,1 M2,2 L3,3 M4,4")) 206 | p = Path("M0,0 L 1,1 L 2,2 L 3,3 L 4,4z") 207 | p[3] = "M3,3" 208 | self.assertEqual(p, Path("M0,0 L 1,1 L 2,2 M3,3 L 4,4z")) 209 | 210 | def test_arc_start_t(self): 211 | m = Path("m 0,0 a 5.01,5.01 180 0,0 0,10 z" 212 | "m 0,0 a 65,65 180 0,0 65,66 z") 213 | for a in m: 214 | if isinstance(a, Arc): 215 | start_t = a.get_start_t() 216 | a_start = a.point_at_t(start_t) 217 | self.assertEqual(a.start, a_start) 218 | self.assertEqual(a.end, a.point_at_t(a.get_end_t())) 219 | 220 | def test_relative_roundabout(self): 221 | m = Path("m 0,0 a 5.01,5.01 180 0,0 0,10 z") 222 | self.assertEqual(m.d(), "m 0,0 a 5.01,5.01 180 0,0 0,10 z") 223 | m = Path("M0,0 1,1 z") 224 | self.assertEqual(m.d(), "M 0,0 L 1,1 z") 225 | self.assertEqual(m.d(relative=True), "m 0,0 l 1,1 z") 226 | self.assertEqual(m.d(relative=False), "M 0,0 L 1,1 Z") 227 | m = Path("M 4,4 L 20,20 L 25,25 L 6,3 Q 20,33 100,100 Q 180,167 13,45 T 16,16 T 34,56 T 4,4 z") 228 | self.assertEqual(m.d(), "M 4,4 L 20,20 L 25,25 L 6,3 Q 20,33 100,100 Q 180,167 13,45 T 16,16 T 34,56 T 4,4 z") 229 | self.assertEqual(m.d(smooth=False), "M 4,4 L 20,20 L 25,25 L 6,3 Q 20,33 100,100 Q 180,167 13,45 Q -154,-77 16,16 Q 186,109 34,56 Q -118,3 4,4 z") 230 | self.assertEqual(m.d(smooth=True), "M 4,4 L 20,20 L 25,25 L 6,3 Q 20,33 100,100 T 13,45 T 16,16 T 34,56 T 4,4 z") 231 | self.assertEqual(m.d(), "M 4,4 L 20,20 L 25,25 L 6,3 Q 20,33 100,100 Q 180,167 13,45 T 16,16 T 34,56 T 4,4 z") 232 | 233 | def test_path_z_termination(self): 234 | m = Path("M 4,4 L 20,20 L 25,25 L 6,3 Q 20,33 Z") 235 | self.assertEqual(m.d(), "M 4,4 L 20,20 L 25,25 L 6,3 Q 20,33 4,4 Z") 236 | m = Path("M 4,4 L 20,20 L 25,25 L 6,3 Q 20,33 100,100 T Z") 237 | self.assertEqual(m.d(), "M 4,4 L 20,20 L 25,25 L 6,3 Q 20,33 100,100 T 4,4 Z") 238 | m = Path("M 4,4 L 20,20 L 25,25 L 6,3 Q Z") 239 | self.assertEqual(m.d(), "M 4,4 L 20,20 L 25,25 L 6,3 Q 4,4 4,4 Z") 240 | m = Path("M 4,4 L 20,20 L 25,25 L 6,3 C Z") 241 | self.assertEqual(m.d(), "M 4,4 L 20,20 L 25,25 L 6,3 C 4,4 4,4 4,4 Z") 242 | m = Path("M 4,4 L 20,20 L 25,25 L 6,3 Q 20,33 100,100 T z") 243 | self.assertEqual(m.d(), "M 4,4 L 20,20 L 25,25 L 6,3 Q 20,33 100,100 T 4,4 z") 244 | m = Path("m 0,0 1,1 A 5.01,5.01 180 0,0 z") 245 | self.assertEqual(m.d(), "m 0,0 l 1,1 A 5.01,5.01 180 0,0 0,0 z") 246 | m = Path("m0,0z") 247 | self.assertEqual(m.d(), "m 0,0 z") 248 | m = Path("M0,0Lz") 249 | self.assertEqual(m.d(), "M 0,0 L 0,0 z") 250 | 251 | m = Path("M 4,4 L 20,20 L 25,25 L 6,3").quad("Z").closed() 252 | self.assertEqual(m.d(), "M 4,4 L 20,20 L 25,25 L 6,3 Q 4,4 4,4 Z") 253 | m = Path("M 4,4 L 20,20 L 25,25 L 6,3").cubic("Z").closed() 254 | self.assertEqual(m.d(), "M 4,4 L 20,20 L 25,25 L 6,3 C 4,4 4,4 4,4 Z") 255 | 256 | def test_path_setitem_slice(self): 257 | m = Path("M0,0 1,1 z") 258 | m[1:] = 'L2,2z' 259 | self.assertEqual(m.d(), "M 0,0 L 2,2 z") 260 | self.assertTrue(m._is_valid()) 261 | del m[1] 262 | self.assertEqual(m.d(), "M 0,0 z") 263 | self.assertTrue(m._is_valid()) 264 | 265 | m = Path("M0,0z") 266 | m[:] = 'M1,1z' 267 | self.assertEqual(m.d(), "M 1,1 z") 268 | self.assertTrue(m._is_valid()) 269 | 270 | m = Path("M0,0z") 271 | del m[:] 272 | self.assertEqual(m, '') 273 | self.assertTrue(m._is_valid()) 274 | 275 | m = Path("M0,0z") 276 | m[0] = "M1,1" 277 | self.assertEqual(m.d(), "M 1,1 z") 278 | self.assertTrue(m._is_valid()) 279 | m[1] = "z" 280 | self.assertTrue(m._is_valid()) 281 | 282 | m = Path("M0,0z") 283 | del m[1] 284 | self.assertEqual(m.d(), "M 0,0") 285 | self.assertTrue(m._is_valid()) 286 | 287 | m = Path("M0,0 1,1 z") 288 | m[3:] = "M5,5z" 289 | self.assertEqual(m.d(), "M 0,0 L 1,1 z M 5,5 z") 290 | self.assertTrue(m._is_valid()) 291 | 292 | m = Path("M0,0 1,1 z") 293 | m[-1:] = "M5,5z" 294 | self.assertEqual(m.d(), "M 0,0 L 1,1 M 5,5 z") 295 | self.assertTrue(m._is_valid()) 296 | 297 | m = Path("M0,0 1,1 z") 298 | def m_assign(): 299 | m[-1] = 'M5,5z' 300 | self.assertRaises(ValueError, m_assign) 301 | 302 | def test_iterative_loop_building_line(self): 303 | path = Path() 304 | path.move(0) 305 | path.line(*([complex(1, 1)] * 2000)) 306 | 307 | def test_iterative_loop_building_vert(self): 308 | path = Path() 309 | path.move(0) 310 | path.vertical(*([5.0] * 2000)) 311 | 312 | def test_iterative_loop_building_horiz(self): 313 | path = Path() 314 | path.move(0) 315 | path.horizontal(*([5.0] * 2000)) 316 | 317 | def test_iterative_loop_building_quad(self): 318 | path = Path() 319 | path.move(0) 320 | path.quad(*([complex(1, 1), complex(1, 1)] * 1000)) 321 | 322 | def test_iterative_loop_building_cubic(self): 323 | path = Path() 324 | path.move(0) 325 | path.cubic(*([complex(1, 1), complex(1, 1), complex(1, 1)] * 1000)) 326 | 327 | def test_iterative_loop_building_arc(self): 328 | path = Path() 329 | path.move(0) 330 | q = [0, 0, 0, 0, 0, complex(1, 1)] * 2000 331 | path.arc(*q) 332 | 333 | -------------------------------------------------------------------------------- /test/test_path_dunder.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from svgelements import * 4 | 5 | 6 | class TestPath(unittest.TestCase): 7 | """Tests of dunder methods of the SVG Path element.""" 8 | 9 | def test_path_iadd_str(self): 10 | p1 = Path("M0,0") 11 | p1 += "z" 12 | self.assertEqual(p1, Path("M0,0z")) 13 | 14 | p1 = Path("M2,2z") 15 | p1 += "M1,1z" 16 | p1 += "M0,0z" 17 | subpaths = list(p1.as_subpaths()) 18 | self.assertEqual(str(subpaths[0]), Path("M2,2z")) 19 | self.assertEqual(str(subpaths[1]), Path("M1,1z")) 20 | self.assertEqual(str(subpaths[2]), Path("M0,0z")) 21 | 22 | def test_path_add_str(self): 23 | p1 = Path("M0,0") 24 | p2 = p1 + "z" 25 | p1 += "z" 26 | self.assertEqual(p1, p2) 27 | 28 | def test_path_radd_str(self): 29 | p1 = Path("M0,0z") 30 | p2 = "M1,1z" + p1 31 | subpaths = list(p2.as_subpaths()) 32 | self.assertEqual(str(subpaths[0]), str(Path("M1,1z"))) 33 | self.assertEqual(str(subpaths[1]), str(Path("M0,0z"))) 34 | 35 | def test_path_iadd_segment(self): 36 | p1 = Path("M0,0") 37 | p1 += Line((0, 0), (7, 7)) 38 | p1 += "z" 39 | self.assertEqual(p1, Path("M0,0 L7,7 z")) 40 | 41 | def test_path_add_segment(self): 42 | p1 = Path("M0,0") 43 | p2 = p1 + Line((0, 0), (7, 7)) 44 | p1 += "z" 45 | p2 += "z" 46 | self.assertEqual(p1, Path("M0,0 z")) 47 | self.assertEqual(p2, Path("M0,0 L7,7 z")) 48 | 49 | def test_path_radd_segment(self): 50 | p1 = Path("L7,7") 51 | p1 = Move((0, 0)) + p1 52 | p1 += "z" 53 | self.assertEqual(p1, Path("M0,0 L7,7 z")) 54 | 55 | def test_path_from_segment(self): 56 | p1 = Move(0) + Line(0, (7, 7)) + "z" 57 | self.assertEqual(p1, Path("M0,0 L7,7 z")) 58 | 59 | p1 = Move(0) + "L7,7" + "z" 60 | self.assertEqual(p1, Path("M0,0 L7,7 z")) 61 | 62 | p1 = Move(0) + "L7,7z" 63 | self.assertEqual(p1, Path("M0,0 L7,7 z")) 64 | 65 | def test_segment_mult_string(self): 66 | p1 = Move(0) * "translate(200,200)" 67 | self.assertEqual(p1, Move((200, 200))) 68 | 69 | p1 += "z" 70 | self.assertEqual(p1, Path("M200,200z")) 71 | 72 | def test_path_mult_string(self): 73 | p1 = Path(Move(0)) * "translate(200,200)" 74 | self.assertEqual(p1, "M200,200") 75 | 76 | p1 = Path(Move(0)).set('vector-effect', 'non-scaling-stroke') * "scale(0.5) translateX(200)" 77 | self.assertEqual(p1, "M100,0") 78 | self.assertNotEqual(p1, "M200,0") 79 | 80 | p1 = Path(Move(0)).set('vector-effect', 'non-scaling-stroke') * "translateX(200) scale(0.5)" 81 | self.assertEqual(p1, "M200,0") 82 | self.assertNotEqual(p1, "M100,0") 83 | 84 | def test_path_equals_string(self): 85 | self.assertEqual(Path("M55,55z"), "M55,55z") 86 | self.assertEqual(Path("M55 55z"), "M 55, 55z") 87 | self.assertTrue(Move(0) * "translate(55,55)" + "z" == "m 55, 55Z") 88 | self.assertTrue(Move(0) * "rotate(0.50turn,100,0)" + "z" == "M200,0z") 89 | self.assertFalse(Path(Move(0)) == "M0,0z") 90 | self.assertEqual(Path("M50,50 100,100 0,100 z").set('vector-effect', 'non-scaling-stroke') * "scale(0.1)", 91 | "M5,5 L10,10 0,10z") 92 | self.assertNotEqual(Path("M50,50 100,100 0,100 z") * "scale(0.11)", "M5,5 L10,10 0,10z") 93 | self.assertEqual( 94 | Path("M0,0 h10 v10 h-10 v-10z").set('vector-effect', 'non-scaling-stroke') * "scale(0.2) translate(-5,-5)", 95 | "M -1,-1, L1,-1, 1,1, -1,1, -1,-1 Z" 96 | ) 97 | 98 | def test_path_mult_matrix(self): 99 | p = Path("L20,20 40,40") * Matrix("Rotate(20)") 100 | self.assertEqual(p, "L11.953449549205,25.634255282232 23.906899098410,51.268510564463") 101 | p.reify() 102 | p += "L 100, 100" 103 | p += Close() 104 | self.assertEqual(p, Path("L11.953449549205,25.634255282232 23.906899098410,51.268510564463 100,100 z")) 105 | 106 | def test_partial_path(self): 107 | p1 = Path("M0,0") 108 | p2 = Path("L7,7") 109 | p3 = Path("Z") 110 | q = p1 + p2 + p3 111 | m = Path("M0,0 7,7z") 112 | self.assertEqual(q, m) 113 | 114 | -------------------------------------------------------------------------------- /test/test_path_segments.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from svgelements import * 4 | 5 | 6 | class TestElementLinear(unittest.TestCase): 7 | 8 | def test_linear_nearest(self): 9 | line = Line((0,0),(5,0)) 10 | r = line.closest_segment_point((17,0)) 11 | self.assertEqual(r, (5,0)) 12 | r = line.closest_segment_point((2, 2)) 13 | self.assertEqual(r, (2, 0)) 14 | 15 | 16 | class TestBoundingBox(unittest.TestCase): 17 | 18 | def test_linear_bbox(self): 19 | line = Line((0,0), (5,0)) 20 | r = line.bbox() 21 | self.assertEqual(r, (0, 0, 5, 0)) 22 | 23 | def test_qbezier_bbox(self): 24 | line = QuadraticBezier((0,0), (2,2), (5,0)) 25 | r = line.bbox() 26 | self.assertEqual(r, (0, 0, 5, 1)) 27 | 28 | def test_cbezier_bbox(self): 29 | line = CubicBezier((0,0), (2,2), (2,-2), (5,0)) 30 | r = line.bbox() 31 | for z in zip(r, (0.0, -0.5773502691896257, 5.0, 0.5773502691896257)): 32 | self.assertAlmostEqual(*z) 33 | 34 | def test_arc_bbox(self): 35 | line = Arc((0,0), (5,0), control=(2.5, 2.5)) 36 | r = line.bbox() 37 | for z in zip(r, (0.0, 0, 5.0, 2.5)): 38 | self.assertAlmostEqual(*z) 39 | 40 | def test_null_arc_bbox(self): 41 | self.assertEqual(Path("M0,0A0,0 0 0 0 0,0z").bbox(), (0,0,0,0)) 42 | 43 | 44 | class TestArcControlPoints(unittest.TestCase): 45 | 46 | def test_coincident_end_arc(self): 47 | """ 48 | Tests the creation of a control point with a coincident start and end. 49 | """ 50 | arc = Arc(start=(0,0), control=(50,0), end=(0,0)) 51 | self.assertAlmostEqual(arc.rx, 25) 52 | 53 | def test_linear_arc(self): 54 | """ 55 | Colinear Arcs should raise value errors. 56 | """ 57 | arc_vertical = Arc(start=(0, 0), control=(25, 0), end=(50, 0)) 58 | # print(arc_vertical) 59 | arc_horizontal = Arc(start=(0, 0), control=(0, 25), end=(0, 50)) 60 | # print(arc_horizontal) 61 | -------------------------------------------------------------------------------- /test/test_point.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from random import random 3 | 4 | from svgelements import * 5 | 6 | 7 | class TestElementPoint(unittest.TestCase): 8 | 9 | def test_point_init_string(self): 10 | p = Point("(0,24)") 11 | self.assertEqual(p, (0, 24)) 12 | self.assertEqual(p, 0 + 24j) 13 | self.assertEqual(p, [0, 24]) 14 | self.assertEqual(p, "(0,24)") 15 | 16 | def test_polar_angle(self): 17 | for i in range(1000): 18 | p = Point(random() * 50, random() * 50) 19 | a = random() * tau - tau / 2 20 | r = random() * 50 21 | m = Point.polar(p, a, r) 22 | self.assertAlmostEqual(Point.angle(p, m), a) 23 | 24 | def test_not_equal_unparsed(self): 25 | self.assertNotEqual(Point(0, 0), "string that doesn't parse to point") 26 | 27 | def test_dunder_iadd(self): 28 | p = Point(0) 29 | p += (1, 0) 30 | self.assertEqual(p, (1, 0)) 31 | p += Point(1, 1) 32 | self.assertEqual(p, (2, 1)) 33 | p += 1 + 2j 34 | self.assertEqual(p, (3, 3)) 35 | 36 | class c: 37 | def __init__(self): 38 | self.x = 1 39 | self.y = 1 40 | 41 | p += c() 42 | self.assertEqual(p, (4, 4)) 43 | p += Point("-4,-4") 44 | self.assertEqual(p, (0, 0)) 45 | p += 1 46 | self.assertEqual(p, (1, 0)) 47 | self.assertRaises(TypeError, 'p += "hello"') 48 | 49 | def test_dunder_isub(self): 50 | p = Point(0) 51 | p -= (1, 0) 52 | self.assertEqual(p, (-1, 0)) 53 | p -= Point(1, 1) 54 | self.assertEqual(p, (-2, -1)) 55 | p -= 1 + 2j 56 | self.assertEqual(p, (-3, -3)) 57 | 58 | class c: 59 | def __init__(self): 60 | self.x = 1 61 | self.y = 1 62 | 63 | p -= c() 64 | self.assertEqual(p, (-4, -4)) 65 | p -= Point("-4,-4") 66 | self.assertEqual(p, (0, 0)) 67 | p -= 1 68 | self.assertEqual(p, (-1, 0)) 69 | r = p - 1 70 | self.assertEqual(r, (-2, 0)) 71 | self.assertRaises(TypeError, 'p -= "hello"') 72 | 73 | def test_dunder_add(self): 74 | p = Point(0) 75 | p = p + (1, 0) 76 | self.assertEqual(p, (1, 0)) 77 | p = p + Point(1, 1) 78 | self.assertEqual(p, (2, 1)) 79 | p = p + 1 + 2j 80 | self.assertEqual(p, (3, 3)) 81 | 82 | class c: 83 | def __init__(self): 84 | self.x = 1 85 | self.y = 1 86 | 87 | p = p + c() 88 | self.assertEqual(p, (4, 4)) 89 | p = p + Point("-4,-4") 90 | self.assertEqual(p, (0, 0)) 91 | p = p + 1 92 | self.assertEqual(p, (1, 0)) 93 | self.assertRaises(TypeError, 'p = p + "hello"') 94 | 95 | def test_dunder_sub(self): 96 | p = Point(0) 97 | p = p - (1, 0) 98 | self.assertEqual(p, (-1, 0)) 99 | p = p - Point(1, 1) 100 | self.assertEqual(p, (-2, -1)) 101 | p = p - (1 + 2j) 102 | self.assertEqual(p, (-3, -3)) 103 | 104 | class c: 105 | def __init__(self): 106 | self.x = 1 107 | self.y = 1 108 | 109 | p = p - c() 110 | self.assertEqual(p, (-4, -4)) 111 | p = p - Point("-4,-4") 112 | self.assertEqual(p, (0, 0)) 113 | p = p - 1 114 | self.assertEqual(p, (-1, 0)) 115 | self.assertRaises(TypeError, 'p = p - "hello"') 116 | 117 | def test_dunder_rsub(self): 118 | p = Point(0) 119 | p = (1, 0) - p 120 | self.assertEqual(p, (1, 0)) 121 | p = Point(1, 1) - p 122 | self.assertEqual(p, (0, 1)) 123 | p = (1 + 2j) - p 124 | self.assertEqual(p, (1, 1)) 125 | 126 | class c: 127 | def __init__(self): 128 | self.x = 1 129 | self.y = 1 130 | 131 | p = c() - p 132 | self.assertEqual(p, (0, 0)) 133 | p = Point("-4,-4") - p 134 | self.assertEqual(p, (-4, -4)) 135 | p = 1 - p 136 | self.assertEqual(p, (5, 4)) 137 | self.assertRaises(TypeError, 'p = "hello" - p') 138 | 139 | def test_dunder_mult(self): 140 | """ 141 | For backwards compatibility multiplication of points works like multiplication of complex variables. 142 | 143 | :return: 144 | """ 145 | p = Point(2, 2) 146 | p *= (1, 0) 147 | self.assertEqual(p, (2, 2)) 148 | p *= Point(1, 1) 149 | self.assertEqual(p, (0, 4)) 150 | p *= 1 + 2j 151 | self.assertEqual(p, (-8, 4)) 152 | 153 | class c: 154 | def __init__(self): 155 | self.x = 1 156 | self.y = 1 157 | 158 | p *= c() 159 | self.assertEqual(p, (-12, -4)) 160 | p *= Point("-4,-4") 161 | self.assertEqual(p, (32, 64)) 162 | p *= 1 163 | self.assertEqual(p, (32, 64)) 164 | r = p * 1 165 | self.assertEqual(r, (32, 64)) 166 | r *= "scale(0.1)" 167 | self.assertEqual(r, (3.2, 6.4)) 168 | 169 | def test_dunder_transform(self): 170 | p = Point(4, 4) 171 | m = Matrix("scale(4)") 172 | p.matrix_transform(m) 173 | self.assertEqual(p, (16, 16)) 174 | 175 | def test_move_towards(self): 176 | p = Point(4, 4) 177 | p.move_towards((6, 6), 0.5) 178 | self.assertEqual(p, (5, 5)) 179 | 180 | def test_distance_to(self): 181 | p = Point(4, 4) 182 | m = p.distance_to((6, 6)) 183 | self.assertEqual(m, 2 * sqrt(2)) 184 | m = p.distance_to(4) 185 | self.assertEqual(m, 4) 186 | 187 | def test_angle_to(self): 188 | p = Point(0) 189 | a = p.angle_to((3, 3)) 190 | self.assertEqual(a, Angle.parse("45deg")) 191 | a = p.angle_to((0, 3)) 192 | self.assertEqual(a, Angle.parse("0.25turn")) 193 | a = p.angle_to((-3, 0)) 194 | self.assertEqual(a, Angle.parse("200grad")) 195 | 196 | def test_polar(self): 197 | p = Point(0) 198 | q = p.polar_to(Angle.parse("45deg"), 10) 199 | self.assertEqual(q, (sqrt(2)/2 * 10, sqrt(2)/2 * 10)) 200 | 201 | def test_reflected_across(self): 202 | p = Point(0) 203 | r = p.reflected_across((10,10)) 204 | self.assertEqual(r, (20,20)) -------------------------------------------------------------------------------- /test/test_quadratic_bezier.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from random import * 3 | 4 | from svgelements import * 5 | 6 | 7 | def get_random_quadratic_bezier(): 8 | return QuadraticBezier((random() * 50, random() * 50), (random() * 50, random() * 50), 9 | (random() * 50, random() * 50)) 10 | 11 | 12 | class TestElementQuadraticBezierPoint(unittest.TestCase): 13 | 14 | def test_quadratic_bezier_point_start_stop(self): 15 | import numpy as np 16 | for _ in range(1000): 17 | b = get_random_quadratic_bezier() 18 | self.assertEqual(b.start, b.point(0)) 19 | self.assertEqual(b.end, b.point(1)) 20 | self.assertTrue(np.all(np.array([list(b.start), list(b.end)]) 21 | == b.npoint([0, 1]))) 22 | 23 | def test_quadratic_bezier_point_implementations_match(self): 24 | import numpy as np 25 | for _ in range(1000): 26 | b = get_random_quadratic_bezier() 27 | 28 | pos = np.linspace(0, 1, 100) 29 | 30 | v1 = b.npoint(pos) 31 | v2 = [] 32 | for i in range(len(pos)): 33 | v2.append(b.point(pos[i])) 34 | 35 | for p, p1, p2 in zip(pos, v1, v2): 36 | self.assertEqual(b.point(p), Point(p1)) 37 | self.assertEqual(Point(p1), Point(p2)) 38 | -------------------------------------------------------------------------------- /test/test_repr.py: -------------------------------------------------------------------------------- 1 | import io 2 | import unittest 3 | 4 | from svgelements import * 5 | 6 | 7 | class TestElementsRepr(unittest.TestCase): 8 | """Tests the functionality of the repr for elements.""" 9 | 10 | def test_repr_length(self): 11 | obj = Length("10cm") 12 | repr_obj = repr(obj) 13 | obj2 = eval(repr_obj) 14 | self.assertTrue(obj == obj2) 15 | self.assertFalse(obj != obj2) 16 | 17 | def test_repr_color(self): 18 | obj = Color("red") 19 | repr_obj = repr(obj) 20 | obj2 = eval(repr_obj) 21 | self.assertTrue(obj == obj2) 22 | self.assertFalse(obj != obj2) 23 | 24 | def test_repr_point(self): 25 | obj = Point("20.3,3.1615926535") 26 | repr_obj = repr(obj) 27 | obj2 = eval(repr_obj) 28 | self.assertTrue(obj == obj2) 29 | self.assertFalse(obj != obj2) 30 | 31 | def test_repr_angle(self): 32 | obj = Angle.parse("1.1turn") 33 | repr_obj = repr(obj) 34 | obj2 = eval(repr_obj) 35 | self.assertAlmostEqual(obj, obj2) 36 | 37 | def test_repr_matrix(self): 38 | obj = Matrix("rotate(20)") 39 | repr_obj = repr(obj) 40 | obj2 = eval(repr_obj) 41 | self.assertTrue(obj == obj2) 42 | self.assertFalse(obj != obj2) 43 | 44 | def test_repr_viewbox(self): 45 | obj = Viewbox("0 0 100 60") 46 | repr_obj = repr(obj) 47 | obj2 = eval(repr_obj) 48 | self.assertTrue(obj == obj2) 49 | self.assertFalse(obj != obj2) 50 | 51 | def test_repr_move(self): 52 | obj = Move(0.1, 50) 53 | repr_obj = repr(obj) 54 | obj2 = eval(repr_obj) 55 | self.assertTrue(obj == obj2) 56 | self.assertFalse(obj != obj2) 57 | 58 | def test_repr_close(self): 59 | obj = Close(0.1, 50) 60 | repr_obj = repr(obj) 61 | obj2 = eval(repr_obj) 62 | self.assertTrue(obj == obj2) 63 | self.assertFalse(obj != obj2) 64 | 65 | def test_repr_line(self): 66 | obj = Line(start=(0.2, 0.99), end=(0.1, 22.9996)) 67 | repr_obj = repr(obj) 68 | obj2 = eval(repr_obj) 69 | self.assertTrue(obj == obj2) 70 | self.assertFalse(obj != obj2) 71 | 72 | obj = Line(end=(0.1, 22.9996)) 73 | repr_obj = repr(obj) 74 | obj2 = eval(repr_obj) 75 | self.assertTrue(obj == obj2) 76 | self.assertFalse(obj != obj2) 77 | 78 | def test_repr_qbez(self): 79 | obj = QuadraticBezier(start=(0.2, 0.99), control=(-3,-3), end=(0.1, 22.9996)) 80 | repr_obj = repr(obj) 81 | obj2 = eval(repr_obj) 82 | self.assertTrue(obj == obj2) 83 | self.assertFalse(obj != obj2) 84 | 85 | def test_repr_cbez(self): 86 | obj = CubicBezier(start=(0.2, 0.99), control1=(-3, -3), control2=(-4, -4), end=(0.1, 22.9996)) 87 | repr_obj = repr(obj) 88 | obj2 = eval(repr_obj) 89 | self.assertTrue(obj == obj2) 90 | self.assertFalse(obj != obj2) 91 | 92 | def test_repr_arc(self): 93 | obj = Arc(start=(0,0), end=(0,100), control=(50,50)) 94 | repr_obj = repr(obj) 95 | obj2 = eval(repr_obj) 96 | self.assertTrue(obj == obj2) 97 | self.assertFalse(obj != obj2) 98 | 99 | def test_repr_path(self): 100 | obj = Path("M0,0Z") 101 | repr_obj = repr(obj) 102 | obj2 = eval(repr_obj) 103 | self.assertTrue(obj == obj2) 104 | self.assertFalse(obj != obj2) 105 | 106 | obj = Path("M0,0L100,100Z") 107 | repr_obj = repr(obj) 108 | obj2 = eval(repr_obj) 109 | self.assertTrue(obj == obj2) 110 | self.assertFalse(obj != obj2) 111 | 112 | obj = Path("M0,0L100,100Z", transform="scale(4)") 113 | repr_obj = repr(obj) 114 | obj2 = eval(repr_obj) 115 | self.assertTrue(obj == obj2) 116 | self.assertFalse(obj != obj2) 117 | 118 | def test_repr_rect(self): 119 | obj = Rect(x=100, y=100, width=500, height=500) 120 | repr_obj = repr(obj) 121 | obj2 = eval(repr_obj) 122 | self.assertTrue(obj == obj2) 123 | self.assertFalse(obj != obj2) 124 | 125 | obj = Rect(x=100, y=100, width=500, height=500, transform="scale(2)", stroke="red", fill="blue") 126 | repr_obj = repr(obj) 127 | obj2 = eval(repr_obj) 128 | self.assertTrue(obj == obj2) 129 | self.assertFalse(obj != obj2) 130 | 131 | def test_repr_ellipse(self): 132 | obj = Ellipse(cx=100, cy=100, rx=500, ry=500) 133 | repr_obj = repr(obj) 134 | obj2 = eval(repr_obj) 135 | self.assertTrue(obj == obj2) 136 | self.assertFalse(obj != obj2) 137 | 138 | obj = Ellipse(cx=100, cy=100, rx=500, ry=500, transform="scale(2)", stroke="red", fill="blue") 139 | repr_obj = repr(obj) 140 | obj2 = eval(repr_obj) 141 | self.assertTrue(obj == obj2) 142 | self.assertFalse(obj != obj2) 143 | 144 | def test_repr_circle(self): 145 | obj = Circle(cx=100, cy=100, r=500) 146 | repr_obj = repr(obj) 147 | obj2 = eval(repr_obj) 148 | self.assertTrue(obj == obj2) 149 | self.assertFalse(obj != obj2) 150 | 151 | obj = Circle(cx=100, cy=100, r=500, transform="scale(2)", stroke="red", fill="blue") 152 | repr_obj = repr(obj) 153 | obj2 = eval(repr_obj) 154 | self.assertTrue(obj == obj2) 155 | self.assertFalse(obj != obj2) 156 | 157 | def test_repr_simpleline(self): 158 | obj = SimpleLine(start=(0,0), end=(100,100)) 159 | repr_obj = repr(obj) 160 | obj2 = eval(repr_obj) 161 | self.assertTrue(obj == obj2) 162 | self.assertFalse(obj != obj2) 163 | 164 | obj = SimpleLine(start=(0, 0), end=(100, 100), transform="scale(2)", stroke="red", fill="blue") 165 | repr_obj = repr(obj) 166 | obj2 = eval(repr_obj) 167 | self.assertTrue(obj == obj2) 168 | self.assertFalse(obj != obj2) 169 | 170 | def test_repr_polyline(self): 171 | obj = Polyline("0,0 7,7 10,10 0 20") 172 | repr_obj = repr(obj) 173 | obj2 = eval(repr_obj) 174 | self.assertTrue(obj == obj2) 175 | self.assertFalse(obj != obj2) 176 | 177 | obj = Polyline("0,0 7,7 10,10 0 20", transform="scale(2)", stroke="red", fill="blue") 178 | repr_obj = repr(obj) 179 | obj2 = eval(repr_obj) 180 | self.assertTrue(obj == obj2) 181 | self.assertFalse(obj != obj2) 182 | 183 | def test_repr_polygon(self): 184 | obj = Polygon("0,0 7,7 10,10 0 20") 185 | repr_obj = repr(obj) 186 | obj2 = eval(repr_obj) 187 | self.assertTrue(obj == obj2) 188 | self.assertFalse(obj != obj2) 189 | 190 | obj = Polygon("0,0 7,7 10,10 0 20", transform="scale(2)", stroke="red", fill="blue") 191 | repr_obj = repr(obj) 192 | obj2 = eval(repr_obj) 193 | self.assertTrue(obj == obj2) 194 | self.assertFalse(obj != obj2) 195 | 196 | def test_repr_group(self): 197 | obj = Group() 198 | repr_obj = repr(obj) 199 | obj2 = eval(repr_obj) 200 | self.assertTrue(obj == obj2) 201 | self.assertFalse(obj != obj2) 202 | 203 | obj = Group(transform="scale(2)", stroke="red", fill="blue") 204 | repr_obj = repr(obj) 205 | obj2 = eval(repr_obj) 206 | self.assertTrue(obj == obj2) 207 | self.assertFalse(obj != obj2) 208 | 209 | def test_repr_clippath(self): 210 | obj = ClipPath() 211 | repr_obj = repr(obj) 212 | obj2 = eval(repr_obj) 213 | self.assertTrue(obj == obj2) 214 | self.assertFalse(obj != obj2) 215 | 216 | def test_repr_pattern(self): 217 | obj = Pattern() 218 | repr_obj = repr(obj) 219 | obj2 = eval(repr_obj) 220 | self.assertTrue(obj == obj2) 221 | self.assertFalse(obj != obj2) 222 | 223 | def test_repr_text(self): 224 | obj = SVGText(x=0, y=0, text="Hello") 225 | repr_obj = repr(obj) 226 | obj2 = eval(repr_obj) 227 | self.assertTrue(obj == obj2) 228 | self.assertFalse(obj != obj2) 229 | 230 | obj = SVGText(x=0, y=0, text="Hello", transform="scale(2)", stroke="red", fill="blue") 231 | repr_obj = repr(obj) 232 | obj2 = eval(repr_obj) 233 | self.assertTrue(obj == obj2) 234 | self.assertFalse(obj != obj2) 235 | 236 | def test_repr_image(self): 237 | obj = SVGImage(href="test.png", transform="scale(2)") 238 | repr_obj = repr(obj) 239 | obj2 = eval(repr_obj) 240 | self.assertTrue(obj == obj2) 241 | self.assertFalse(obj != obj2) 242 | 243 | def test_repr_desc(self): 244 | obj = Desc("Describes Object") 245 | repr_obj = repr(obj) 246 | obj2 = eval(repr_obj) 247 | self.assertTrue(obj == obj2) 248 | self.assertFalse(obj != obj2) 249 | 250 | def test_repr_title(self): 251 | obj = Title(title="SVG Description") 252 | repr_obj = repr(obj) 253 | obj2 = eval(repr_obj) 254 | self.assertTrue(obj == obj2) 255 | self.assertFalse(obj != obj2) 256 | 257 | -------------------------------------------------------------------------------- /test/test_stroke_width.py: -------------------------------------------------------------------------------- 1 | import io 2 | import unittest 3 | 4 | from svgelements import * 5 | 6 | 7 | class TestStrokeWidth(unittest.TestCase): 8 | """Tests the functionality of the Stroke-Width values""" 9 | 10 | def test_viewport_scaling_stroke(self): 11 | """ 12 | See Issue #199 13 | 14 | The stroke width of both objects here is the same and there is no scaling on the path itself. However, the 15 | viewport is scaled and applies that scaling to the overall result. Given that these two objects should have 16 | if drawn in a static fashion the same size stroke. They should not scale based on the viewport since that 17 | governs a sort of zoom and pan functionality which can be taken as equal to stroke but not in regard to 18 | vector-effect="non-scaling-stroke". 19 | """ 20 | q = io.StringIO( 21 | """ 25 | 28 | 30 | 31 | """ 32 | ) 33 | m = SVG.parse(q) 34 | q = list(m.elements()) 35 | self.assertAlmostEqual(q[1].stroke_width, q[2].stroke_width) 36 | -------------------------------------------------------------------------------- /test/test_text.py: -------------------------------------------------------------------------------- 1 | import io 2 | import time 3 | import unittest 4 | 5 | from svgelements import * 6 | 7 | 8 | class TestElementText(unittest.TestCase): 9 | def test_issue_157(self): 10 | q = io.StringIO( 11 | """ 12 | 13 | 14 | Test 20 | 21 | 22 | """ 23 | ) 24 | m = SVG.parse(q) 25 | q = list(m.elements()) 26 | self.assertIsNotNone(q[1].id) # Group 27 | self.assertIsNotNone(q[2].id) # Text 28 | self.assertIsNotNone(q[3].id) # TSpan 29 | 30 | def test_shorthand_fontproperty_1(self): 31 | font = "12pt/14pt sans-serif" 32 | 33 | q = io.StringIO( 34 | f""" 35 | 36 | Shorthand 39 | 40 | """ 41 | ) 42 | m = SVG.parse(q) 43 | text_object = list(m.elements())[1] 44 | self.assertEqual(text_object.font_style, "normal") 45 | self.assertEqual(text_object.font_variant, 'normal') 46 | self.assertEqual(text_object.font_weight, "normal") # Normal 47 | self.assertEqual(text_object.font_stretch, "normal") 48 | self.assertEqual(text_object.font_size, Length("12pt").value()) 49 | self.assertEqual(text_object.line_height, Length("14pt").value()) 50 | self.assertEqual(text_object.font_family, "sans-serif") 51 | self.assertEqual(text_object.font_list, ["sans-serif"]) 52 | 53 | def test_shorthand_fontproperty_2(self): 54 | font = "80% sans-serif" 55 | 56 | q = io.StringIO( 57 | f""" 58 | 59 | Shorthand 62 | 63 | """ 64 | ) 65 | m = SVG.parse(q) 66 | text_object = list(m.elements())[1] 67 | self.assertEqual(text_object.font_style, 'normal') 68 | self.assertEqual(text_object.font_variant, 'normal') 69 | self.assertEqual(text_object.font_weight, "normal") # Normal 70 | self.assertEqual(text_object.font_stretch, "normal") 71 | self.assertEqual(text_object.font_size, "80%") 72 | self.assertEqual(text_object.line_height, 16.0) 73 | self.assertEqual(text_object.font_family, "sans-serif") 74 | 75 | def test_shorthand_fontproperty_3(self): 76 | font = 'x-large/110% "new century schoolbook", serif' 77 | 78 | q = io.StringIO( 79 | f""" 80 | 81 | Shorthand 84 | 85 | """ 86 | ) 87 | m = SVG.parse(q) 88 | text_object = list(m.elements())[1] 89 | self.assertEqual(text_object.font_style, "normal") 90 | self.assertEqual(text_object.font_variant, 'normal') 91 | self.assertEqual(text_object.font_weight, "normal") # Normal 92 | self.assertEqual(text_object.font_stretch, "normal") 93 | self.assertEqual(text_object.font_size, "x-large") 94 | self.assertEqual(text_object.line_height, "110%") 95 | self.assertEqual(text_object.font_family, '"new century schoolbook", serif') 96 | self.assertEqual(text_object.font_list, ["new century schoolbook", "serif"]) 97 | 98 | def test_shorthand_fontproperty_4(self): 99 | font = "bold italic large Palatino, serif" 100 | 101 | q = io.StringIO( 102 | f""" 103 | 104 | Shorthand 107 | 108 | """ 109 | ) 110 | m = SVG.parse(q) 111 | text_object = list(m.elements())[1] 112 | 113 | self.assertEqual(text_object.font_style, "italic") 114 | self.assertEqual(text_object.font_variant, 'normal') 115 | self.assertEqual(text_object.font_weight, "bold") # Normal 116 | self.assertEqual(text_object.font_stretch, "normal") 117 | self.assertEqual(text_object.font_size, "large") 118 | self.assertEqual(text_object.line_height, 16.0) 119 | self.assertEqual(text_object.font_family, 'Palatino, serif') 120 | self.assertEqual(text_object.font_list, ["Palatino", "serif"]) 121 | 122 | def test_shorthand_fontproperty_5(self): 123 | font = "normal small-caps 120%/120% fantasy" 124 | 125 | q = io.StringIO( 126 | f""" 127 | 128 | Shorthand 131 | 132 | """ 133 | ) 134 | m = SVG.parse(q) 135 | text_object = list(m.elements())[1] 136 | self.assertEqual(text_object.font_style, "normal") 137 | self.assertEqual(text_object.font_variant, 'small-caps') 138 | self.assertEqual(text_object.font_weight, "normal") # Normal 139 | self.assertEqual(text_object.font_stretch, "normal") 140 | self.assertEqual(text_object.font_size, "120%") 141 | self.assertEqual(text_object.line_height, "120%") 142 | self.assertEqual(text_object.font_family, 'fantasy') 143 | 144 | def test_shorthand_fontproperty_6(self): 145 | font = 'condensed oblique 12pt "Helvetica Neue", serif;' 146 | 147 | q = io.StringIO( 148 | f""" 149 | 150 | Shorthand 153 | 154 | """ 155 | ) 156 | m = SVG.parse(q) 157 | text_object = list(m.elements())[1] 158 | self.assertEqual(text_object.font_style, 'oblique') 159 | self.assertEqual(text_object.font_variant, 'normal') 160 | self.assertEqual(text_object.font_weight, "normal") 161 | self.assertEqual(text_object.font_stretch, "condensed") 162 | self.assertEqual(text_object.font_size, Length("12pt").value()) 163 | self.assertEqual(text_object.line_height, Length("12pt").value()) 164 | self.assertEqual(text_object.font_family, '"Helvetica Neue", serif') 165 | self.assertEqual(text_object.font_list, ["Helvetica Neue", "serif"]) 166 | 167 | def test_shorthand_fontproperty_7(self): 168 | font = """condensed oblique 12pt "Helvetica", 'Veranda', serif;""" 169 | 170 | q = io.StringIO( 171 | f""" 172 | 173 | Shorthand 176 | 177 | """ 178 | ) 179 | m = SVG.parse(q) 180 | text_object = list(m.elements())[1] 181 | self.assertEqual(text_object.font_style, 'oblique') 182 | self.assertEqual(text_object.font_variant, 'normal') 183 | self.assertEqual(text_object.font_weight, "normal") 184 | self.assertEqual(text_object.font_stretch, "condensed") 185 | self.assertEqual(text_object.font_size, Length("12pt").value()) 186 | self.assertEqual(text_object.line_height, Length("12pt").value()) 187 | self.assertEqual(text_object.font_family, '''"Helvetica", 'Veranda', serif''') 188 | self.assertEqual(text_object.font_list, ["Helvetica", "Veranda", "serif"]) 189 | 190 | 191 | def test_issue_154(self): 192 | """ 193 | reDoS check. If suffering from Issue 154 this takes about 20 seconds. Normally 0.01s. 194 | """ 195 | font = "normal " * 12 196 | q = io.StringIO( 197 | f""" 198 | 199 | reDoS 202 | 203 | """ 204 | ) 205 | t = time.time() 206 | m = SVG.parse(q) 207 | t2 = time.time() 208 | self.assertTrue((time.time() - t) < 1000) 209 | -------------------------------------------------------------------------------- /test/test_use.py: -------------------------------------------------------------------------------- 1 | import io 2 | import unittest 3 | 4 | from svgelements import * 5 | 6 | 7 | class TestElementUse(unittest.TestCase): 8 | 9 | def test_use_bbox_method(self): 10 | q = io.StringIO(u''' 11 | 13 | 14 | 15 | 16 | 17 | 18 | ''') 19 | svg = SVG.parse(q) 20 | use = list(svg.select(lambda e: isinstance(e, Use))) 21 | self.assertEqual(2, len(use)) 22 | self.assertEqual((0.0, 20.0, (0.0 + 50.0), (20.0 + 50.0)), use[0].bbox()) 23 | self.assertEqual((20.0 + 0.0, 20.0 + 20.0, (20.0 + 50.0), (20.0 + 20.0 + 50.0)), use[1].bbox()) 24 | 25 | def test_issue_156(self): 26 | q1 = io.StringIO(u''' 27 | 38 | 39 | 43 | 50 | 58 | 59 | ''') 60 | layout = SVG.parse( 61 | source=q1, 62 | reify=True, 63 | ppi=DEFAULT_PPI, 64 | width=1, 65 | height=1, 66 | color="black", 67 | transform=None, 68 | context=None 69 | ) 70 | 71 | template1 = layout.get_element_by_id("rect2728") 72 | rect_before_use = template1.bbox() 73 | 74 | q2 = io.StringIO(u''' 75 | 86 | 87 | 91 | 99 | 106 | 107 | ''') 108 | layout = SVG.parse( 109 | source=q2, 110 | reify=True, 111 | ppi=DEFAULT_PPI, 112 | width=1, 113 | height=1, 114 | color="black", 115 | transform=None, 116 | context=None 117 | ) 118 | 119 | template2 = layout.get_element_by_id("rect2728") 120 | use_before_rect = template2.bbox() 121 | self.assertEqual(use_before_rect, rect_before_use) 122 | 123 | def test_issue_192(self): 124 | """ 125 | Rendered wrongly because the matrix from the group as well as the viewport and even the original parse routine 126 | is utilized twice. The use references the use object rather than the use xml. And should only have the render 127 | elements of where it is inserted and not of where it appeared in the tree. A Use is effectively copying and 128 | pasting that node into that place and only overriding x, y, length, and width. 129 | """ 130 | 131 | q1 = io.StringIO(u''' 132 | 134 | 135 | 136 | 137 | 138 | 139 | 185 | 186 | 187 | 188 | 189 | 190 | ''') 191 | layout = SVG.parse( 192 | source=q1, 193 | reify=False, 194 | ppi=DEFAULT_PPI, 195 | width=1, 196 | height=1, 197 | color="black", 198 | transform=None, 199 | context=None 200 | ) 201 | 202 | path1 = layout.get_element_by_id("use1")[0] 203 | path2 = Path('''M 4448 1489 204 | C 4454 1508 4474 1554 4474 1579 205 | C 4474 1611 4448 1643 4410 1643 206 | C 4384 1643 4371 1637 4352 1617 207 | C 4339 1611 4339 1598 4282 1469 208 | C 3904 570 3629 185 2605 185 209 | L 1670 185 210 | C 1581 185 1568 185 1530 192 211 | C 1459 198 1453 211 1453 262 212 | C 1453 307 1466 345 1478 403 213 | L 1920 2176 214 | L 2554 2176 215 | C 3053 2176 3091 2066 3091 1874 216 | C 3091 1810 3091 1752 3046 1560 217 | C 3034 1534 3027 1508 3027 1489 218 | C 3027 1444 3059 1425 3098 1425 219 | C 3155 1425 3162 1469 3187 1559 220 | L 3552 3042 221 | C 3552 3073 3526 3105 3488 3105 222 | C 3430 3105 3424 3080 3398 2990 223 | C 3270 2501 3142 2361 2573 2361 224 | L 1965 2361 225 | L 2362 3930 226 | C 2419 4154 2432 4154 2694 4154 227 | L 3610 4154 228 | C 4397 4154 4557 3943 4557 3458 229 | C 4557 3451 4557 3273 4531 3062 230 | C 4525 3036 4518 2998 4518 2985 231 | C 4518 2934 4550 2915 4589 2915 232 | C 4634 2915 4659 2941 4672 3055 233 | L 4806 4174 234 | C 4806 4195 4819 4263 4819 4277 235 | C 4819 4352 4762 4352 4646 4352 236 | L 1523 4352 237 | C 1402 4352 1338 4352 1338 4229 238 | C 1338 4154 1382 4154 1491 4154 239 | C 1888 4154 1888 4114 1888 4052 240 | C 1888 4020 1882 3994 1862 3924 241 | L 998 473 242 | C 941 249 928 185 480 185 243 | C 358 185 294 185 294 70 244 | C 294 0 333 0 461 0 245 | L 3674 0 246 | C 3814 0 3821 6 3866 110 247 | L 4448 1489 248 | z''', transform="translate(0, 0) scale(1.333333333333, 1.333333333333) translate(0 21.294961) scale(0.3 -0.3) scale(0.996264) scale(0.015625)") 249 | self.assertEqual(path1.values["transform"], path2.values["transform"]) 250 | self.assertEqual(path1.transform, path2.transform) 251 | self.assertEqual(path1, path2) 252 | 253 | def test_issue_170(self): 254 | """ 255 | Rendered wrongly since the x and y values do not get applied correctly to the use in question. 256 | """ 257 | 258 | q1 = io.StringIO(u''' 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | ''') 274 | layout = SVG.parse( 275 | source=q1, 276 | reify=False, 277 | ppi=DEFAULT_PPI, 278 | width=1, 279 | height=1, 280 | color="black", 281 | transform=None, 282 | context=None 283 | ) 284 | elements = list(layout.elements(lambda e: isinstance(e, Path))) 285 | for i in range(2, len(elements)): 286 | self.assertEqual(elements[i-1].d(transformed=False), elements[i].d(transformed=False)) 287 | self.assertNotEqual(elements[i - 1].transform, elements[i].transform) 288 | self.assertNotEqual(elements[i-1], elements[i]) 289 | -------------------------------------------------------------------------------- /test/test_viewbox.py: -------------------------------------------------------------------------------- 1 | import io 2 | import unittest 3 | 4 | from svgelements import * 5 | 6 | 7 | class TestElementViewbox(unittest.TestCase): 8 | 9 | def test_viewbox_creation(self): 10 | """Test various ways of creating a viewbox are equal.""" 11 | v1 = Viewbox('0 0 100 100', 'xMid') 12 | v2 = Viewbox(viewBox="0 0 100 100", preserve_aspect_ratio="xMid") 13 | v3 = Viewbox(x=0, y=0, width=100, height=100, preserveAspectRatio="xMid") 14 | v4 = Viewbox(v1) 15 | v5 = Viewbox({"x":0, "y":0, "width":100, "height":100, "preserveAspectRatio":"xMid"}) 16 | self.assertEqual(v1, v2) 17 | self.assertEqual(v1, v3) 18 | self.assertEqual(v1, v4) 19 | self.assertEqual(v1, v5) 20 | self.assertEqual(v2, v3) 21 | self.assertEqual(v2, v4) 22 | self.assertEqual(v2, v5) 23 | self.assertEqual(v3, v4) 24 | self.assertEqual(v3, v5) 25 | self.assertEqual(v4, v5) 26 | 27 | def test_viewbox_incomplete_none(self): 28 | """ 29 | Test viewboxes based on incomplete information. 30 | """ 31 | q = io.StringIO(u''' 32 | ''') 33 | m = SVG.parse(q) 34 | self.assertEqual(m.viewbox_transform, '') 35 | self.assertEqual(m.width, 1000) 36 | self.assertEqual(m.height, 1000) 37 | 38 | q = io.StringIO(u''' 39 | ''') 40 | m = SVG.parse(q, width=500, height=500) 41 | self.assertEqual(m.viewbox_transform, '') 42 | self.assertEqual(m.width, 500) 43 | self.assertEqual(m.height, 500) 44 | 45 | def test_viewbox_incomplete_height(self): 46 | """ 47 | Test viewboxes based on incomplete information, only height. 48 | """ 49 | q = io.StringIO(u''' 50 | ''') 51 | m = SVG.parse(q) 52 | self.assertEqual(m.viewbox_transform, '') 53 | self.assertEqual(m.width, 1000) 54 | self.assertEqual(m.height, 200) 55 | q = io.StringIO(u''' 56 | ''') 57 | m = SVG.parse(q, width=500, height=500) 58 | self.assertEqual(m.viewbox_transform, '') 59 | self.assertEqual(m.width, 500) 60 | self.assertEqual(m.height, 200) 61 | 62 | def test_viewbox_incomplete_width(self): 63 | """ 64 | Test viewboxes based on incomplete information, only width. 65 | """ 66 | q = io.StringIO(u''' 67 | ''') 68 | m = SVG.parse(q) 69 | self.assertEqual(m.viewbox_transform, '') 70 | self.assertEqual(m.width, 200) 71 | self.assertEqual(m.height, 1000) 72 | q = io.StringIO(u''' 73 | ''') 74 | m = SVG.parse(q, width=500, height=500) 75 | self.assertEqual(m.viewbox_transform, '') 76 | self.assertEqual(m.width, 200) 77 | self.assertEqual(m.height, 500) 78 | 79 | def test_viewbox_incomplete_dims(self): 80 | """ 81 | Test viewboxes based on incomplete information, only dims. 82 | """ 83 | q = io.StringIO(u''' 84 | ''') 85 | m = SVG.parse(q) 86 | self.assertEqual(m.viewbox_transform, '') 87 | self.assertEqual(m.width, 200) 88 | self.assertEqual(m.height, 200) 89 | q = io.StringIO(u''' 90 | ''') 91 | m = SVG.parse(q, width=500, height=500) 92 | self.assertEqual(m.viewbox_transform, '') 93 | self.assertEqual(m.width, 200) 94 | self.assertEqual(m.height, 200) 95 | 96 | def test_viewbox_incomplete_viewbox(self): 97 | """ 98 | Test viewboxes based on incomplete information, only viewbox. 99 | """ 100 | q = io.StringIO(u''' 101 | ''') 102 | m = SVG.parse(q) 103 | self.assertEqual(Matrix(m.viewbox_transform), 'scale(1)') 104 | self.assertEqual(m.width, 100) 105 | self.assertEqual(m.height, 100) 106 | q = io.StringIO(u''' 107 | ''') 108 | m = SVG.parse(q, width=500, height=500) 109 | self.assertEqual(Matrix(m.viewbox_transform), 'scale(5)') 110 | self.assertEqual(m.width, 500) 111 | self.assertEqual(m.height, 500) 112 | 113 | def test_viewbox_incomplete_height_viewbox(self): 114 | """ 115 | Test viewboxes based on incomplete information, only height and viewbox. 116 | """ 117 | q = io.StringIO(u''' 118 | ''') 119 | m = SVG.parse(q) 120 | self.assertEqual(Matrix(m.viewbox_transform), '') 121 | self.assertEqual(m.width, 100) 122 | self.assertEqual(m.height, 100) 123 | q = io.StringIO(u''' 124 | ''') 125 | m = SVG.parse(q, width=500, height=500) 126 | self.assertEqual(Matrix(m.viewbox_transform), 'scale(1) translateX(200)') 127 | self.assertEqual(m.width, 500) 128 | self.assertEqual(m.height, 100) 129 | 130 | q = io.StringIO(u''' 131 | ''') 132 | m = SVG.parse(q, width=500, height=500) 133 | self.assertEqual(Matrix(m.viewbox_transform), 'scale(2)') 134 | self.assertEqual(m.width, 200) 135 | self.assertEqual(m.height, 200) 136 | 137 | def test_viewbox_aspect_ratio_xMinMax(self): 138 | q = io.StringIO(u''' 139 | ''') 140 | m = SVG.parse(q) 141 | self.assertEqual(Matrix(m.viewbox_transform), 'translateX(100)') 142 | self.assertEqual(m.width, 300) 143 | self.assertEqual(m.height, 100) 144 | 145 | q = io.StringIO(u''' 146 | ''') 147 | m = SVG.parse(q) 148 | self.assertEqual(Matrix(m.viewbox_transform), 'translateX(0)') 149 | self.assertEqual(m.width, 300) 150 | self.assertEqual(m.height, 100) 151 | 152 | q = io.StringIO(u''' 153 | ''') 154 | m = SVG.parse(q) 155 | self.assertEqual(Matrix(m.viewbox_transform), 'translateX(200)') 156 | self.assertEqual(m.width, 300) 157 | self.assertEqual(m.height, 100) 158 | 159 | def test_viewbox_aspect_ratio_xMinMaxSlice(self): 160 | q = io.StringIO(u''' 161 | ''') 162 | m = SVG.parse(q) 163 | self.assertEqual(Matrix(m.viewbox_transform), 'scale(3)') 164 | self.assertEqual(m.width, 300) 165 | self.assertEqual(m.height, 100) 166 | 167 | q = io.StringIO(u''' 168 | ''') 169 | m = SVG.parse(q) 170 | self.assertEqual(Matrix(m.viewbox_transform), 'scale(3)') 171 | self.assertEqual(m.width, 300) 172 | self.assertEqual(m.height, 100) 173 | 174 | q = io.StringIO(u''' 175 | ''') 176 | m = SVG.parse(q) 177 | self.assertEqual(Matrix(m.viewbox_transform), 'scale(3)') 178 | self.assertEqual(m.width, 300) 179 | self.assertEqual(m.height, 100) 180 | 181 | def test_viewbox_aspect_ratio_yMinMax(self): 182 | q = io.StringIO(u''' 183 | ''') 184 | m = SVG.parse(q) 185 | self.assertEqual(Matrix(m.viewbox_transform), 'translateY(100)') 186 | self.assertEqual(m.width, 100) 187 | self.assertEqual(m.height, 300) 188 | 189 | q = io.StringIO(u''' 190 | ''') 191 | m = SVG.parse(q) 192 | self.assertEqual(Matrix(m.viewbox_transform), 'translateY(0)') 193 | self.assertEqual(m.width, 100) 194 | self.assertEqual(m.height, 300) 195 | 196 | q = io.StringIO(u''' 197 | ''') 198 | m = SVG.parse(q) 199 | self.assertEqual(Matrix(m.viewbox_transform), 'translateY(200)') 200 | self.assertEqual(m.width, 100) 201 | self.assertEqual(m.height, 300) 202 | 203 | def test_viewbox_aspect_ratio_yMinMaxSlice(self): 204 | q = io.StringIO(u''' 205 | ''') 206 | m = SVG.parse(q) 207 | self.assertEqual(Matrix(m.viewbox_transform), 'scale(3)') 208 | self.assertEqual(m.width, 100) 209 | self.assertEqual(m.height, 300) 210 | 211 | q = io.StringIO(u''' 212 | ''') 213 | m = SVG.parse(q) 214 | self.assertEqual(Matrix(m.viewbox_transform), 'scale(3)') 215 | self.assertEqual(m.width, 100) 216 | self.assertEqual(m.height, 300) 217 | 218 | q = io.StringIO(u''' 219 | ''') 220 | m = SVG.parse(q) 221 | self.assertEqual(Matrix(m.viewbox_transform), 'scale(3)') 222 | self.assertEqual(m.width, 100) 223 | self.assertEqual(m.height, 300) 224 | 225 | def test_viewbox_simple(self): 226 | r = Rect(0, 0, 100, 100) 227 | v = Viewbox({'viewBox': '0 0 100 100'}) 228 | self.assertEqual(v.transform(r), '') 229 | 230 | def test_viewbox_issue_228(self): 231 | self.assertIn( 232 | SVG(viewBox="0 0 10 10", width="10mm", height="10mm").string_xml(), 233 | ( 234 | """""", 235 | """""", # Python 3.6 236 | ), 237 | ) 238 | 239 | def test_issue_228b(self): 240 | svg = SVG(viewBox="0 0 10 10", width="10mm", height="10mm") 241 | svg.append(Rect(x="1mm", y="1mm", width="5mm", height="5mm", rx="0.5mm", stroke="red")) 242 | svg.append(Circle(cx="5mm", cy="5mm", r="0.5mm", stroke="blue")) 243 | svg.append(Ellipse(cx="5mm", cy="5mm", rx="0.5mm", ry="0.8mm", stroke="lime")) 244 | svg.append(SimpleLine(x1="5mm", y1="5em", x2="10%", y2="15%", stroke="gray")) 245 | svg.append(Polygon(5, 10, 20, 30, 40, 7)) 246 | svg.append(Path("M10,10z", stroke="yellow")) 247 | print(svg.string_xml()) 248 | 249 | def test_issue_228c(self): 250 | rect = Rect(x="1mm", y="1mm", width="5mm", height="5mm", rx="0.5mm", stroke="red") 251 | print(rect.length()) 252 | 253 | def test_viewbox_scale(self): 254 | r = Rect(0, 0, 200, 200) 255 | v = Viewbox('0 0 100 100') 256 | self.assertEqual(v.transform(r), 'scale(2, 2)') 257 | 258 | def test_viewbox_translate(self): 259 | r = Rect(0, 0, 100, 100) 260 | v = Viewbox(Viewbox('-50 -50 100 100')) 261 | self.assertEqual(v.transform(r), 'translate(50, 50)') 262 | 263 | def test_viewbox_parse_empty(self): 264 | q = io.StringIO(u''' 265 | 266 | ''') 267 | m = SVG.parse(q) 268 | q = list(m.elements()) 269 | self.assertEqual(len(q), 1) 270 | self.assertEqual(None, m.viewbox) 271 | 272 | def test_viewbox_parse_100(self): 273 | q = io.StringIO(u''' 274 | 275 | ''') 276 | m = SVG.parse(q, width=100, height=100) 277 | q = list(m.elements()) 278 | self.assertEqual(len(q), 1) 279 | self.assertEqual(Matrix(m.viewbox_transform), Matrix.identity()) 280 | 281 | def test_viewbox_parse_translate(self): 282 | q = io.StringIO(u''' 283 | 284 | ''') 285 | m = SVG.parse(q) 286 | q = list(m.elements()) 287 | self.assertEqual(len(q), 1) 288 | self.assertEqual(Matrix(m.viewbox_transform), Matrix.translate(1, 1)) 289 | -------------------------------------------------------------------------------- /test/test_write.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import pathlib 4 | import unittest 5 | from xml.etree.ElementTree import ParseError 6 | 7 | from svgelements import * 8 | 9 | 10 | class TestElementWrite(unittest.TestCase): 11 | 12 | def test_write(self): 13 | q = io.StringIO( 14 | u''' 15 | ' 16 | 17 | 18 | 19 | 20 | ''') 21 | svg = SVG.parse(q, reify=False) 22 | print(svg.string_xml()) 23 | # svg.write_xml("myfile.svg") 24 | 25 | def test_write_group(self): 26 | g = Group() 27 | self.assertEqual(g.string_xml(), "") 28 | 29 | def test_write_rect(self): 30 | r = Rect("1in", "1in", "3in", "3in", rx="5%") 31 | self.assertIn( 32 | r.string_xml(), 33 | ( 34 | '', 35 | '', 36 | ), 37 | ) 38 | r *= "scale(3)" 39 | self.assertIn( 40 | r.string_xml(), 41 | ( 42 | '', 43 | '', 44 | ), 45 | ) 46 | r.reify() 47 | self.assertIn( 48 | r.string_xml(), 49 | ( 50 | '', 51 | '', 52 | ), 53 | ) 54 | r = Path(r) 55 | self.assertIn( 56 | r.string_xml(), 57 | ( 58 | '', 59 | '', 60 | ), 61 | ) 62 | 63 | def test_write_path(self): 64 | r = Path("M0,0zzzz") 65 | r *= "translate(5,5)" 66 | self.assertEqual( 67 | r.string_xml(), 68 | '', 69 | ) 70 | r.reify() 71 | self.assertEqual(r.string_xml(), '') 72 | 73 | def test_write_circle(self): 74 | c = Circle(r=5, stroke="none", fill="yellow") 75 | q = SVG.parse(io.StringIO(c.string_xml())) 76 | self.assertEqual(c, q) 77 | 78 | def test_write_ellipse(self): 79 | c = Ellipse(rx=3, ry=2, fill="cornflower blue") 80 | q = SVG.parse(io.StringIO(c.string_xml())) 81 | self.assertEqual(c, q) 82 | 83 | def test_write_line(self): 84 | c = SimpleLine(x1=0, x2=10, y1=5, y2=6, id="line", fill="light grey") 85 | q = SVG.parse(io.StringIO(c.string_xml())) 86 | self.assertEqual(c, q) 87 | 88 | def test_write_pathlib_issue_227(self): 89 | """ 90 | Tests pathlib.Path file saving. This is permitted by the xml writer but would crash see issue #227 91 | 92 | This also provides an example of pretty-print off and short_empty_elements off (an xml writer option). 93 | 94 | """ 95 | file1 = "myfile.svg" 96 | self.addCleanup(os.remove, file1) 97 | file = pathlib.Path(file1) 98 | svg = SVG(viewport="0 0 1000 1000", height="10mm", width="10mm") 99 | svg.append(Rect("10%", "10%", "80%", "80%", fill="red")) 100 | svg.write_xml(file, pretty=False, short_empty_elements=False) 101 | 102 | def test_write_filename(self): 103 | """ 104 | Tests filename file saving. This is permitted by the xml writer but would crash see issue #227 105 | 106 | This also provides an example short_empty_elements off, utf-8 encoding. 107 | 108 | """ 109 | file1 = "myfile-f.svg" 110 | self.addCleanup(os.remove, file1) 111 | svg = SVG(viewport="0 0 1000 1000", height="10mm", width="10mm") 112 | svg.append(Rect("10%", "10%", "80%", "80%", fill="red")) 113 | svg.write_xml(file1, short_empty_elements=False, encoding="utf-8") 114 | 115 | def test_write_filename_svgz(self): 116 | """ 117 | Tests pathlib.Path file saving. This is permitted by the xml writer but would crash see issue #227 118 | 119 | This also provides an example of xml_declaration=True. 120 | 121 | """ 122 | file1 = "myfile-f.svgz" 123 | self.addCleanup(os.remove, file1) 124 | svg = SVG(viewport="0 0 1000 1000", height="10mm", width="10mm") 125 | svg.append(Rect("10%", "10%", "80%", "80%", fill="red")) 126 | svg.write_xml(file1, xml_declaration=True) 127 | 128 | 129 | # def test_read_write(self): 130 | # import glob 131 | # for g in glob.glob("*.svg"): 132 | # if g.startswith("test-"): 133 | # continue 134 | # print(g) 135 | # try: 136 | # svg = SVG.parse(g, transform="translate(1,1)") 137 | # except ParseError: 138 | # print(f"{g} could not be parsed.") 139 | # continue 140 | # except ValueError: 141 | # continue 142 | # svg.write_xml(f"test-{g}.") 143 | -------------------------------------------------------------------------------- /tools/build_pypi.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo Build and upload a package for PyPi 3 | mkdir dist 4 | mkdir old_dist 5 | move dist\* old_dist 6 | python setup.py sdist bdist_wheel --universal 7 | twine.exe upload dist\* --repository SVGELEMENTS --------------------------------------------------------------------------------