├── .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 |
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 |
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 | ''')
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 |
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 |
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 | '''
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 |
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 | ''')
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'''''')
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'''''')
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'''''')
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 | ''')
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 | ''')
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 | ''')
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 | ''')
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'''''')
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 | """
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 |
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 |
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 | ''')
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 | ''')
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 | ''')
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 | ''')
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 | ''')
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 | ''')
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'''''')
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 | """
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 | ''')
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 | ''')
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 | ''')
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 | ''')
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 | ''')
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 | ''')
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 | ''')
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 | ''')
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 |
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
--------------------------------------------------------------------------------