├── .github ├── dependabot.yml └── workflows │ ├── black.yml │ ├── pre-commit-detect-outdated.yml │ ├── pre-commit-run.yml │ ├── python-publish.yml │ └── run-tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.rst ├── CONTRIBUTING.rst ├── LICENSE ├── README.rst ├── RELEASE.rst ├── docs ├── Makefile └── source │ ├── compose.rst │ ├── conf.py │ ├── getting_started.rst │ ├── index.rst │ ├── reference.rst │ ├── transform.rst │ ├── tutorials.rst │ └── tutorials │ ├── composing_multipanel_figures.rst │ ├── figures │ ├── Makefile │ ├── anscombe.png │ ├── anscombe.svg │ ├── composing_multipanel_figure_ex1.svg │ ├── composing_multipanel_figures │ │ ├── ex1.svg │ │ ├── ex1a.svg │ │ ├── ex1b.svg │ │ ├── ex2.svg │ │ ├── ex3.svg │ │ ├── ex3b.svg │ │ ├── ex4.svg │ │ ├── ex5.svg │ │ ├── ex6.svg │ │ ├── ex7.svg │ │ └── ex8.svg │ ├── composing_multipanel_figures_ex1.svg │ ├── composing_multipanel_figures_ex2.svg │ ├── fig_final.png │ ├── fig_final.svg │ ├── fig_final_compose.svg │ ├── pyplot_publication.svg │ ├── sigmoid_fit.png │ └── sigmoid_fit.svg │ ├── publication_quality_figures.rst │ └── scripts │ ├── anscombe.py │ ├── composing_multipanel_figures_examples.py │ ├── fig_compose.py │ ├── fig_final.py │ └── sigmoid_fit.py ├── examples ├── compose_example.py ├── compose_scaling.py ├── files │ ├── example.svg │ ├── lion.jpeg │ └── svg_logo.svg ├── stack_plots.py └── stack_svg.py ├── requirements.txt ├── setup.py ├── src └── svgutils │ ├── __init__.py │ ├── common.py │ ├── compose.py │ ├── templates.py │ └── transform.py └── tests ├── letter_spacing.py ├── test_compose.py └── test_transform.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Sebastian Pipping 2 | # Licensed under the MIT License 3 | 4 | version: 2 5 | updates: 6 | 7 | - package-ecosystem: "github-actions" 8 | commit-message: 9 | include: "scope" 10 | prefix: "Actions" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | -------------------------------------------------------------------------------- /.github/workflows/black.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3.0.2 10 | - uses: actions/setup-python@v4.2.0 11 | - uses: psf/black@22.8.0 12 | with: 13 | options: --check --diff --target-version py36 --exclude ^/docs/source/conf\.py$ 14 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit-detect-outdated.yml: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Sebastian Pipping 2 | # Licensed under the MIT License 3 | 4 | name: Detect outdated pre-commit hooks 5 | 6 | on: 7 | schedule: 8 | - cron: '0 16 * * 5' # Every Friday 4pm 9 | 10 | jobs: 11 | pip_detect_outdated: 12 | name: Detect outdated pre-commit hooks 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3.0.2 16 | 17 | - name: Set up Python 3.9 18 | uses: actions/setup-python@v4.2.0 19 | with: 20 | python-version: 3.9 21 | 22 | - name: Install pre-commit 23 | run: |- 24 | pip install \ 25 | --disable-pip-version-check \ 26 | --no-warn-script-location \ 27 | --user \ 28 | pre-commit 29 | echo "PATH=${HOME}/.local/bin:${PATH}" >> "${GITHUB_ENV}" 30 | 31 | - name: Check for outdated hooks (and fail if any) 32 | run: |- 33 | pre-commit autoupdate 34 | git diff --exit-code -- .pre-commit-config.yaml 35 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit-run.yml: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Sebastian Pipping 2 | # Licensed under the MIT License 3 | 4 | name: Run pre-commit on all files 5 | 6 | on: 7 | - pull_request 8 | - push 9 | 10 | jobs: 11 | run_pre_commit: 12 | name: Run pre-commit on all files 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3.0.2 16 | 17 | - name: Set up Python 3.9 18 | uses: actions/setup-python@v4.2.0 19 | with: 20 | python-version: 3.9 21 | 22 | - name: Install pre-commit 23 | run: |- 24 | pip install \ 25 | --disable-pip-version-check \ 26 | --user \ 27 | --no-warn-script-location \ 28 | pre-commit 29 | echo "PATH=${HOME}/.local/bin:${PATH}" >> "${GITHUB_ENV}" 30 | 31 | - name: Install pre-commit hooks 32 | run: |- 33 | pre-commit install --install-hooks 34 | 35 | - name: Run pre-commit on all files 36 | run: |- 37 | pre-commit run --all-files --show-diff-on-failure 38 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3.0.2 17 | - name: Set up Python 18 | uses: actions/setup-python@v4.2.0 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python3 -m pip install --upgrade pip 24 | pip3 install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python3 setup.py sdist bdist_wheel 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Sebastian Pipping 2 | # Licensed under the MIT License 3 | 4 | name: Run the test suite 5 | 6 | on: 7 | - pull_request 8 | - push 9 | 10 | jobs: 11 | run-tests: 12 | name: Run the test suite 13 | strategy: 14 | matrix: 15 | python-version: [3.6, 3.9] # no explicit need for 3.7, 3.8 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3.0.2 19 | - uses: actions/setup-python@v4.2.0 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install runtime dependencies 23 | run: | 24 | python3 --version 25 | pip3 install --user -r requirements.txt 26 | pip3 install --user -e . 27 | echo "PATH=${HOME}/.local/bin:${PATH}" >> "${GITHUB_ENV}" 28 | - name: Run the test suite 29 | env: 30 | MPLBACKEND: agg 31 | run: | 32 | nosetests -v 33 | ( cd docs && make doctest ) 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swc 3 | *.pyc 4 | docs/build 5 | build/ 6 | dist/ 7 | src/svgutils.egg-info/ 8 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 22.8.0 4 | hooks: 5 | - id: black 6 | language_version: python3 7 | args: ['--target-version', 'py36'] 8 | exclude: '^docs/source/conf\.py$' 9 | 10 | - repo: https://github.com/pycqa/isort 11 | rev: 5.10.1 12 | hooks: 13 | - id: isort 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.rst: -------------------------------------------------------------------------------- 1 | Code of Conduct 2 | =============== 3 | 4 | Like the technical community as a whole, the SVGutils team and community is made up of a mixture of professionals and volunteers from all over the world. 5 | 6 | Diversity is one of our huge strengths, but it can also lead to communication issues and unhappiness. To that end, we have a few ground rules that we ask people to adhere to when they're participating within this community and project. These rules apply equally to founders, mentors and those seeking help and guidance. 7 | 8 | This code of conduct applies to all communication: the issue tracker, comments, commits, and any other forums created by the project team which the community uses for communication. 9 | 10 | If you believe someone is violating the code of conduct, we ask that you report it by contacting any of our community leaders: 11 | 12 | - **Bartosz Telenczuk**, `GitHub `_ 13 | 14 | New members may become community leaders with the support of at least two contributors, that created at least 2 pull requests in the period of last 2 years. This can be done by adding a pull request and any contributors supporting can comment on the issue. 15 | Community leaders will review your complaint and decide on course of action following the incident. 16 | 17 | This isn't an exhaustive list of things that you can't do. Rather, take it in the spirit in which it's intended - a guide to make it easier to enrich all of us and the technical community. 18 | 19 | - **Be friendly and patient**. 20 | - **Be welcoming**. We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. 21 | - **Be considerate**. Your work will be used by other people, and you in turn will depend on the work of others. Any decision you make will affect users and colleagues, and you should take those consequences into account when making decisions. 22 | - **Be respectful**. Not all of us will agree all the time, but disagreement is no excuse for poor behaviour and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It's important to remember that a community where people feel uncomfortable or threatened is not a productive one. 23 | - **Be careful in the words that you choose**. Remember that sexist, racist, and other exclusionary jokes can be offensive to those around you. Be kind to others. Do not insult or put down other participants. Behave professionally. Remember that harassment and sexist, racist, or exclusionary jokes are not appropriate for the community. 24 | - Violent threats or language directed against another person. 25 | - Discriminatory jokes and language. 26 | - Posting sexually explicit or violent material. 27 | - Posting (or threatening to post) other people's personally identifying information ("doxing"). 28 | - Personal insults, especially those using racist or sexist terms. 29 | - Unwelcome sexual attention. 30 | - Advocating for, or encouraging, any of the above behavior. 31 | - Repeated harassment of others. In general, if someone asks you to stop, then stop. 32 | - **When we disagree, we try to understand why**. Disagreements, both social and technical, happen all the time and our community is no exception. It is important that we resolve disagreements and differing views constructively. Remember that we're different. The strength comes from its varied community, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn't mean that they're wrong. Don't forget that it is human to err and blaming each other doesn't get us anywhere, rather offer to help resolving issues and to help learn from mistakes. 33 | 34 | 35 | Adapted from Code of Conduct of the `Django project `_. -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | If you like the project and you would like to contribute, you can submit pull requests (PRs). 2 | 3 | Make sure to included the following with your PR: 4 | 5 | * short description of the contribution (in the Github form) 6 | * unit tests for the code that you implemented 7 | * docummentation of new functions, classes etc. 8 | 9 | You can also fix some the issues in the github tracker. 10 | If you decide to work on it, make sure to add a comment 11 | saying that you take it. If there was no activity for 12 | a week on a PR or an issue, please feel free to adopt it. 13 | 14 | 15 | ## Code formatting 16 | 17 | This project uses [black](https://github.com/psf/black) for automatically format the Python source code. 18 | You can run the check and formatting hooks using pre-commit: 19 | 20 | ``` 21 | pip3 install pre-commit 22 | pre-commit run --all 23 | ``` 24 | 25 | You can also integrate pre-commit with git, to run it for each commit: 26 | 27 | ``` 28 | pre-commit install 29 | ``` 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011 by Bartosz Telenczuk 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://github.com/btel/svg_utils/workflows/Run%20the%20test%20suite/badge.svg 2 | :target: https://github.com/btel/svg_utils/actions 3 | 4 | .. image:: https://readthedocs.org/projects/svgutils/badge/?version=latest 5 | :target: http://svgutils.readthedocs.io/en/latest/?badge=latest 6 | 7 | Python-based SVG editor 8 | ======================= 9 | 10 | This is an utility package that helps to edit and concatenate SVG 11 | files. It is especially directed at scientists preparing final figures 12 | for submission to journal. So far it supports arbitrary placement and 13 | scaling of svg figures and adding markers, such as labels. 14 | 15 | See the `blog post `_ for a short tutorial. 16 | 17 | The full documentation is available 18 | `here `_. 19 | 20 | Install 21 | ------- 22 | 23 | From PyPI 24 | ````````` 25 | 26 | You can install `svgutils` from Python Package Index (PyPI) using the `pip3` utility:: 27 | 28 | pip3 install svgutils --user 29 | 30 | Note that the `pip3` will attempt to install `lxml` library if it is not already installed. 31 | For the installation to be sucessful, you need development libraries of `libxml2` and `libxslt1`. 32 | On Ubuntu and other Debian-derived Linux distributions you can install them via:: 33 | 34 | sudo apt-get install libxml2-dev libxslt-dev 35 | 36 | From Conda 37 | `````````` 38 | Installing `svgutils` from the `conda-forge` channel can be achieved by adding `conda-forge` to your channels with:: 39 | 40 | conda config --add channels conda-forge 41 | 42 | You can install `svgutils` from `conda-forge` channel:: 43 | 44 | conda install svgutils 45 | 46 | If you don't want to add the channel to your configuration, you can specify it at the time of installation:: 47 | 48 | conda install svgutils -c conda-forge 49 | 50 | From sources 51 | ```````````` 52 | 53 | To install system-wide (needs administrator privilages):: 54 | 55 | python3 setup.py install 56 | 57 | To install locally (do not forget to add 58 | ``$HOME/python/lib/python3.6/site-packages/`` to your Python path):: 59 | 60 | python3 setup.py install --user 61 | 62 | License 63 | ------- 64 | 65 | The package is distributed under MIT license (see LICENSE file for 66 | information). 67 | 68 | Related packages 69 | ---------------- 70 | 71 | `svg_stack `_ is a similar 72 | package that layouts multiple SVG files automatically (in a Qt-style). 73 | 74 | `svgmanip `_ a related 75 | library that aims for a simple API with the ability to export to 76 | PNG accurately 77 | 78 | `cairosvg `_ a command-line SVG to PNG converter 79 | for Python 3.4+ 80 | 81 | `svglib `_ a pure-Python 82 | library for reading and converting SVG 83 | 84 | Authors 85 | ------- 86 | 87 | Bartosz Telenczuk (bartosz.telenczuk@gmail.com) 88 | -------------------------------------------------------------------------------- /RELEASE.rst: -------------------------------------------------------------------------------- 1 | To do a new release: 2 | 3 | - update version information and download link in setup.py 4 | - add a new tag and push it to github `git push --tags` 5 | - create a new release on github using the new tag 6 | - github actions should automatically build and upload the release to PYPI 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/svgutils.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/svgutils.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/svgutils" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/svgutils" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/source/compose.rst: -------------------------------------------------------------------------------- 1 | ``compose`` -- easy figure composing 2 | ------------------------------------ 3 | 4 | ``compose`` module is a wrapper on top of :py:mod:`svgutils.transform` that 5 | simplifies composing SVG figures. Here is a short example of how a figure could 6 | be constructed:: 7 | 8 | Figure( "10cm", "5cm", 9 | SVG('svg_logo.svg').scale(0.2), 10 | Image(120, 120, 'lion.jpeg').move(120, 0) 11 | ).save('test.svg') 12 | 13 | .. automodule:: svgutils.compose 14 | :members: 15 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # svgutils documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Apr 12 21:52:16 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import os 15 | import sys 16 | 17 | import sphinx_rtd_theme 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | sys.path.insert(0, os.path.abspath("../../src")) 23 | 24 | # -- General configuration ----------------------------------------------------- 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | # needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be extensions 30 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 31 | extensions = [ 32 | "sphinx.ext.autodoc", 33 | "sphinx.ext.doctest", 34 | "sphinx.ext.viewcode", 35 | "numpydoc", 36 | "sphinx.ext.autosummary", 37 | "doctest", 38 | ] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ["_templates"] 42 | 43 | # The suffix of source filenames. 44 | source_suffix = ".rst" 45 | 46 | # The encoding of source files. 47 | # source_encoding = 'utf-8-sig' 48 | 49 | # The master toctree document. 50 | master_doc = "index" 51 | 52 | # General information about the project. 53 | project = u"svgutils" 54 | copyright = u"2011, Bartosz Telenczuk" 55 | 56 | # The version info for the project you're documenting, acts as replacement for 57 | # |version| and |release|, also used in various other places throughout the 58 | # built documents. 59 | # 60 | # The short X.Y version. 61 | version = "0.1" 62 | # The full version, including alpha/beta/rc tags. 63 | release = "0.1" 64 | 65 | # The language for content autogenerated by Sphinx. Refer to documentation 66 | # for a list of supported languages. 67 | # language = None 68 | 69 | # There are two options for replacing |today|: either, you set today to some 70 | # non-false value, then it is used: 71 | # today = '' 72 | # Else, today_fmt is used as the format for a strftime call. 73 | # today_fmt = '%B %d, %Y' 74 | 75 | # List of patterns, relative to source directory, that match files and 76 | # directories to ignore when looking for source files. 77 | exclude_patterns = [] 78 | 79 | # The reST default role (used for this markup: `text`) to use for all documents. 80 | # default_role = None 81 | 82 | # If true, '()' will be appended to :func: etc. cross-reference text. 83 | # add_function_parentheses = True 84 | 85 | # If true, the current module name will be prepended to all description 86 | # unit titles (such as .. function::). 87 | # add_module_names = True 88 | 89 | # If true, sectionauthor and moduleauthor directives will be shown in the 90 | # output. They are , 'sphinx.ext.autosummary'ignored by default. 91 | # show_authors = False 92 | 93 | # The name of the Pygments (syntax highlighting) style to use. 94 | pygments_style = "sphinx" 95 | 96 | # A list of ignored prefixes for module index sorting. 97 | # modindex_common_prefix = [] 98 | 99 | 100 | # -- Options for HTML output --------------------------------------------------- 101 | 102 | # The theme to use for HTML and HTML Help pages. See the documentation for 103 | # a list of builtin themes. 104 | html_theme = "sphinx_rtd_theme" 105 | 106 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 107 | # Theme options are theme-specific and customize the look and feel of a theme 108 | # further. For a list of options available for each theme, see the 109 | # documentation. 110 | html_theme_options = {"nosidebar": False} 111 | 112 | 113 | # Add any paths that contain custom themes here, relative to this directory. 114 | # html_theme_path = [] 115 | 116 | # The name for this set of Sphinx documents. If None, it defaults to 117 | # " v documentation". 118 | # html_title = None 119 | 120 | # A shorter title for the navigation bar. Default is the same as html_title. 121 | # html_short_title = None 122 | 123 | # The name of an image file (relative to this directory) to place at the top 124 | # of the sidebar. 125 | # html_logo = None 126 | 127 | # The name of an image file (within the static path) to use as favicon of the 128 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 129 | # pixels large. 130 | # html_favicon = None 131 | 132 | # Add any paths that contain custom static files (such as style sheets) here, 133 | # relative to this directory. They are copied after the builtin static files, 134 | # so a file named "default.css" will overwrite the builtin "default.css". 135 | # html_static_path = ['_static'] 136 | html_static_path = [] 137 | 138 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 139 | # using the given strftime format. 140 | # html_last_updated_fmt = '%b %d, %Y' 141 | 142 | # If true, SmartyPants will be used to convert quotes and dashes to 143 | # typographically correct entities. 144 | # html_use_smartypants = True 145 | 146 | # Custom sidebar templates, maps document names to template names. 147 | # html_sidebars = {} 148 | 149 | # Additional templates that should be rendered to pages, maps page names to 150 | # template names. 151 | # html_additional_pages = {} 152 | 153 | # If false, no module index is generated. 154 | # html_domain_indices = True 155 | 156 | # If false, no index is generated. 157 | # html_use_index = True 158 | 159 | # If true, the index is split into individual pages for each letter. 160 | # html_split_index = False 161 | 162 | # If true, links to the reST sources are added to the pages. 163 | # html_show_sourcelink = True 164 | 165 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 166 | # html_show_sphinx = True 167 | 168 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 169 | # html_show_copyright = True 170 | 171 | # If true, an OpenSearch description file will be output, and all pages will 172 | # contain a tag referring to it. The value of this option must be the 173 | # base URL from which the finished HTML is served. 174 | # html_use_opensearch = '' 175 | 176 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 177 | # html_file_suffix = None 178 | 179 | # Output file base name for HTML help builder. 180 | htmlhelp_basename = "svgutilsdoc" 181 | 182 | 183 | # -- Options for LaTeX output -------------------------------------------------- 184 | 185 | # The paper size ('letter' or 'a4'). 186 | # latex_paper_size = 'letter' 187 | 188 | # The font size ('10pt', '11pt' or '12pt'). 189 | # latex_font_size = '10pt' 190 | 191 | # Grouping the document tree into LaTeX files. List of tuples 192 | # (source start file, target name, title, author, documentclass [howto/manual]). 193 | latex_documents = [ 194 | ( 195 | "index", 196 | "svgutils.tex", 197 | u"svgutils Documentation", 198 | u"Bartosz Telenczuk", 199 | "manual", 200 | ), 201 | ] 202 | 203 | # The name of an image file (relative to this directory) to place at the top of 204 | # the title page. 205 | # latex_logo = None 206 | 207 | # For "manual" documents, if this is true, then toplevel headings are parts, 208 | # not chapters. 209 | # latex_use_parts = False 210 | 211 | # If true, show page references after internal links. 212 | # latex_show_pagerefs = False 213 | 214 | # If true, show URL addresses after external links. 215 | # latex_show_urls = False 216 | 217 | # Additional stuff for the LaTeX preamble. 218 | # latex_preamble = '' 219 | 220 | # Documents to append as an appendix to all manuals. 221 | # latex_appendices = [] 222 | 223 | # If false, no module index is generated. 224 | # latex_domain_indices = True 225 | 226 | 227 | # -- Options for manual page output -------------------------------------------- 228 | 229 | # One entry per manual page. List of tuples 230 | # (source start file, name, description, authors, manual section). 231 | man_pages = [ 232 | ("index", "svgutils", u"svgutils Documentation", [u"Bartosz Telenczuk"], 1) 233 | ] 234 | -------------------------------------------------------------------------------- /docs/source/getting_started.rst: -------------------------------------------------------------------------------- 1 | .. title:: svgutils tutorial 2 | 3 | ===================================== 4 | Getting Started 5 | ===================================== 6 | 7 | Install 8 | ------- 9 | 10 | From PyPI 11 | ````````` 12 | 13 | You can install `svgutils` from Python Package Index (PyPI) using the `pip3` utility:: 14 | 15 | pip3 install svgutils --user 16 | 17 | Note that the `pip3` will attempt to install `lxml` library if it is not already installed. 18 | For the installation to be sucessful, you need development libraries of `libxml2` and `libxslt1`. 19 | On Ubuntu and other Debian-derived Linux distributions you can install them via:: 20 | 21 | sudo apt-get install libxml2-dev libxslt-dev 22 | 23 | From Conda 24 | `````````` 25 | Installing `svgutils` from the `conda-forge` channel can be achieved by adding `conda-forge` to your channels with:: 26 | 27 | conda config --add channels conda-forge 28 | 29 | You can install `svgutils` from `conda-forge` channel:: 30 | 31 | conda install svgutils 32 | 33 | If you don't want to add the channel to your configuration, you can specify it at the time of installation:: 34 | 35 | conda install svgutils -c conda-forge 36 | 37 | From sources 38 | ```````````` 39 | 40 | To install system-wide (needs administrator privilages):: 41 | 42 | python3 setup.py install 43 | 44 | To install locally (do not forget to add 45 | ``$HOME/python/lib/python3.6/site-packages/`` to your Python path):: 46 | 47 | python3 setup.py install --user 48 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to svgutils's documentation! 2 | ==================================== 3 | 4 | Contents 5 | ======== 6 | 7 | .. toctree:: 8 | :numbered: 9 | :maxdepth: 2 10 | 11 | getting_started 12 | tutorials 13 | reference 14 | 15 | Indices and tables 16 | ================== 17 | 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /docs/source/reference.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | --------- 3 | 4 | .. toctree:: 5 | 6 | transform 7 | compose 8 | -------------------------------------------------------------------------------- /docs/source/transform.rst: -------------------------------------------------------------------------------- 1 | ``transform`` -- basic SVG transformations 2 | ------------------------------------------ 3 | 4 | This module implements low-level API allowing to open and manipulate SVG files. 5 | An example use is described in the :doc:`tutorials/publication_quality_figures` 6 | tutorial. 7 | 8 | .. automodule:: svgutils.transform 9 | :members: 10 | -------------------------------------------------------------------------------- /docs/source/tutorials.rst: -------------------------------------------------------------------------------- 1 | Tutorials 2 | --------- 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | tutorials/publication_quality_figures.rst 8 | tutorials/composing_multipanel_figures.rst 9 | -------------------------------------------------------------------------------- /docs/source/tutorials/composing_multipanel_figures.rst: -------------------------------------------------------------------------------- 1 | Composing multi-panel figures 2 | ============================= 3 | 4 | As I already explained in the previous tutorial, creating figures 5 | programmatically has many advantages. However, obtaining a complex 6 | layout only by scripting can be very time consuming and even 7 | distressing. Therefore, the possible gains can be crippled by the 8 | time spent tweaking the programs to obtain optimal results and under 9 | time pressure many of us resort to visual editors. One way to alleviate 10 | the problem is to use a library with little boilerplate code and which 11 | simplifies the common tasks (such as inserting a new panel and adjusting 12 | its position). That's why I introduced the :doc:`compose` module, which 13 | is a wrapper around the low-level API described in :doc:`publication_quality_figures`. 14 | 15 | Let's take the example from the previous tutorial 16 | 17 | .. figure:: figures/fig_final.png 18 | 19 | To obtain this nicely-formatted final figure we needed a :ref:`considerable ` amount of code. 20 | The same effect could be achieved in ``compose`` with fewer lines of code: 21 | 22 | .. literalinclude:: scripts/fig_compose.py 23 | 24 | The ``compose`` module offers the same functionality as the ``transform``, but 25 | rather than being based on procedural description of the figure it attempts 26 | declarative approach. The code defining the figure mimics a hierarchical 27 | structure typical of most figures: A figure contains multiple panels; these panels can in 28 | turn contain several graphical elements such as text, markers or other 29 | (sub-)panels. 30 | 31 | Defining a figure 32 | ----------------- 33 | 34 | Before we start we need to import the definitions from ``svgutils.compose`` module:: 35 | 36 | from svgutils.compose import * 37 | 38 | In `compose` the top-most element is the ``Figure()`` object. To create a figure we need to specify 39 | its size (width and height) and its contents. For example, to create a figure consisting of a single 40 | imported SVG file we might write:: 41 | 42 | Figure("16cm", "6.5cm", 43 | SVG("sigmoid_fit.svg") 44 | ) 45 | 46 | This will create a 16-by-6.5 cm figure with showing the ``sigmoid_fit.svg`` file. 47 | Note that the dimensions can be defined together with units supported by SVG 48 | (so far "px" and "cm" are implemented). If no units are defined it defaults 49 | to "px". ``SVG()`` is another object from ``compose`` module, which simply 50 | parses and pastes the content of a SVG file into the figure. 51 | 52 | The ``Figure()`` object also defines several methods; the ``save()`` method 53 | saves the figure in a SVG file: 54 | 55 | .. code-block:: python 56 | :caption: :download:`Figure preview ` 57 | 58 | Figure("16cm", "6.5cm", 59 | SVG("sigmoid_fit.svg") 60 | ).save("fig1.svg") 61 | 62 | .. figure:: figures/composing_multipanel_figures/ex1.svg 63 | 64 | Adding annotations 65 | ------------------ 66 | 67 | The simple example of previous section is superfluous, because it does not modify the ``sigmoid_fit.svg`` 68 | file apart from changing its size. Let us try then overlaying some text on top of the figure. 69 | In ``compose`` we can add text using ``Text()`` object: 70 | 71 | .. code-block:: python 72 | :caption: :download:`Figure preview ` 73 | 74 | Figure("16cm", "6.5cm", 75 | Text("A", 25, 20), 76 | SVG("sigmoid_fit.svg") 77 | ) 78 | 79 | In addition to the text itself we defined the $x$ and $y$ coordinates of the text element in pixel units. 80 | We can also add additional style arguments -- to increase the font size and change to bold letters we can use: 81 | 82 | .. code-block:: python 83 | :caption: :download:`Figure preview ` 84 | 85 | Figure("16cm", "6.5cm", 86 | Text("A", 25, 20, size=12, weight='bold'), 87 | SVG("sigmoid_fit.svg") 88 | ) 89 | 90 | .. figure:: figures/composing_multipanel_figures/ex1b.svg 91 | 92 | Arranging multiple elements 93 | --------------------------- 94 | 95 | We can combine multiple SVG drawings by simply listing them inside the ``Figure()`` object: 96 | 97 | .. code-block:: python 98 | :caption: :download:`Figure preview ` 99 | 100 | Figure("16cm", "6.5cm", 101 | SVG("sigmoid_fit.svg"), 102 | SVG("anscombe.svg") 103 | ) 104 | 105 | The problem with this 106 | figure is that the drawings will overlap and become quite unreadable. To avoid it 107 | we have to move figure elements. To do that automatically you 108 | can use ``tile()`` method of ``Figure()``, which arranges the elements 109 | on a regular two-dimensional grid. For example, to arrange the two SVG elements 110 | in a single row we might use: 111 | 112 | 113 | .. code-block:: python 114 | :caption: :download:`Figure preview ` 115 | 116 | Figure("16cm", "6.5cm", 117 | SVG("sigmoid_fit.svg"), 118 | SVG("anscombe.svg") 119 | ).tile(2, 1) 120 | 121 | The second figure (:file:`anscombe.svg`) does not fit entirely in the figure so 122 | we have to scale it down. For this aim each element of the Figure exposes a ``scale()`` 123 | method, which takes the scaling factor as its sole argument: 124 | 125 | .. code-block:: python 126 | :caption: :download:`Figure preview ` 127 | 128 | Figure("16cm", "6.5cm", 129 | SVG("sigmoid_fit.svg"), 130 | SVG("anscombe.svg").scale(0.5) 131 | ).tile(2, 1) 132 | 133 | 134 | .. figure:: figures/composing_multipanel_figures/ex3b.svg 135 | 136 | 137 | For more control over the final figure layout we can position the 138 | individual elements using their ``move()`` method: 139 | 140 | .. code-block:: python 141 | :caption: :download:`Figure preview ` 142 | 143 | Figure("16cm", "6.5cm", 144 | SVG("sigmoid_fit.svg"), 145 | SVG("anscombe.svg").move(280, 0) 146 | ) 147 | 148 | This will move the ``ansombe.svg`` 280 px horizontally. Methods can be also 149 | chained: 150 | 151 | .. code-block:: python 152 | :caption: :download:`Figure preview ` 153 | 154 | Figure("16cm", "6.5cm", 155 | SVG("sigmoid_fit.svg"), 156 | SVG("anscombe.svg").scale(0.5) 157 | .move(280, 0) 158 | ) 159 | 160 | It's often difficult to arrange the figures correctly and it can involve mundane 161 | going back and fro between the code and generated SVG file. To ease the process 162 | ``compose`` offers several helper objects: The ``Grid()`` object generates a grid of 163 | horizontal and vertical lines labelled with their position in pixel units. To 164 | add it simply list ``Grid()`` as one of ``Figure()`` elements: 165 | 166 | .. code-block:: python 167 | :caption: :download:`Figure preview ` 168 | 169 | Figure("16cm", "6.5cm", 170 | SVG("sigmoid_fit.svg"), 171 | SVG("anscombe.svg").scale(0.5) 172 | .move(280, 0), 173 | Grid(20, 20) 174 | ) 175 | 176 | The two parameters of ``Grid()`` define the spacing between the vertical and 177 | horizontal lines, respectively. You can use the lines and numerical labels to 178 | quickly estimate the required vertical and horizontal shifts of the figure 179 | elements. 180 | 181 | 182 | Grouping elements into panels 183 | ----------------------------- 184 | 185 | Figures prepared for publications often consist of sub-panels, which can 186 | contain multiple elements such as graphs, legends and annotations (text, arrows 187 | etc.). Although it is possible to list all these elements separately in the 188 | ``Figure()`` object, it's more convenient to work with all elements belonging to 189 | a single panel as an entire group. In ``compose`` one can group the elements 190 | into panels using ``Panel()`` object: 191 | 192 | .. code-block:: python 193 | :caption: :download:`Figure preview ` 194 | 195 | Figure("16cm", "6.5cm", 196 | Panel( 197 | Text("A", 25, 20), 198 | SVG("sigmoid_fit.svg") 199 | ), 200 | Panel( 201 | Text("B", 25, 20).move(280, 0), 202 | SVG("anscombe.svg").scale(0.5) 203 | .move(280, 0) 204 | ) 205 | ) 206 | 207 | ``Panel()`` just like a ``Figure()`` object takes a list of elements such as 208 | text objects or SVG drawings. However, in contrast to ``Figure()`` it does not 209 | allow to define the size and does not offer ``save()`` method. The two ``Panel()`` 210 | objects of this example contain each a text element and a SVG file. 211 | 212 | In this example the ``Panel()`` 213 | object serve no other role than grouping elements that refer to a single panel 214 | -- it may enhance the readability of the code generating the figure, but it does 215 | not simplify the task of creating the figure. In the second ``Panel()`` we apply 216 | twice the method ``move()`` to position both the text element and the SVG. The 217 | advantage of ``Panel()`` is that we can apply such transforms to the entire 218 | panel: 219 | 220 | .. code-block:: python 221 | :caption: :download:`Figure preview ` 222 | 223 | Figure("16cm", "6.5cm", 224 | Panel( 225 | Text("A", 25, 20), 226 | SVG("sigmoid_fit.svg") 227 | ), 228 | Panel( 229 | Text("B", 25, 20), 230 | SVG("anscombe.svg").scale(0.5) 231 | ).move(280, 0) 232 | ) 233 | 234 | This way we simplified the code, but also the change allows for easier 235 | arrangement of the panels. An additional advantage is that the ``tile()`` method 236 | will automatically arrange the entire panels not the individual elements. 237 | -------------------------------------------------------------------------------- /docs/source/tutorials/figures/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | python3 ../scripts/anscombe.py 3 | python3 ../scripts/sigmoid_fit.py 4 | python3 ../scripts/fig_final.py 5 | python3 ../scripts/fig_compose.py 6 | -------------------------------------------------------------------------------- /docs/source/tutorials/figures/anscombe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btel/svg_utils/5b3e09008181965e3b8b168f4dcda39e8722c568/docs/source/tutorials/figures/anscombe.png -------------------------------------------------------------------------------- /docs/source/tutorials/figures/anscombe.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 20 | 21 | 22 | 23 | 24 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /docs/source/tutorials/figures/composing_multipanel_figure_ex1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | -------------------------------------------------------------------------------- /docs/source/tutorials/figures/composing_multipanel_figures_ex1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | -------------------------------------------------------------------------------- /docs/source/tutorials/figures/fig_final.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btel/svg_utils/5b3e09008181965e3b8b168f4dcda39e8722c568/docs/source/tutorials/figures/fig_final.png -------------------------------------------------------------------------------- /docs/source/tutorials/figures/sigmoid_fit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btel/svg_utils/5b3e09008181965e3b8b168f4dcda39e8722c568/docs/source/tutorials/figures/sigmoid_fit.png -------------------------------------------------------------------------------- /docs/source/tutorials/publication_quality_figures.rst: -------------------------------------------------------------------------------- 1 | .. title:: svgutils tutorial 2 | 3 | ===================================== 4 | Creating publication-quality figures 5 | ===================================== 6 | 7 | `Matplotlib `_ is a decent Python library 8 | for creating publication-quality plots which offers a multitude of 9 | different plot types. However, one limitation of ``matplotlib`` is that 10 | creating complex layouts can be at times complicated. Therefore, 11 | post-processing of plots is usually done in some other vector graphics 12 | editor such as `inkscape `_ or Adobe 13 | Illustrator. The typical workflow is as following: 14 | 15 | 1. Import and analyse data in Python 16 | #. Create figures in ``matplotlib`` 17 | #. Export figures to PDF/SVG 18 | #. Import figures to vector-graphics editor 19 | #. Arrange and edit figures manually 20 | #. Export the figure to PDF 21 | 22 | As you probably see, the typical workflow is quite complicated. To 23 | make things worse you may need to repeat the process several times, 24 | when, for example, you want to include more data into the analysis. 25 | This includes manual editing and arranging the figure, which is 26 | obviously time consuming. Therefore it makes sense to try and 27 | automate the process. A description of an automatic workflow 28 | which completely resides on Python tools is given here. 29 | 30 | 1. *Creating Matplotlib plots* 31 | 32 | To create nice matplotlib-based plots so as 33 | to compose figures from. Download 34 | the following example scripts: 35 | `anscombe.py `_ and `sigmoid_fit.py `_. 36 | 37 | .. figure:: figures/sigmoid_fit.png 38 | :scale: 20 % 39 | 40 | ``sigmoid_fit.py`` 41 | 42 | .. figure:: figures/anscombe.png 43 | :scale: 70 % 44 | 45 | ``anscombe.py`` 46 | 47 | 2. *Exporting to SVG* 48 | 49 | A nice feature of matplotlib is that one can export figures to 50 | Scalable Vector Graphics (SVG) which is an open vector format [1]_ 51 | understood by many applications (such as Inkscape, Adobe 52 | Illustrator or even web browsers). In a nutshell, SVG files are text files with special 53 | predefined tags (similar to HTML tags). One can open it in a text editor too. 54 | 55 | 3. *Arranging plots into composite figures* 56 | 57 | Using ``svgutils``, one can combine both plots into one figure and add 58 | some annotations (such as one-letter labels: A,B, etc.). The github source code link for ``svgutils`` is available here 59 | `_. 60 | 61 | The basic operations are similar to what one would do in a vector 62 | graphics editor but using scripts instead of a mouse cursor. This reduces repitition as one will not have to repeat the process when, 63 | for some reason, one needs to modify the plots they generated 64 | with matplotlib (to add more data or modify the 65 | parameters of the existing analysis, for example). 66 | 67 | An example script is shown and explained below: 68 | 69 | .. _transform-example-code: 70 | 71 | .. literalinclude:: scripts/fig_final.py 72 | 73 | 4. *Convert to PDF/PNG* 74 | 75 | After running the script, one can convert the output file to a 76 | format of their choice. For this, we suggest using ``inkscape`` which 77 | can produce PNG and PDF files from SVG source. 78 | One can do that directly from command line without the need of opening the whole application:: 79 | 80 | inkscape --export-pdf=fig_final.pdf fig_final.svg 81 | inkscape --export-png=fig_final.png fig_final.svg 82 | 83 | And here is the final result: 84 | 85 | .. figure:: figures/fig_final.png 86 | 87 | Final publication-ready figure. 88 | 89 | If one wishes to re-do the plots, they can simply re-run the 90 | above scripts. Automation of the process by means of a build 91 | system, such as GNU ``make`` or similar is also an option. This part will be covered in 92 | some of the next tutorials from the series. 93 | 94 | Good luck and happy plotting! 95 | 96 | 97 | .. [1] A vector format in contrast to other 98 | (raster) formats such as PNG, JPEG does not represent graphics as 99 | individual pixels, but rather as modifiable objects (lines, circles, 100 | points etc.). They usually offer better qualitiy for publication plots 101 | (PDF files are one of them) and are also editable. 102 | -------------------------------------------------------------------------------- /docs/source/tutorials/scripts/anscombe.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | 4 | Edward Tufte uses this example from Anscombe to show 4 datasets of x 5 | and y that have the same mean, standard deviation, and regression 6 | line, but which are qualitatively different. 7 | 8 | matplotlib fun for a rainy day 9 | 10 | Downloaded from: http://matplotlib.sourceforge.net/examples/pylab_examples/anscombe.html 11 | """ 12 | 13 | from pylab import * 14 | 15 | x = array([10, 8, 13, 9, 11, 14, 6, 4, 12, 7, 5]) 16 | y1 = array([8.04, 6.95, 7.58, 8.81, 8.33, 9.96, 7.24, 4.26, 10.84, 4.82, 5.68]) 17 | y2 = array([9.14, 8.14, 8.74, 8.77, 9.26, 8.10, 6.13, 3.10, 9.13, 7.26, 4.74]) 18 | y3 = array([7.46, 6.77, 12.74, 7.11, 7.81, 8.84, 6.08, 5.39, 8.15, 6.42, 5.73]) 19 | x4 = array([8, 8, 8, 8, 8, 8, 8, 19, 8, 8, 8]) 20 | y4 = array([6.58, 5.76, 7.71, 8.84, 8.47, 7.04, 5.25, 12.50, 5.56, 7.91, 6.89]) 21 | 22 | 23 | def fit(x): 24 | return 3 + 0.5 * x 25 | 26 | 27 | xfit = array([amin(x), amax(x)]) 28 | 29 | subplot(221, frameon=False) 30 | pts, ls1 = plot(x, y1, "ks", xfit, fit(xfit), "r-", lw=2) 31 | ls1.set_visible(False) 32 | axis([2, 20, 2, 14]) 33 | setp(gca(), xticklabels=[], yticks=(4, 8, 12), xticks=(0, 10, 20)) 34 | xticks([]) 35 | yticks([]) 36 | 37 | subplot(222, frameon=False) 38 | pts, ls2 = plot(x, y2, "ks", xfit, fit(xfit), "r-", lw=2) 39 | ls2.set_visible(False) 40 | axis([2, 20, 2, 14]) 41 | setp(gca(), xticklabels=[], yticks=(4, 8, 12), yticklabels=[], xticks=(0, 10, 20)) 42 | xticks([]) 43 | yticks([]) 44 | 45 | subplot(223, frameon=False) 46 | pts, ls3 = plot(x, y3, "ks", xfit, fit(xfit), "r-", lw=2) 47 | ls3.set_visible(False) 48 | axis([2, 20, 2, 14]) 49 | setp(gca(), yticks=(4, 8, 12), xticks=(0, 10, 20)) 50 | xticks([]) 51 | yticks([]) 52 | 53 | subplot(224, frameon=False) 54 | xfit = array([amin(x4), amax(x4)]) 55 | pts, ls4 = plot(x4, y4, "ks", xfit, fit(xfit), "r-", lw=2) 56 | ls4.set_visible(False) 57 | axis([2, 20, 2, 14]) 58 | setp(gca(), yticklabels=[], yticks=(4, 8, 12), xticks=(0, 10, 20)) 59 | xticks([]) 60 | yticks([]) 61 | 62 | # verify the stats 63 | pairs = (x, y1), (x, y2), (x, y3), (x4, y4) 64 | 65 | ls1.set_visible(True) 66 | ls2.set_visible(True) 67 | ls3.set_visible(True) 68 | ls4.set_visible(True) 69 | 70 | savefig("anscombe.png", transparent=True) 71 | savefig("anscombe.svg", transparent=True) 72 | -------------------------------------------------------------------------------- /docs/source/tutorials/scripts/composing_multipanel_figures_examples.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | from svgutils.compose import * 4 | 5 | CONFIG["figure.save_path"] = "composing_multipanel_figures" 6 | 7 | Figure("16cm", "6.5cm", SVG("sigmoid_fit.svg")).save("ex1.svg") 8 | 9 | Figure("16cm", "6.5cm", Text("A", 25, 20), SVG("sigmoid_fit.svg")).save("ex1a.svg") 10 | 11 | Figure( 12 | "16cm", "6.5cm", Text("A", 25, 20, size=12, weight="bold"), SVG("sigmoid_fit.svg") 13 | ).save("ex1b.svg") 14 | 15 | Figure("16cm", "6.5cm", SVG("sigmoid_fit.svg"), SVG("anscombe.svg")).save("ex2.svg") 16 | 17 | Figure("16cm", "6.5cm", SVG("sigmoid_fit.svg"), SVG("anscombe.svg")).tile(2, 1).save( 18 | "ex3.svg" 19 | ) 20 | 21 | Figure("16cm", "6.5cm", SVG("sigmoid_fit.svg"), SVG("anscombe.svg").scale(0.5)).tile( 22 | 2, 1 23 | ).save("ex3b.svg") 24 | 25 | Figure("16cm", "6.5cm", SVG("sigmoid_fit.svg"), SVG("anscombe.svg").move(280, 0)).save( 26 | "ex4.svg" 27 | ) 28 | 29 | Figure( 30 | "16cm", "6.5cm", SVG("sigmoid_fit.svg"), SVG("anscombe.svg").scale(0.5).move(280, 0) 31 | ).save("ex5.svg") 32 | 33 | 34 | Figure( 35 | "16cm", 36 | "6.5cm", 37 | SVG("sigmoid_fit.svg"), 38 | SVG("anscombe.svg").scale(0.5).move(280, 0), 39 | Grid(20, 20), 40 | ).save("ex6.svg") 41 | 42 | 43 | Figure( 44 | "16cm", 45 | "6.5cm", 46 | Panel(Text("A", 25, 20), SVG("sigmoid_fit.svg")), 47 | Panel(Text("B", 25, 20).move(280, 0), SVG("anscombe.svg").scale(0.5).move(280, 0)), 48 | ).save("ex7.svg") 49 | 50 | 51 | Figure( 52 | "16cm", 53 | "6.5cm", 54 | Panel(Text("A", 25, 20), SVG("sigmoid_fit.svg")), 55 | Panel(Text("B", 25, 20), SVG("anscombe.svg").scale(0.5)).move(280, 0), 56 | ).save("ex8.svg") 57 | -------------------------------------------------------------------------------- /docs/source/tutorials/scripts/fig_compose.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | 4 | from svgutils.compose import * 5 | 6 | Figure( 7 | "16cm", 8 | "6.5cm", 9 | Panel(SVG("sigmoid_fit.svg"), Text("A", 25, 20, size=12, weight="bold")), 10 | Panel( 11 | SVG("anscombe.svg").scale(0.5), Text("B", 25, 20, size=12, weight="bold") 12 | ).move(280, 0), 13 | ).save("fig_final_compose.svg") 14 | -------------------------------------------------------------------------------- /docs/source/tutorials/scripts/fig_final.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import svgutils.transform as sg 4 | 5 | # create new SVG figure 6 | fig = sg.SVGFigure("16cm", "6.5cm") 7 | 8 | # load matpotlib-generated figures 9 | fig1 = sg.fromfile("sigmoid_fit.svg") 10 | fig2 = sg.fromfile("anscombe.svg") 11 | 12 | # get the plot objects 13 | plot1 = fig1.getroot() 14 | plot2 = fig2.getroot() 15 | plot2.moveto(280, 0, scale=0.5) 16 | 17 | # add text labels 18 | txt1 = sg.TextElement(25, 20, "A", size=12, weight="bold") 19 | txt2 = sg.TextElement(305, 20, "B", size=12, weight="bold") 20 | 21 | # append plots and labels to figure 22 | fig.append([plot1, plot2]) 23 | fig.append([txt1, txt2]) 24 | 25 | # save generated SVG files 26 | fig.save("fig_final.svg") 27 | -------------------------------------------------------------------------------- /docs/source/tutorials/scripts/sigmoid_fit.py: -------------------------------------------------------------------------------- 1 | from matplotlib import rcParams 2 | 3 | # set plot attributes 4 | fig_width = 5 # width in inches 5 | fig_height = 3 # height in inches 6 | fig_size = [fig_width, fig_height] 7 | params = { 8 | "backend": "Agg", 9 | "axes.labelsize": 8, 10 | "axes.titlesize": 8, 11 | "font.size": 8, 12 | "xtick.labelsize": 8, 13 | "ytick.labelsize": 8, 14 | "figure.figsize": fig_size, 15 | "savefig.dpi": 600, 16 | "font.family": "sans-serif", 17 | "axes.linewidth": 0.5, 18 | "xtick.major.size": 2, 19 | "ytick.major.size": 2, 20 | "font.size": 8, 21 | } 22 | rcParams.update(params) 23 | 24 | 25 | import matplotlib.pyplot as plt 26 | import numpy as np 27 | 28 | 29 | def sigmoid(x): 30 | return 1.0 / (1 + np.exp(-(x - 5))) + 1 31 | 32 | 33 | np.random.seed(1234) 34 | t = np.arange(0.1, 9.2, 0.15) 35 | y = sigmoid(t) + 0.2 * np.random.randn(len(t)) 36 | residuals = y - sigmoid(t) 37 | 38 | t_fitted = np.linspace(0, 10, 100) 39 | 40 | # adjust subplots position 41 | fig = plt.figure() 42 | 43 | ax1 = plt.axes((0.18, 0.20, 0.55, 0.65)) 44 | plt.plot(t, y, "k.", ms=4.0, clip_on=False) 45 | plt.plot(t_fitted, sigmoid(t_fitted), "r-", lw=0.8) 46 | 47 | plt.text( 48 | 5, 49 | 1.0, 50 | r"L = $\frac{1}{1+\exp(-V+5)}+10$", 51 | fontsize=10, 52 | transform=ax1.transData, 53 | clip_on=False, 54 | va="top", 55 | ha="left", 56 | ) 57 | 58 | # set axis limits 59 | ax1.set_xlim((t.min(), t.max())) 60 | ax1.set_ylim((y.min(), y.max())) 61 | 62 | # hide right and top axes 63 | ax1.spines["top"].set_visible(False) 64 | ax1.spines["right"].set_visible(False) 65 | ax1.spines["bottom"].set_position(("outward", 20)) 66 | ax1.spines["left"].set_position(("outward", 30)) 67 | ax1.yaxis.set_ticks_position("left") 68 | ax1.xaxis.set_ticks_position("bottom") 69 | 70 | # set labels 71 | plt.xlabel(r"voltage (V, $\mu$V)") 72 | plt.ylabel("luminescence (L)") 73 | 74 | # make inset 75 | ax_inset = plt.axes((0.2, 0.75, 0.2, 0.2), frameon=False) 76 | plt.hist(residuals, fc="0.8", ec="w", lw=2) 77 | plt.xticks([-0.5, 0, 0.5], [-0.5, 0, 0.5], size=6) 78 | plt.xlim((-0.5, 0.5)) 79 | plt.yticks([5, 10], size=6) 80 | plt.xlabel("residuals", size=6) 81 | ax_inset.xaxis.set_ticks_position("none") 82 | ax_inset.yaxis.set_ticks_position("left") 83 | # plt.hlines([0, 5, 10],-0.6,0.6, lw=1, color='w') 84 | ax_inset.yaxis.grid(lw=1, color="w", ls="-") 85 | plt.text( 86 | 0, 0.9, "frequency", transform=ax_inset.transAxes, va="center", ha="right", size=6 87 | ) 88 | # export to svg 89 | plt.savefig("sigmoid_fit.png", transparent=True) 90 | plt.savefig("sigmoid_fit.svg", transparent=True) 91 | -------------------------------------------------------------------------------- /examples/compose_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | 4 | from svgutils.compose import * 5 | 6 | CONFIG["svg.file_path"] = "files" 7 | CONFIG["image.file_path"] = "files" 8 | 9 | Figure( 10 | "10cm", 11 | "5cm", 12 | SVG("svg_logo.svg").scale(0.2), 13 | Image( 14 | 120, 15 | 120, 16 | "lion.jpeg", 17 | ).move(120, 0), 18 | ).save("compose_example.svg") 19 | -------------------------------------------------------------------------------- /examples/compose_scaling.py: -------------------------------------------------------------------------------- 1 | from svgutils.compose import * 2 | 3 | CONFIG["svg.file_path"] = "files" 4 | CONFIG["image.file_path"] = "files" 5 | 6 | svg1 = SVG("example.svg") 7 | svg2 = SVG("example.svg") 8 | 9 | Figure( 10 | svg1.width + svg2.width, 11 | max(svg1.height, svg2.height), 12 | Panel(svg1.scale(2.0, 1.0), Text("(a)", 8, 18, size=14, font="sans")), 13 | Panel(svg2.scale(1.0, 0.5), Text("(b)", 8, 18, size=14, font="sans")).move( 14 | svg1.width, 0 15 | ), 16 | ).save("compose_scaling.svg") 17 | -------------------------------------------------------------------------------- /examples/files/example.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/files/lion.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btel/svg_utils/5b3e09008181965e3b8b168f4dcda39e8722c568/examples/files/lion.jpeg -------------------------------------------------------------------------------- /examples/files/svg_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SVG Simple Logo Basic 5 | Designed for the SVG Logo Contest in 2006 by Harvey Rayner. It is available under the Creative Commons license for those who have an SVG product or who are using SVG on their site. 6 | 7 | 8 | Creative Commons License 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /examples/stack_plots.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | 7 | from svgutils.templates import VerticalLayout 8 | from svgutils.transform import from_mpl 9 | 10 | layout = VerticalLayout() 11 | 12 | fig1 = plt.figure() 13 | plt.plot([1, 2]) 14 | fig2 = plt.figure() 15 | plt.plot([2, 1]) 16 | 17 | layout.add_figure(from_mpl(fig1)) 18 | layout.add_figure(from_mpl(fig2)) 19 | 20 | layout.save("stack_plots.svg") 21 | -------------------------------------------------------------------------------- /examples/stack_svg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | 4 | from svgutils.templates import ColumnLayout, VerticalLayout 5 | from svgutils.transform import fromfile 6 | 7 | layout = ColumnLayout(5) 8 | 9 | for i in range(12): 10 | svg = fromfile("files/example.svg") 11 | layout.add_figure(svg) 12 | 13 | layout.save("stack_svg.svg") 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | nose 2 | numpydoc 3 | sphinx 4 | sphinx_rtd_theme 5 | matplotlib 6 | lxml 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | 4 | from setuptools import setup 5 | 6 | version_str = "0.3.4" 7 | 8 | setup( 9 | name="svgutils", 10 | version=version_str, 11 | description="Python SVG editor", 12 | long_description="""This is an utility package that helps to edit and 13 | concatenate SVG files. It is especially directed at scientists preparing 14 | final figures for submission to journal. So far it supports arbitrary 15 | placement and scaling of svg figures and 16 | adding markers, such as labels.""", 17 | author="Bartosz Telenczuk", 18 | author_email="bartosz.telenczuk@gmail.com", 19 | url="https://svgutils.readthedocs.io", 20 | packages=["svgutils"], 21 | classifiers=[ 22 | "Development Status :: 4 - Beta", 23 | "Environment :: Console", 24 | "Intended Audience :: Science/Research", 25 | "License :: OSI Approved :: MIT License", 26 | "Operating System :: OS Independent", 27 | "Programming Language :: Python :: 3 :: Only", 28 | "Programming Language :: Python :: 3.9", 29 | "Programming Language :: Python :: 3.8", 30 | "Programming Language :: Python :: 3.7", 31 | "Programming Language :: Python :: 3.6", 32 | "Programming Language :: Python :: 3", 33 | "Topic :: Multimedia :: Graphics :: Editors :: Vector-Based", 34 | "Topic :: Scientific/Engineering :: Visualization", 35 | "Topic :: Text Processing :: Markup", 36 | ], 37 | package_dir={"": "src"}, 38 | python_requires=">=3.6", 39 | install_requires=["lxml"], 40 | download_url="https://github.com/btel/svg_utils/archive/v{}.tar.gz".format( 41 | version_str 42 | ), 43 | ) 44 | -------------------------------------------------------------------------------- /src/svgutils/__init__.py: -------------------------------------------------------------------------------- 1 | from . import compose, transform 2 | -------------------------------------------------------------------------------- /src/svgutils/common.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | class Unit: 5 | """Implementation of SVG units and conversions between them. 6 | 7 | Parameters 8 | ---------- 9 | measure : str 10 | value with unit (for example, '2cm') 11 | """ 12 | 13 | per_inch = {"px": 90, "cm": 2.54, "mm": 25.4, "pt": 72.0, "in": 1.0} 14 | 15 | def __init__(self, measure): 16 | try: 17 | self.value = float(measure) 18 | self.unit = "px" 19 | except ValueError: 20 | m = re.match("([0-9]+\.?[0-9]*)([a-z]+)", measure) 21 | value, unit = m.groups() 22 | self.value = float(value) 23 | self.unit = unit 24 | 25 | def to(self, unit): 26 | """Convert to a given unit. 27 | 28 | Parameters 29 | ---------- 30 | unit : str 31 | Name of the unit to convert to. 32 | 33 | Returns 34 | ------- 35 | u : Unit 36 | new Unit object with the requested unit and computed value. 37 | """ 38 | u = Unit("0cm") 39 | u.value = self.value / self.per_inch[self.unit] * self.per_inch[unit] 40 | u.unit = unit 41 | return u 42 | 43 | def __str__(self): 44 | return "{}{}".format(self.value, self.unit) 45 | 46 | def __repr__(self): 47 | return "Unit({})".format(str(self)) 48 | 49 | def __mul__(self, number): 50 | u = Unit("0cm") 51 | u.value = self.value * number 52 | u.unit = self.unit 53 | return u 54 | 55 | def __truediv__(self, number): 56 | return self * (1.0 / number) 57 | 58 | def __div__(self, number): 59 | return self * (1.0 / number) 60 | -------------------------------------------------------------------------------- /src/svgutils/compose.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | """SVG definitions designed for easy SVG composing 4 | 5 | Features: 6 | * allow for wildcard import 7 | * defines a mini language for SVG composing 8 | * short but readable names 9 | * easy nesting 10 | * method chaining 11 | * no boilerplate code (reading files, extracting objects from svg, 12 | transversing XML tree) 13 | * universal methods applicable to all element types 14 | * don't have to learn python 15 | """ 16 | 17 | import os 18 | 19 | from svgutils import transform as _transform 20 | from svgutils.common import Unit 21 | 22 | CONFIG = { 23 | "svg.file_path": ".", 24 | "figure.save_path": ".", 25 | "image.file_path": ".", 26 | "text.position": (0, 0), 27 | "text.size": 8, 28 | "text.weight": "normal", 29 | "text.font": "Verdana", 30 | } 31 | 32 | 33 | class Element(_transform.FigureElement): 34 | """Base class for new SVG elements.""" 35 | 36 | def rotate(self, angle, x=0, y=0): 37 | """Rotate element by given angle around given pivot. 38 | 39 | Parameters 40 | ---------- 41 | angle : float 42 | rotation angle in degrees 43 | x, y : float 44 | pivot coordinates in user coordinate system (defaults to top-left 45 | corner of the figure) 46 | """ 47 | super(Element, self).rotate(angle, x, y) 48 | return self 49 | 50 | def move(self, x, y): 51 | """Move the element by x, y. 52 | 53 | Parameters 54 | ---------- 55 | x,y : int, str 56 | amount of horizontal and vertical shift 57 | 58 | Notes 59 | ----- 60 | The x, y can be given with a unit (for example, "3px", "5cm"). If no 61 | unit is given the user unit is assumed ("px"). In SVG all units are 62 | defined in relation to the user unit [1]_. 63 | 64 | .. [1] W3C SVG specification: 65 | https://www.w3.org/TR/SVG/coords.html#Units 66 | """ 67 | self.moveto(x, y) 68 | return self 69 | 70 | def find_id(self, element_id): 71 | """Find a single element with the given ID. 72 | 73 | Parameters 74 | ---------- 75 | element_id : str 76 | ID of the element to find 77 | 78 | Returns 79 | ------- 80 | found element 81 | """ 82 | element = _transform.FigureElement.find_id(self, element_id) 83 | return Element(element.root) 84 | 85 | def find_ids(self, element_ids): 86 | """Find elements with given IDs. 87 | 88 | Parameters 89 | ---------- 90 | element_ids : list of strings 91 | list of IDs to find 92 | 93 | Returns 94 | ------- 95 | a new `Panel` object which contains all the found elements. 96 | """ 97 | elements = [_transform.FigureElement.find_id(self, eid) for eid in element_ids] 98 | return Panel(*elements) 99 | 100 | 101 | class SVG(Element): 102 | """SVG from file. 103 | 104 | Parameters 105 | ---------- 106 | fname : str 107 | full path to the file 108 | fix_mpl : bool 109 | replace pt units with px units to fix files created with matplotlib 110 | """ 111 | 112 | def __init__(self, fname=None, fix_mpl=False): 113 | if fname: 114 | fname = os.path.join(CONFIG["svg.file_path"], fname) 115 | svg = _transform.fromfile(fname) 116 | if fix_mpl: 117 | w, h = svg.get_size() 118 | svg.set_size((w.replace("pt", ""), h.replace("pt", ""))) 119 | super(SVG, self).__init__(svg.getroot().root) 120 | 121 | # if height/width is in % units, we can't store the absolute values 122 | if svg.width.endswith("%"): 123 | self._width = None 124 | else: 125 | self._width = Unit(svg.width).to("px") 126 | if svg.height.endswith("%"): 127 | self._height = None 128 | else: 129 | self._height = Unit(svg.height).to("px") 130 | 131 | @property 132 | def width(self): 133 | """Get width of the svg file in px""" 134 | if self._width: 135 | return self._width.value 136 | 137 | @property 138 | def height(self): 139 | """Get height of the svg file in px""" 140 | if self._height: 141 | return self._height.value 142 | 143 | 144 | class MplFigure(SVG): 145 | """Matplotlib figure 146 | 147 | Parameters 148 | ---------- 149 | fig : matplotlib Figure instance 150 | instance of Figure to be converted 151 | kws : 152 | keyword arguments passed to matplotlib's savefig method 153 | """ 154 | 155 | def __init__(self, fig, **kws): 156 | svg = _transform.from_mpl(fig, savefig_kw=kws) 157 | self.root = svg.getroot().root 158 | 159 | 160 | class Image(Element): 161 | """Raster or vector image 162 | 163 | Parameters 164 | ---------- 165 | width : float 166 | height : float 167 | image dimensions 168 | fname : str 169 | full path to the file 170 | """ 171 | 172 | def __init__(self, width, height, fname): 173 | fname = os.path.join(CONFIG["image.file_path"], fname) 174 | _, fmt = os.path.splitext(fname) 175 | fmt = fmt.lower()[1:] 176 | with open(fname, "rb") as fid: 177 | img = _transform.ImageElement(fid, width, height, fmt) 178 | self.root = img.root 179 | 180 | 181 | class Text(Element): 182 | """Text element. 183 | 184 | Parameters 185 | ---------- 186 | text : str 187 | content 188 | x, y : float or str 189 | Text position. If unit is not given it will assume user units (px). 190 | size : float, optional 191 | Font size. 192 | weight : str, optional 193 | Font weight. It can be one of: normal, bold, bolder or lighter. 194 | font : str, optional 195 | Font family. 196 | """ 197 | 198 | def __init__(self, text, x=None, y=None, **kwargs): 199 | params = { 200 | "size": CONFIG["text.size"], 201 | "weight": CONFIG["text.weight"], 202 | "font": CONFIG["text.font"], 203 | } 204 | if x is None or y is None: 205 | x, y = CONFIG["text.position"] 206 | params.update(kwargs) 207 | element = _transform.TextElement(x, y, text, **params) 208 | Element.__init__(self, element.root) 209 | 210 | 211 | class Panel(Element): 212 | """Figure panel. 213 | 214 | Panel is a group of elements that can be transformed together. Usually 215 | it relates to a labeled figure panel. 216 | 217 | Parameters 218 | ---------- 219 | svgelements : objects deriving from Element class 220 | one or more elements that compose the panel 221 | 222 | Notes 223 | ----- 224 | The grouped elements need to be properly arranged in scale and position. 225 | """ 226 | 227 | def __init__(self, *svgelements): 228 | element = _transform.GroupElement(svgelements) 229 | Element.__init__(self, element.root) 230 | 231 | def __iter__(self): 232 | elements = self.root.getchildren() 233 | return (Element(el) for el in elements) 234 | 235 | 236 | class Line(Element): 237 | """Line element connecting given points. 238 | 239 | Parameters 240 | ---------- 241 | points : sequence of tuples 242 | List of point x,y coordinates. 243 | width : float, optional 244 | Line width. 245 | color : str, optional 246 | Line color. Any of the HTML/CSS color definitions are allowed. 247 | """ 248 | 249 | def __init__(self, points, width=1, color="black"): 250 | element = _transform.LineElement(points, width=width, color=color) 251 | Element.__init__(self, element.root) 252 | 253 | 254 | class Grid(Element): 255 | """Line grid with coordinate labels to facilitate placement of new 256 | elements. 257 | 258 | Parameters 259 | ---------- 260 | dx : float 261 | Spacing between the vertical lines. 262 | dy : float 263 | Spacing between horizontal lines. 264 | size : float or str 265 | Font size of the labels. 266 | 267 | Notes 268 | ----- 269 | This element is mainly useful for manual placement of the elements. 270 | """ 271 | 272 | def __init__(self, dx, dy, size=8): 273 | self.size = size 274 | lines = self._gen_grid(dx, dy) 275 | element = _transform.GroupElement(lines) 276 | Element.__init__(self, element.root) 277 | 278 | def _gen_grid(self, dx, dy, width=0.5): 279 | xmax, ymax = 1000, 1000 280 | x, y = 0, 0 281 | lines = [] 282 | txt = [] 283 | while x < xmax: 284 | lines.append(_transform.LineElement([(x, 0), (x, ymax)], width=width)) 285 | txt.append(_transform.TextElement(x, dy / 2, str(x), size=self.size)) 286 | x += dx 287 | while y < ymax: 288 | lines.append(_transform.LineElement([(0, y), (xmax, y)], width=width)) 289 | txt.append(_transform.TextElement(0, y, str(y), size=self.size)) 290 | y += dy 291 | return lines + txt 292 | 293 | 294 | class Figure(Panel): 295 | """Main figure class. 296 | 297 | This should be always the top class of all the generated SVG figures. 298 | 299 | Parameters 300 | ---------- 301 | width, height : float or str 302 | Figure size. If unit is not given, user units (px) are assumed. 303 | """ 304 | 305 | def __init__(self, width, height, *svgelements): 306 | Panel.__init__(self, *svgelements) 307 | self.width = Unit(width) 308 | self.height = Unit(height) 309 | 310 | def save(self, fname): 311 | """Save figure to SVG file. 312 | 313 | Parameters 314 | ---------- 315 | fname : str 316 | Full path to file. 317 | """ 318 | element = _transform.SVGFigure(self.width, self.height) 319 | element.append(self) 320 | element.save(os.path.join(CONFIG["figure.save_path"], fname)) 321 | 322 | def tostr(self): 323 | """Export SVG as a string""" 324 | element = _transform.SVGFigure(self.width, self.height) 325 | element.append(self) 326 | svgstr = element.to_str() 327 | return svgstr 328 | 329 | def _repr_svg_(self): 330 | return self.tostr().decode("ascii") 331 | 332 | def tile(self, ncols, nrows): 333 | """Automatically tile the panels of the figure. 334 | 335 | This will re-arranged all elements of the figure (first in the 336 | hierarchy) so that they will uniformly cover the figure area. 337 | 338 | Parameters 339 | ---------- 340 | ncols, nrows : type 341 | The number of columns and rows to arrange the elements into. 342 | 343 | 344 | Notes 345 | ----- 346 | ncols * nrows must be larger or equal to number of 347 | elements, otherwise some elements will go outside the figure borders. 348 | """ 349 | dx = (self.width / ncols).to("px").value 350 | dy = (self.height / nrows).to("px").value 351 | ix, iy = 0, 0 352 | for el in self: 353 | el.move(dx * ix, dy * iy) 354 | ix += 1 355 | if ix >= ncols: 356 | ix = 0 357 | iy += 1 358 | if iy > nrows: 359 | break 360 | return self 361 | -------------------------------------------------------------------------------- /src/svgutils/templates.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | 4 | from svgutils.transform import GroupElement, SVGFigure 5 | 6 | 7 | class BaseTemplate(SVGFigure): 8 | def __init__(self): 9 | SVGFigure.__init__(self) 10 | self.figures = [] 11 | 12 | def add_figure(self, fig): 13 | w, h = fig.get_size() 14 | root = fig.getroot() 15 | self.figures.append({"root": root, "width": w, "height": h}) 16 | 17 | def _transform(self): 18 | pass 19 | 20 | def save(self, fname): 21 | self._generate_layout() 22 | SVGFigure.save(self, fname) 23 | 24 | def _generate_layout(self): 25 | 26 | for i, f in enumerate(self.figures): 27 | new_element = self._transform(f["root"], self.figures[:i]) 28 | self.append(new_element) 29 | 30 | 31 | class VerticalLayout(BaseTemplate): 32 | def _transform(self, element, transform_list): 33 | for t in transform_list: 34 | element = GroupElement([element]) 35 | element.moveto(0, t["height"]) 36 | return element 37 | 38 | 39 | class ColumnLayout(BaseTemplate): 40 | def __init__(self, nrows, row_height=None, col_width=None): 41 | """Multiple column layout with nrows and required number of 42 | columns. col_width 43 | determines the width of the column (defaults to width of the 44 | first added element)""" 45 | 46 | self.nrows = nrows 47 | self.col_width = col_width 48 | self.row_height = row_height 49 | 50 | BaseTemplate.__init__(self) 51 | 52 | def _transform(self, element, transform_list): 53 | 54 | rows = 0 55 | 56 | if not transform_list: 57 | return element 58 | 59 | n_elements = len(transform_list) 60 | rows = n_elements % self.nrows 61 | cols = int(n_elements / self.nrows) 62 | 63 | if self.col_width is None: 64 | self.col_width = transform_list[0]["width"] 65 | if self.row_height is None: 66 | self.row_height = transform_list[0]["height"] 67 | 68 | for i in range(rows): 69 | element = GroupElement([element]) 70 | element.moveto(0, self.row_height) 71 | 72 | for i in range(cols): 73 | element = GroupElement([element]) 74 | element.moveto(self.col_width, 0) 75 | 76 | return element 77 | -------------------------------------------------------------------------------- /src/svgutils/transform.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | from copy import deepcopy 3 | 4 | from lxml import etree 5 | 6 | try: 7 | from StringIO import StringIO 8 | except ImportError: 9 | from io import StringIO 10 | 11 | from svgutils.common import Unit 12 | 13 | SVG_NAMESPACE = "http://www.w3.org/2000/svg" 14 | XLINK_NAMESPACE = "http://www.w3.org/1999/xlink" 15 | SVG = "{%s}" % SVG_NAMESPACE 16 | XLINK = "{%s}" % XLINK_NAMESPACE 17 | NSMAP = {None: SVG_NAMESPACE, "xlink": XLINK_NAMESPACE} 18 | 19 | 20 | class FigureElement(object): 21 | """Base class representing single figure element""" 22 | 23 | def __init__(self, xml_element, defs=None): 24 | 25 | self.root = xml_element 26 | 27 | def moveto(self, x, y, scale_x=1, scale_y=None): 28 | """Move and scale element. 29 | 30 | Parameters 31 | ---------- 32 | x, y : float 33 | displacement in x and y coordinates in user units ('px'). 34 | scale_x : float 35 | x-direction scaling factor. To scale down scale_x < 1, scale up scale_x > 1. 36 | scale_y : (optional) float 37 | y-direction scaling factor. To scale down scale_y < 1, scale up scale_y > 1. 38 | If set to default (None), then scale_y=scale_x. 39 | """ 40 | if scale_y is None: 41 | scale_y = scale_x 42 | self.root.set( 43 | "transform", 44 | "translate(%s, %s) scale(%s %s) %s" 45 | % (x, y, scale_x, scale_y, self.root.get("transform") or ""), 46 | ) 47 | 48 | def rotate(self, angle, x=0, y=0): 49 | """Rotate element by given angle around given pivot. 50 | 51 | Parameters 52 | ---------- 53 | angle : float 54 | rotation angle in degrees 55 | x, y : float 56 | pivot coordinates in user coordinate system (defaults to top-left 57 | corner of the figure) 58 | """ 59 | self.root.set( 60 | "transform", 61 | "%s rotate(%f %f %f)" % (self.root.get("transform") or "", angle, x, y), 62 | ) 63 | 64 | def skew(self, x=0, y=0): 65 | """Skew the element by x and y degrees 66 | Convenience function which calls skew_x and skew_y 67 | 68 | Parameters 69 | ---------- 70 | x,y : float, float 71 | skew angle in degrees (default 0) 72 | 73 | If an x/y angle is given as zero degrees, that transformation is omitted. 74 | """ 75 | if x != 0: 76 | self.skew_x(x) 77 | if y != 0: 78 | self.skew_y(y) 79 | 80 | return self 81 | 82 | def skew_x(self, x): 83 | """Skew element along the x-axis by the given angle. 84 | 85 | Parameters 86 | ---------- 87 | x : float 88 | x-axis skew angle in degrees 89 | """ 90 | self.root.set( 91 | "transform", "%s skewX(%f)" % (self.root.get("transform") or "", x) 92 | ) 93 | return self 94 | 95 | def skew_y(self, y): 96 | """Skew element along the y-axis by the given angle. 97 | 98 | Parameters 99 | ---------- 100 | y : float 101 | y-axis skew angle in degrees 102 | """ 103 | self.root.set( 104 | "transform", "%s skewY(%f)" % (self.root.get("transform") or "", y) 105 | ) 106 | return self 107 | 108 | def scale(self, x=0, y=None): 109 | """Scale element separately across the two axes x and y. 110 | If y is not provided, it is assumed equal to x (according to the 111 | W3 specification). 112 | 113 | Parameters 114 | ---------- 115 | x : float 116 | x-axis scaling factor. To scale down x < 1, scale up x > 1. 117 | y : (optional) float 118 | y-axis scaling factor. To scale down y < 1, scale up y > 1. 119 | 120 | """ 121 | self.moveto(0, 0, x, y) 122 | return self 123 | 124 | def __getitem__(self, i): 125 | return FigureElement(self.root.getchildren()[i]) 126 | 127 | def copy(self): 128 | """Make a copy of the element""" 129 | return deepcopy(self.root) 130 | 131 | def tostr(self): 132 | """String representation of the element""" 133 | return etree.tostring(self.root, pretty_print=True) 134 | 135 | def find_id(self, element_id): 136 | """Find element by its id. 137 | 138 | Parameters 139 | ---------- 140 | element_id : str 141 | ID of the element to find 142 | 143 | Returns 144 | ------- 145 | FigureElement 146 | one of the children element with the given ID.""" 147 | find = etree.XPath("//*[@id=$id]") 148 | return FigureElement(find(self.root, id=element_id)[0]) 149 | 150 | 151 | class TextElement(FigureElement): 152 | """Text element. 153 | 154 | Corresponds to SVG ```` tag.""" 155 | 156 | def __init__( 157 | self, 158 | x, 159 | y, 160 | text, 161 | size=8, 162 | font="Verdana", 163 | weight="normal", 164 | letterspacing=0, 165 | anchor="start", 166 | color="black", 167 | ): 168 | txt = etree.Element( 169 | SVG + "text", 170 | { 171 | "x": str(x), 172 | "y": str(y), 173 | "font-size": str(size), 174 | "font-family": font, 175 | "font-weight": weight, 176 | "letter-spacing": str(letterspacing), 177 | "text-anchor": str(anchor), 178 | "fill": str(color), 179 | }, 180 | ) 181 | txt.text = text 182 | FigureElement.__init__(self, txt) 183 | 184 | 185 | class ImageElement(FigureElement): 186 | """Inline image element. 187 | 188 | Correspoonds to SVG ```` tag. Image data encoded as base64 string. 189 | """ 190 | 191 | def __init__(self, stream, width, height, format="png"): 192 | base64str = codecs.encode(stream.read(), "base64").rstrip() 193 | uri = "data:image/{};base64,{}".format(format, base64str.decode("ascii")) 194 | attrs = {"width": str(width), "height": str(height), XLINK + "href": uri} 195 | img = etree.Element(SVG + "image", attrs) 196 | FigureElement.__init__(self, img) 197 | 198 | 199 | class LineElement(FigureElement): 200 | """Line element. 201 | 202 | Corresponds to SVG ```` tag. It handles only piecewise 203 | straight segments 204 | """ 205 | 206 | def __init__(self, points, width=1, color="black"): 207 | linedata = "M{} {} ".format(*points[0]) 208 | linedata += " ".join(map(lambda x: "L{} {}".format(*x), points[1:])) 209 | line = etree.Element( 210 | SVG + "path", {"d": linedata, "stroke-width": str(width), "stroke": color} 211 | ) 212 | FigureElement.__init__(self, line) 213 | 214 | 215 | class GroupElement(FigureElement): 216 | """Group element. 217 | 218 | Container for other elements. Corresponds to SVG ```` tag. 219 | """ 220 | 221 | def __init__(self, element_list, attrib=None): 222 | new_group = etree.Element(SVG + "g", attrib=attrib) 223 | for e in element_list: 224 | if isinstance(e, FigureElement): 225 | new_group.append(e.root) 226 | else: 227 | new_group.append(e) 228 | self.root = new_group 229 | 230 | 231 | class SVGFigure(object): 232 | """SVG Figure. 233 | 234 | It setups standalone SVG tree. It corresponds to SVG ```` tag. 235 | """ 236 | 237 | def __init__(self, width=None, height=None): 238 | self.root = etree.Element(SVG + "svg", nsmap=NSMAP) 239 | self.root.set("version", "1.1") 240 | 241 | self._width = 0 242 | self._height = 0 243 | 244 | if width: 245 | self.width = width # this goes to @width.setter a few lines down 246 | 247 | if height: 248 | self.height = height # this goes to @height.setter a few lines down 249 | 250 | @property 251 | def width(self): 252 | """Figure width""" 253 | return self.root.get("width") 254 | 255 | @width.setter 256 | def width(self, value): 257 | if not isinstance(value, Unit): 258 | value = Unit(value) 259 | self._width = value.value 260 | self.root.set("width", str(value)) 261 | self.root.set("viewBox", "0 0 %s %s" % (self._width, self._height)) 262 | 263 | @property 264 | def height(self): 265 | """Figure height""" 266 | return self.root.get("height") 267 | 268 | @height.setter 269 | def height(self, value): 270 | if not isinstance(value, Unit): 271 | value = Unit(value) 272 | self._height = value.value 273 | self.root.set("height", str(value)) 274 | self.root.set("viewBox", "0 0 %s %s" % (self._width, self._height)) 275 | 276 | def append(self, element): 277 | """Append new element to the SVG figure""" 278 | try: 279 | self.root.append(element.root) 280 | except AttributeError: 281 | self.root.append(GroupElement(element).root) 282 | 283 | def getroot(self): 284 | """Return the root element of the figure. 285 | 286 | The root element is a group of elements after stripping the toplevel 287 | ```` tag. 288 | 289 | Returns 290 | ------- 291 | GroupElement 292 | All elements of the figure without the ```` tag. 293 | """ 294 | if "class" in self.root.attrib: 295 | attrib = {"class": self.root.attrib["class"]} 296 | else: 297 | attrib = None 298 | return GroupElement(self.root.getchildren(), attrib=attrib) 299 | 300 | def to_str(self): 301 | """ 302 | Returns a string of the SVG figure. 303 | """ 304 | return etree.tostring( 305 | self.root, xml_declaration=True, standalone=True, pretty_print=True 306 | ) 307 | 308 | def save(self, fname, encoding=None): 309 | """ 310 | Save figure to a file 311 | Default encoding is "ASCII" when None is specified, as dictated by lxml . 312 | """ 313 | out = etree.tostring( 314 | self.root, 315 | xml_declaration=True, 316 | standalone=True, 317 | pretty_print=True, 318 | encoding=encoding, 319 | ) 320 | with open(fname, "wb") as fid: 321 | fid.write(out) 322 | 323 | def find_id(self, element_id): 324 | """Find elements with the given ID""" 325 | find = etree.XPath("//*[@id=$id]") 326 | return FigureElement(find(self.root, id=element_id)[0]) 327 | 328 | def get_size(self): 329 | """Get figure size""" 330 | return self.root.get("width"), self.root.get("height") 331 | 332 | def set_size(self, size): 333 | """Set figure size""" 334 | w, h = size 335 | self.root.set("width", w) 336 | self.root.set("height", h) 337 | 338 | 339 | def fromfile(fname): 340 | """Open SVG figure from file. 341 | 342 | Parameters 343 | ---------- 344 | fname : str 345 | name of the SVG file 346 | 347 | Returns 348 | ------- 349 | SVGFigure 350 | newly created :py:class:`SVGFigure` initialised with the file content 351 | """ 352 | fig = SVGFigure() 353 | with open(fname) as fid: 354 | svg_file = etree.parse(fid, parser=etree.XMLParser(huge_tree=True)) 355 | 356 | fig.root = svg_file.getroot() 357 | return fig 358 | 359 | 360 | def fromstring(text): 361 | """Create a SVG figure from a string. 362 | 363 | Parameters 364 | ---------- 365 | text : str 366 | string representing the SVG content. Must be valid SVG. 367 | 368 | Returns 369 | ------- 370 | SVGFigure 371 | newly created :py:class:`SVGFigure` initialised with the string 372 | content. 373 | """ 374 | fig = SVGFigure() 375 | svg = etree.fromstring(text.encode(), parser=etree.XMLParser(huge_tree=True)) 376 | 377 | fig.root = svg 378 | 379 | return fig 380 | 381 | 382 | def from_mpl(fig, savefig_kw=None): 383 | """Create a SVG figure from a ``matplotlib`` figure. 384 | 385 | Parameters 386 | ---------- 387 | fig : matplotlib.Figure instance 388 | 389 | savefig_kw : dict 390 | keyword arguments to be passed to matplotlib's 391 | `savefig` 392 | 393 | 394 | 395 | Returns 396 | ------- 397 | SVGFigure 398 | newly created :py:class:`SVGFigure` initialised with the string 399 | content. 400 | 401 | 402 | Examples 403 | -------- 404 | 405 | If you want to overlay the figure on another SVG, you may want to pass 406 | the `transparent` option: 407 | 408 | >>> from svgutils import transform 409 | >>> import matplotlib.pyplot as plt 410 | >>> fig = plt.figure() 411 | >>> line, = plt.plot([1,2]) 412 | >>> svgfig = transform.from_mpl(fig, 413 | ... savefig_kw=dict(transparent=True)) 414 | >>> svgfig.getroot() 415 | 416 | 417 | 418 | """ 419 | 420 | fid = StringIO() 421 | if savefig_kw is None: 422 | savefig_kw = {} 423 | 424 | try: 425 | fig.savefig(fid, format="svg", **savefig_kw) 426 | except ValueError: 427 | raise (ValueError, "No matplotlib SVG backend") 428 | fid.seek(0) 429 | fig = fromstring(fid.read()) 430 | 431 | # workaround mpl units bug 432 | w, h = fig.get_size() 433 | fig.set_size((w.replace("pt", ""), h.replace("pt", ""))) 434 | 435 | return fig 436 | -------------------------------------------------------------------------------- /tests/letter_spacing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | 4 | from svgutils import transform 5 | 6 | fig = transform.SVGFigure("100px", "40px") 7 | 8 | txt1 = transform.TextElement("0", "10", "ABCDEFGHIJ", size=12) 9 | txt2 = transform.TextElement("0", "20", "ABCDEFGHIJ", size=12, letterspacing=1) 10 | txt3 = transform.TextElement("0", "30", "ABCDEFGHIJ", size=12, letterspacing=2) 11 | txt4 = transform.TextElement("0", "40", "ABCDEFGHIJ", size=12, letterspacing=-1) 12 | 13 | fig.append([txt1, txt2, txt3, txt4]) 14 | fig.save("letterspacing.svg") 15 | -------------------------------------------------------------------------------- /tests/test_compose.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import hashlib 3 | 4 | from nose.tools import assert_almost_equal, ok_ 5 | 6 | import svgutils.compose as sc 7 | from svgutils.compose import * 8 | from svgutils.transform import SVG, XLINK 9 | 10 | 11 | def test_embedded_svg(): 12 | svg = sc.SVG("examples/files/svg_logo.svg") 13 | fig = sc.Figure("5cm", "5cm", svg) 14 | poly = fig.root.find(".//{}polygon".format(SVG)) 15 | 16 | ok_(poly.get("id") == "V") 17 | 18 | ok_(svg.height is None) 19 | ok_(svg.width is None) 20 | 21 | 22 | def test_embedded_image(): 23 | lion_jpg_md5 = "f4a7c2a05f2acefa50cbd75a32d2733c" 24 | 25 | fig = Figure("5cm", "5cm", Image(120, 120, "examples/files/lion.jpeg")) 26 | image = fig.root.find(SVG + "image") 27 | image_data = image.attrib[XLINK + "href"] 28 | image_data = image_data.replace("data:image/jpeg;base64,", "").encode("ascii") 29 | base64 = codecs.decode(image_data, "base64") 30 | md5 = hashlib.md5(base64).hexdigest() 31 | 32 | ok_(lion_jpg_md5 == md5) 33 | 34 | 35 | def test_text(): 36 | 37 | fig = Figure("5cm", "5cm", Text("lion")) 38 | txt = fig.root.find(SVG + "text") 39 | 40 | ok_(txt.text == "lion") 41 | 42 | 43 | def test_no_unit(): 44 | """no unit defaults to px""" 45 | 46 | no_unit = Unit(10) 47 | assert no_unit.unit == "px" 48 | assert no_unit.value == 10.0 49 | 50 | 51 | def test_units(): 52 | """test unit parsing""" 53 | 54 | length = Unit("10cm") 55 | assert length.unit == "cm" 56 | assert length.value == 10 57 | 58 | length = Unit("10.5cm") 59 | assert length.unit == "cm" 60 | assert length.value == 10.5 61 | 62 | 63 | def test_unit_div(): 64 | """test divding a number with unit by a number""" 65 | 66 | length = Unit("10cm") 67 | shorter_length = length / 2 68 | assert length.unit == "cm" 69 | assert_almost_equal(shorter_length.value, 5) 70 | 71 | shorter_length = length / 2.0 72 | assert length.unit == "cm" 73 | assert_almost_equal(shorter_length.value, 5.0) 74 | -------------------------------------------------------------------------------- /tests/test_transform.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | from tempfile import NamedTemporaryFile 4 | 5 | from nose.tools import ok_ 6 | 7 | from svgutils import transform 8 | from svgutils.compose import Unit 9 | 10 | circle = """ 11 | 13 | 14 | 15 | 17 | 18 | """ 19 | 20 | 21 | def test_get_size(): 22 | svg_fig = transform.fromstring(circle) 23 | w, h = svg_fig.get_size() 24 | ok_((w == "150") & (h == "50")) 25 | 26 | 27 | def test_group_class(): 28 | svg_fig = transform.fromstring(circle) 29 | group = svg_fig.getroot() 30 | ok_((group.root.attrib["class"] == "main")) 31 | 32 | 33 | def test_skew(): 34 | svg_fig = transform.fromstring(circle) 35 | group = svg_fig.getroot() 36 | 37 | # Test skew in y-axis 38 | group.skew(0, 30) 39 | ok_("skewY(30" in group.root.get("transform")) 40 | 41 | # Test skew in x-axis 42 | group.skew(30, 0) 43 | ok_("skewX(30" in group.root.get("transform")) 44 | 45 | 46 | def test_scale_xy(): 47 | svg_fig = transform.fromstring(circle) 48 | group = svg_fig.getroot() 49 | 50 | group.scale(0, 30) 51 | ok_("scale(0" in group.root.get("transform")) 52 | 53 | 54 | def test_create_svg_figure(): 55 | svg_fig = transform.SVGFigure() 56 | assert "svg" in svg_fig.to_str().decode("ascii") 57 | 58 | svg_fig = transform.SVGFigure(Unit("2px"), Unit("2px")) 59 | assert "svg" in svg_fig.to_str().decode("ascii") 60 | 61 | svg_fig = transform.SVGFigure(2, 3) 62 | assert "svg" in svg_fig.to_str().decode("ascii") 63 | 64 | svg_fig = transform.SVGFigure("2", "3") 65 | assert "svg" in svg_fig.to_str().decode("ascii") 66 | 67 | 68 | def test_svg_figure_writes_width_height_and_view_box(): 69 | svg_fig = transform.SVGFigure(Unit("400mm"), Unit("300mm")) 70 | 71 | with NamedTemporaryFile() as f1: 72 | svg_fig.save(f1.name) 73 | with open(f1.name) as f2: 74 | written_content = f2.read() 75 | 76 | assert 'width="400.0mm"' in written_content 77 | assert 'height="300.0mm"' in written_content 78 | assert 'viewBox="0 0 400.0 300.0"' in written_content 79 | 80 | 81 | def test_svg_figure__width_height_tostr(): 82 | 83 | svg_fig = transform.SVGFigure("400px", "300px") 84 | assert b'height="300.0px"' in svg_fig.to_str() 85 | assert b'width="400.0px"' in svg_fig.to_str() 86 | --------------------------------------------------------------------------------