# soda - a fast SVG generation tool
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/evtn/soda/build.yml?branch=lord)](https://github.com/evtn/soda/actions/workflows/build.yml)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/evtn/soda/test.yml?branch=lord&label=tests)](https://github.com/evtn/soda/actions/workflows/test.yml)
[![PyPI](https://img.shields.io/pypi/v/soda-svg)](https://pypi.org/project/soda-svg/)
![PyPI - Downloads](https://pepy.tech/badge/soda-svg)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/soda-svg)
[![Coveralls](https://img.shields.io/coverallsCoverage/github/evtn/soda?label=test%20coverage)](https://coveralls.io/github/evtn/soda?branch=lord)
![License](https://img.shields.io/github/license/evtn/soda)
[![wordstreamer badge](https://img.shields.io/badge/renderable-what?label=wordstreamer&color=%2333bb33)](https://github.com/evtn/wordstreamer)
![Badge Count](https://img.shields.io/badge/badges-9-important)
Here's some basic usage:
> [!NOTE]
> Since soda 2.0.0, a Tag instance is also a [wordstreamer.Renderable](https://github.com/evtn/wordstreamer), which makes it possible to use things like:
>
> ```python
> from wordstreamer import Renderer
> from soda import Tag
>
> byte_stream = Renderer().byte_stream(Tag.rect()) # Iterable[bytes]
> ```
```python
from soda import Tag, Root
# root is a custom svg tag
root = Root(viewBox="0 0 10 10")(
Tag.rect(width=10, height=10, fill="#ff5"),
Tag.circle(cx=5, cy=5, r=4, fill="#222")
)
print(root.render(pretty=True))
```
## Installation
Install `soda-svg` from PyPI, like `python -m pip install soda-svg`.
Note that `soda` on PyPI is a different package.
## Tag construction
The main class of the module is `Tag`. You can create it with a constructor:
```python
Tag("g")
```
or with a shorthand:
```python
Tag.g
```
You can also pass children and attributes into the constructor:
```python
Tag("g", child1, child2, attr1=1, attr2=2)
```
or as call arguments (this would change tag in place, no additional copies):
```python
Tag("g")(child1, attr1=1)(child2, child3, attr2=10, attr3=3)
# or
Tag.g(child1, attr1=1)(child2, child3, attr2=10, attr3=3)
# '_' to '-' conversion
Tag.g(child1, attr_1=1)(child2, child3, attr_2=10, attr_3=3) # <g attr-1="1" attr-2="10" attr-3="3" >child1child2child3</g>
```
## Float rounding
As floats can be used as attribute values (even though in resulting SVG every value is a string), module will round floats to 3 digits after the decimal point:
```python
from soda import Tag
g = Tag.g(x=1/3)
print(g.render()) # '<g x="0.333"/>'
```
To change this behaviour, edit `soda.config.decimal_length` before value is assigned:
```python
from soda import Tag, config as soda_config
soda_config.decimal_length = 4
g = Tag.g(x=1/3)
print(g.render()) # '<g x="0.3333"/>'
soda_config.decimal_length = 2
g(y=1/3)
print(g.render()) # '<g x="0.3333" y="0.33"/>'
```
## Attribute conversion
For convenience, leading and trailing underscores are removed by default, and underscores in the middle of words are replaced with hyphens:
```python
from soda import Tag
g = Tag.g(cla_ss_="test")
print(g.render()) # <g cla-ss="test"/>
```
To disable replacing behavior, use `config` class:
```python
from soda import Tag, config as soda_config
soda_config.replace_underscores = False
g = Tag.g(cla_ss_="test")
print(g.render()) # <g cla_ss="test"/>
```
...and to disable stripping of leading/traililng underscores:
```python
from soda import Tag, config as soda_config
soda_config.strip_underscores = False
g = Tag.g(cla_ss_="test")
print(g.render()) # <g cla-ss_="test"/>
```
It's important to do that before tag creation, as all conversions are happening at the tag creation time:
```python
from soda import Tag, config as soda_config
g1 = Tag.g(cla_ss_="test") # g.attributes == {"cla-ss": "test"}
soda_config.replace_underscores = False
g2 = Tag.g(cla_ss_="test") # g.attributes == {"cla_ss_": "test"}
print(g1.render()) # <g cla-ss="test"/>
print(g2.render()) # <g cla_ss_="test"/>
```
## Creating a Tag from XML string
_new in 1.1.0_
You can use `Tag.from_str(xml_string)` to parse an XML document in that string.
Note that currently this doesn't preserve any comments or declarations of original document.
```python
from soda import Tag, Root
root = Root(viewBox="0 0 10 10")(
Tag.rect(width=10, height=10, fill="#ff5"),
Tag.circle(cx=5, cy=5, r=4, fill="#222")
)
rendered_root = root.render(pretty=True)
new_root = Tag.from_str(rendered_root)
assert rendered_root == new_root.render(pretty=True)
```
## Text
Basic text handling is pretty straightforward:
```python
from soda import Tag
Tag.text("Hello, World") # just pass a string as a children
```
This code is roughly equivalent to:
```python
from soda import Tag, Literal
Tag.text(Literal("Hello, World"))
```
...except that first piece doesn't create a `Literal` object.
If you need to add unescaped text (such as prerendered XML), you should pass `escape=False` to a `Literal` constructor:
## XML Declarations and comments
To insert an XML declaration (i.e. `<?xml version="1.0" encoding="UTF-8"?>`), use `XMLDeclaration`:
```python
from soda import XMLDeclaration
print(XMLDeclaration(version="2.0", encoding="UTF-8").render()) # '<?xml version="2.0" encoding="UTF-8"?>'
```
Default values for version and encoding are "1.0" and "UTF-8" respectively
XML comments are used similarly:
```python
from soda import XMLComment
print(XMLComment("comment text!!").render()) # '<!-- comment text!! -->'
```
```python
from soda import Tag, Literal
Tag.g(Literal('<path d="M0 0 L10 0 Z"/>', escape=False))
```
## Accessing data
`tag[attr]` syntax can be used to manage tag attributes (where `attr` should be a string).
```python
from soda import Tag
tag = Tag.g
tag["id"] = "yes-thats-an-id" # sets attribute
tag["cool"] = None # deletes attribute if exists, otherwise does nothing
print(tag["id"]) # prints attribute
print(tag["non-existent-attribute"]) # prints None
```
`tag[index]` syntax can be used to manage tag children (where `index` should be either integer or slice).
```python
from soda import Tag
tag = Tag.g(Tag.a)
tag[0]["href"] = "https://github.com/evtn/soda"
print(tag[1]) # IndexError
print(tag[0]) # prints <a href="https://github.com/evtn/soda" />
```
~~Children can also be accessed directly through `tag.children` attribute.~~
This is not necessary for most tasks as of 1.1.6 with new methods and iterator protocol:
- `Tag.insert(int, Node)` inserts a node on an index. Be aware that `tag.children` is not flattened: `Tag.g("test", ["test1", "test2"]).insert(2, elem)` will insert `elem` _after_ the array.
- `Tag.append(Node)` appends one node to the tag.
- `Tag.extend(*Node)` appends several nodes to the tag. *This is not the same as `.append([*Node])`\*
- `Tag.pop(int?)` pops one node from specified index. If index is not provided, pops the last one.
- `Tag.iter_raw()` returns an iterable to get every Node of the tag. This doesn't dive into nested arrays, for that behaviour iterate over `Tag`
- You can also iterate over the `Tag` itself to get every flat node of it (no arrays)
## Fragments
Fragments use concept similar to React's fragment. It renders just it's children:
```python
from soda import Tag, Fragment
tag = Tag.g(
Fragment(Tag.a, Tag.a)
)
print(tag) # <g><a/><a/></g>
```
## Paths
_new in 0.1.7_
There is a builder for SVG path commands in soda:
<svg viewBox="0 0 100 100">
<rect width="100%" height="100%" fill="white"/>
<path d="M 10,30
A 20,20 0,0,1 50,30
A 20,20 0,0,1 90,30
Q 90,60 50,90
Q 10,60 10,30 z"
/>
</svg>
You can build a list of path commands using descriptive command names:
```python
from soda import Tag, Root, Path
commands = (
Path.moveto(x=10, y=30),
Path.arc(
radius_x=20,
radius_y=20,
# for convenience, omitted arguments
# (here: x_axis_rotation and large_arc_flag) are set to 0
sweep_flag=1,
x=50,
y=30,
),
Path.arc(
radius_x=20,
radius_y=20,
sweep_flag=1,
x=90,
y=30,
),
Path.quadratic(
x1=90,
y1=60,
x=50,
y=90,
),
Path.quadratic(
x1=10,
y1=60,
x=10,
y=30,
),
Path.close()
)
```
...or using common SVG command names (letter case signifies if command is relative):
```python
# or
commands = (
Path.M(10, 30),
Path.A(20, 20, 0, 0, 1, 50, 30),
Path.A(20, 20, 0, 0, 1, 50, 30),
Path.Q(90, 60, 50, 90),
Path.Q(10, 60, 10, 30),
Path.Z()
)
```
...and render it with `Path.build(*commands, compact=False)` method
```python
root = Root(
viewBox="0 0 100 100",
use_namespace=True,
)(
Tag.rect(width="100%", height="100%", fill="white"),
Tag.path()(
d=Path.build(*commands)
)
)
print(root.render(pretty=True))
"""
yields:
<svg
viewBox="0 0 100 100"
version="2.0"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<rect
width="100%"
height="100%"
fill="white"
/>
<path
d="M 10 30 A 20 20 0 0 1 50 30 A 20 20 0 0 1 50 30 Q 90 60 50 90 Q 10 60 10 30 Z"
/>
</svg>
"""
```
You can also optimize resulting path with `compact` argument:
```python
print(Path.build(*commands, compact=True))
# prints M10 30A20 20 0 0 1 50 30A20 20 0 0 1 50 30Q90 60 50 90Q10 60 10 30Z
```
## Points
_new in 1.1.0_
To work with coordinates, you can use `Point` class:
```python
from soda import Point
a = Point(1, 2)
b = Point(4, 5)
```
In any context where a point could be used, "point-like" values can be used:
- `(1, 2)` <-> `Point(1, 2)`
- `[1, 2]` <-> `Point(1, 2)`
- `1` <-> `Point(1, 1)`
You can use coordinates of a point:
```python
print(a) # Point[1, 2]
print(a.x) # 1
print(a.coords) # (1, 2)
print([*a]) # [1, 2] (same as [*a.coords])
```
...perform mathematical operations on points:
```python
print(a + b) # Point[5, 7]
print(a - b) # Point[-3, -3]
print(a * b) # Point[4, 10] (a.x * b.x, a.y * b.y)
print(a / b) # Point[0.25, 0.4]
print(a % b) # Point[1, 2]
```
...and any point-like values:
```python
print(a + 10) # Point[11, 12]
print(a * 2) # Point[2, 4]
```
You also can calculate distance between points and rotate a point around some point:
```python
from math import pi
print(a.distance(b)) # 4.242640687119285
print(a.distance()) # 2.23606797749979 (distance between a and (0, 0), basically the length of a vector)
print(a.rotate(degrees=90)) # Point[-2, 1]
print(a.rotate((10, 10), degrees=90))
print(a.rotate((10, 10), radians=pi / 2)) # Point[18, 1]
```
...and get a normalized vector:
```python
print(a.normalized()) # Point[0.4472135954999579, 0.8944271909999159]
```
You also can get an angle (in radians) between two vectors (with specified starting point):
```python
print(a.angle(b)) # 0.21109333322274684
print(a.angle(b, (10, 10))) # 0.03190406448501816 (second argument specifies starting point (0, 0) by default)
print(a.angle()) # 1.1071487177940904 (angle between `a` and (1, 0) vector)
print(
a.angle(
a.rotate(radians=2)
)
) # 2
```
### Using as attributes
`Point.as_` provides a convenient way of using points as tag attributes:
```python
from soda import Tag, Point
size = Point(100, 200)
print(size.as_()) # {"x": 100, "y": 200}
print(size.as_("width", "height")) # {"width": 100, "height": 200}
print(
Tag.rect(
**size.as_("width", "height")
)
) # <rect width="100" height="200"/>
```
### PointPath
A version of [`Path`](#paths) accepting `Point` instead of some arguments.
Where Path.something(...) accepts coordinates (as two arguments) or some size (like `radius_x` and `radius_y` in `arc`), `PointPath` accepts a point-like object instead.
## Custom components
You can build custom components, using different approaches:
### Building a tree on init
Builds a tree on every component creation
```python
from soda import Tag, Fragment
class CustomComponent(Fragment):
def __init__(self):
children = Tag.g(
Tag.anythingother,
Tag.lalala(
Tag.yes,
Tag.no
)
)
super().__init__(*children)
CustomComponent().render()
```
### Functional approach
Builds a tree on every call
```python
from soda import Tag
def custom_component():
return Tag.g(
Tag.anythingother,
Tag.lalala(
Tag.yes,
Tag.no
)
)
custom_component().render()
```
## Speed
soda is able to render tens of thousands tags per second, but if you wanna optimize your execution, there are some tips:
### Building a tree efficiently
If you using the same structure many times (especially if it's a heavy one), avoid rebuilds. Rather than building a new tree every time, consider changing specific parts of it when needed. It won't speed up the render time, though (check Prerendering right below for that)
### Prerendering
If you have some static tags, you can use `tag.prerender()` to get a prerendered `Literal`.
This could speed up your render significantly in some complex cases.
Raw data
{
"_id": null,
"home_page": "https://github.com/evtn/soda",
"name": "soda-svg",
"maintainer": null,
"docs_url": null,
"requires_python": "<3.13,>=3.8",
"maintainer_email": null,
"keywords": "soda, svg, xml",
"author": "Dmitry Gritsenko",
"author_email": "soda@evtn.ru",
"download_url": "https://files.pythonhosted.org/packages/eb/8d/9a2b7885caa68f1bf533f8748a7db9a6526d263579cdb1ae60ae4ce3be74/soda_svg-2.0.3.tar.gz",
"platform": null,
"description": "# soda - a fast SVG generation tool\n\n[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/evtn/soda/build.yml?branch=lord)](https://github.com/evtn/soda/actions/workflows/build.yml)\n[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/evtn/soda/test.yml?branch=lord&label=tests)](https://github.com/evtn/soda/actions/workflows/test.yml)\n[![PyPI](https://img.shields.io/pypi/v/soda-svg)](https://pypi.org/project/soda-svg/)\n![PyPI - Downloads](https://pepy.tech/badge/soda-svg)\n![PyPI - Python Version](https://img.shields.io/pypi/pyversions/soda-svg)\n[![Coveralls](https://img.shields.io/coverallsCoverage/github/evtn/soda?label=test%20coverage)](https://coveralls.io/github/evtn/soda?branch=lord)\n![License](https://img.shields.io/github/license/evtn/soda)\n[![wordstreamer badge](https://img.shields.io/badge/renderable-what?label=wordstreamer&color=%2333bb33)](https://github.com/evtn/wordstreamer)\n![Badge Count](https://img.shields.io/badge/badges-9-important)\n\nHere's some basic usage:\n\n> [!NOTE]\n> Since soda 2.0.0, a Tag instance is also a [wordstreamer.Renderable](https://github.com/evtn/wordstreamer), which makes it possible to use things like:\n>\n> ```python\n> from wordstreamer import Renderer\n> from soda import Tag\n>\n> byte_stream = Renderer().byte_stream(Tag.rect()) # Iterable[bytes]\n> ```\n\n```python\nfrom soda import Tag, Root\n\n# root is a custom svg tag\nroot = Root(viewBox=\"0 0 10 10\")(\n Tag.rect(width=10, height=10, fill=\"#ff5\"),\n Tag.circle(cx=5, cy=5, r=4, fill=\"#222\")\n)\n\nprint(root.render(pretty=True))\n```\n\n## Installation\n\nInstall `soda-svg` from PyPI, like `python -m pip install soda-svg`.\n\nNote that `soda` on PyPI is a different package.\n\n## Tag construction\n\nThe main class of the module is `Tag`. You can create it with a constructor:\n\n```python\nTag(\"g\")\n```\n\nor with a shorthand:\n\n```python\nTag.g\n```\n\nYou can also pass children and attributes into the constructor:\n\n```python\nTag(\"g\", child1, child2, attr1=1, attr2=2)\n```\n\nor as call arguments (this would change tag in place, no additional copies):\n\n```python\nTag(\"g\")(child1, attr1=1)(child2, child3, attr2=10, attr3=3)\n# or\nTag.g(child1, attr1=1)(child2, child3, attr2=10, attr3=3)\n\n# '_' to '-' conversion\nTag.g(child1, attr_1=1)(child2, child3, attr_2=10, attr_3=3) # <g attr-1=\"1\" attr-2=\"10\" attr-3=\"3\" >child1child2child3</g>\n```\n\n## Float rounding\n\nAs floats can be used as attribute values (even though in resulting SVG every value is a string), module will round floats to 3 digits after the decimal point:\n\n```python\nfrom soda import Tag\n\ng = Tag.g(x=1/3)\nprint(g.render()) # '<g x=\"0.333\"/>'\n\n```\n\nTo change this behaviour, edit `soda.config.decimal_length` before value is assigned:\n\n```python\nfrom soda import Tag, config as soda_config\n\nsoda_config.decimal_length = 4\n\ng = Tag.g(x=1/3)\nprint(g.render()) # '<g x=\"0.3333\"/>'\n\nsoda_config.decimal_length = 2\ng(y=1/3)\n\nprint(g.render()) # '<g x=\"0.3333\" y=\"0.33\"/>'\n\n```\n\n## Attribute conversion\n\nFor convenience, leading and trailing underscores are removed by default, and underscores in the middle of words are replaced with hyphens:\n\n```python\nfrom soda import Tag\n\ng = Tag.g(cla_ss_=\"test\")\n\nprint(g.render()) # <g cla-ss=\"test\"/>\n\n```\n\nTo disable replacing behavior, use `config` class:\n\n```python\nfrom soda import Tag, config as soda_config\n\nsoda_config.replace_underscores = False\n\ng = Tag.g(cla_ss_=\"test\")\n\nprint(g.render()) # <g cla_ss=\"test\"/>\n\n```\n\n...and to disable stripping of leading/traililng underscores:\n\n```python\nfrom soda import Tag, config as soda_config\n\nsoda_config.strip_underscores = False\n\ng = Tag.g(cla_ss_=\"test\")\n\nprint(g.render()) # <g cla-ss_=\"test\"/>\n```\n\nIt's important to do that before tag creation, as all conversions are happening at the tag creation time:\n\n```python\n\nfrom soda import Tag, config as soda_config\n\ng1 = Tag.g(cla_ss_=\"test\") # g.attributes == {\"cla-ss\": \"test\"}\n\nsoda_config.replace_underscores = False\n\ng2 = Tag.g(cla_ss_=\"test\") # g.attributes == {\"cla_ss_\": \"test\"}\n\nprint(g1.render()) # <g cla-ss=\"test\"/>\nprint(g2.render()) # <g cla_ss_=\"test\"/>\n\n```\n\n## Creating a Tag from XML string\n\n_new in 1.1.0_\n\nYou can use `Tag.from_str(xml_string)` to parse an XML document in that string. \nNote that currently this doesn't preserve any comments or declarations of original document.\n\n```python\nfrom soda import Tag, Root\n\nroot = Root(viewBox=\"0 0 10 10\")(\n Tag.rect(width=10, height=10, fill=\"#ff5\"),\n Tag.circle(cx=5, cy=5, r=4, fill=\"#222\")\n)\n\nrendered_root = root.render(pretty=True)\nnew_root = Tag.from_str(rendered_root)\n\nassert rendered_root == new_root.render(pretty=True)\n```\n\n## Text\n\nBasic text handling is pretty straightforward:\n\n```python\nfrom soda import Tag\n\nTag.text(\"Hello, World\") # just pass a string as a children\n```\n\nThis code is roughly equivalent to:\n\n```python\nfrom soda import Tag, Literal\n\nTag.text(Literal(\"Hello, World\"))\n```\n\n...except that first piece doesn't create a `Literal` object.\n\nIf you need to add unescaped text (such as prerendered XML), you should pass `escape=False` to a `Literal` constructor:\n\n## XML Declarations and comments\n\nTo insert an XML declaration (i.e. `<?xml version=\"1.0\" encoding=\"UTF-8\"?>`), use `XMLDeclaration`:\n\n```python\nfrom soda import XMLDeclaration\n\n\nprint(XMLDeclaration(version=\"2.0\", encoding=\"UTF-8\").render()) # '<?xml version=\"2.0\" encoding=\"UTF-8\"?>'\n```\n\nDefault values for version and encoding are \"1.0\" and \"UTF-8\" respectively\n\nXML comments are used similarly:\n\n```python\nfrom soda import XMLComment\n\n\nprint(XMLComment(\"comment text!!\").render()) # '<!-- comment text!! -->'\n```\n\n```python\nfrom soda import Tag, Literal\n\nTag.g(Literal('<path d=\"M0 0 L10 0 Z\"/>', escape=False))\n```\n\n## Accessing data\n\n`tag[attr]` syntax can be used to manage tag attributes (where `attr` should be a string).\n\n```python\nfrom soda import Tag\n\ntag = Tag.g\ntag[\"id\"] = \"yes-thats-an-id\" # sets attribute\ntag[\"cool\"] = None # deletes attribute if exists, otherwise does nothing\nprint(tag[\"id\"]) # prints attribute\nprint(tag[\"non-existent-attribute\"]) # prints None\n```\n\n`tag[index]` syntax can be used to manage tag children (where `index` should be either integer or slice).\n\n```python\nfrom soda import Tag\n\ntag = Tag.g(Tag.a)\ntag[0][\"href\"] = \"https://github.com/evtn/soda\"\nprint(tag[1]) # IndexError\nprint(tag[0]) # prints <a href=\"https://github.com/evtn/soda\" />\n```\n\n~~Children can also be accessed directly through `tag.children` attribute.~~\n\nThis is not necessary for most tasks as of 1.1.6 with new methods and iterator protocol:\n\n- `Tag.insert(int, Node)` inserts a node on an index. Be aware that `tag.children` is not flattened: `Tag.g(\"test\", [\"test1\", \"test2\"]).insert(2, elem)` will insert `elem` _after_ the array.\n- `Tag.append(Node)` appends one node to the tag.\n- `Tag.extend(*Node)` appends several nodes to the tag. *This is not the same as `.append([*Node])`\\*\n- `Tag.pop(int?)` pops one node from specified index. If index is not provided, pops the last one.\n- `Tag.iter_raw()` returns an iterable to get every Node of the tag. This doesn't dive into nested arrays, for that behaviour iterate over `Tag`\n- You can also iterate over the `Tag` itself to get every flat node of it (no arrays)\n\n## Fragments\n\nFragments use concept similar to React's fragment. It renders just it's children:\n\n```python\nfrom soda import Tag, Fragment\n\ntag = Tag.g(\n Fragment(Tag.a, Tag.a)\n)\nprint(tag) # <g><a/><a/></g>\n\n```\n\n## Paths\n\n_new in 0.1.7_\n\nThere is a builder for SVG path commands in soda:\n\n<svg viewBox=\"0 0 100 100\">\n <rect width=\"100%\" height=\"100%\" fill=\"white\"/>\n <path d=\"M 10,30\n A 20,20 0,0,1 50,30\n A 20,20 0,0,1 90,30\n Q 90,60 50,90\n Q 10,60 10,30 z\"\n />\n</svg>\n\nYou can build a list of path commands using descriptive command names:\n\n```python\nfrom soda import Tag, Root, Path\n\ncommands = (\n Path.moveto(x=10, y=30),\n Path.arc(\n radius_x=20,\n radius_y=20,\n # for convenience, omitted arguments\n # (here: x_axis_rotation and large_arc_flag) are set to 0\n sweep_flag=1,\n x=50,\n y=30,\n ),\n Path.arc(\n radius_x=20,\n radius_y=20,\n sweep_flag=1,\n x=90,\n y=30,\n ),\n Path.quadratic(\n x1=90,\n y1=60,\n x=50,\n y=90,\n ),\n Path.quadratic(\n x1=10,\n y1=60,\n x=10,\n y=30,\n ),\n Path.close()\n)\n\n\n```\n\n...or using common SVG command names (letter case signifies if command is relative):\n\n```python\n\n# or\n\ncommands = (\n Path.M(10, 30),\n Path.A(20, 20, 0, 0, 1, 50, 30),\n Path.A(20, 20, 0, 0, 1, 50, 30),\n Path.Q(90, 60, 50, 90),\n Path.Q(10, 60, 10, 30),\n Path.Z()\n)\n\n```\n\n...and render it with `Path.build(*commands, compact=False)` method\n\n```python\n\nroot = Root(\n viewBox=\"0 0 100 100\",\n use_namespace=True,\n)(\n Tag.rect(width=\"100%\", height=\"100%\", fill=\"white\"),\n Tag.path()(\n d=Path.build(*commands)\n )\n)\n\nprint(root.render(pretty=True))\n\n\"\"\"\nyields:\n\n<svg\n viewBox=\"0 0 100 100\"\n version=\"2.0\"\n xmlns=\"http://www.w3.org/2000/svg\"\n xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n>\n <rect\n width=\"100%\"\n height=\"100%\"\n fill=\"white\"\n />\n <path\n d=\"M 10 30 A 20 20 0 0 1 50 30 A 20 20 0 0 1 50 30 Q 90 60 50 90 Q 10 60 10 30 Z\"\n />\n</svg>\n\"\"\"\n```\n\nYou can also optimize resulting path with `compact` argument:\n\n```python\nprint(Path.build(*commands, compact=True))\n# prints M10 30A20 20 0 0 1 50 30A20 20 0 0 1 50 30Q90 60 50 90Q10 60 10 30Z\n```\n\n## Points\n\n_new in 1.1.0_\n\nTo work with coordinates, you can use `Point` class:\n\n```python\nfrom soda import Point\n\na = Point(1, 2)\nb = Point(4, 5)\n```\n\nIn any context where a point could be used, \"point-like\" values can be used:\n\n- `(1, 2)` <-> `Point(1, 2)`\n- `[1, 2]` <-> `Point(1, 2)`\n- `1` <-> `Point(1, 1)`\n\nYou can use coordinates of a point:\n\n```python\nprint(a) # Point[1, 2]\nprint(a.x) # 1\nprint(a.coords) # (1, 2)\nprint([*a]) # [1, 2] (same as [*a.coords])\n```\n\n...perform mathematical operations on points:\n\n```python\nprint(a + b) # Point[5, 7]\nprint(a - b) # Point[-3, -3]\nprint(a * b) # Point[4, 10] (a.x * b.x, a.y * b.y)\nprint(a / b) # Point[0.25, 0.4]\nprint(a % b) # Point[1, 2]\n```\n\n...and any point-like values:\n\n```python\nprint(a + 10) # Point[11, 12]\nprint(a * 2) # Point[2, 4]\n```\n\nYou also can calculate distance between points and rotate a point around some point:\n\n```python\nfrom math import pi\n\nprint(a.distance(b)) # 4.242640687119285\nprint(a.distance()) # 2.23606797749979 (distance between a and (0, 0), basically the length of a vector)\nprint(a.rotate(degrees=90)) # Point[-2, 1]\nprint(a.rotate((10, 10), degrees=90))\nprint(a.rotate((10, 10), radians=pi / 2)) # Point[18, 1]\n```\n\n...and get a normalized vector:\n\n```python\nprint(a.normalized()) # Point[0.4472135954999579, 0.8944271909999159]\n```\n\nYou also can get an angle (in radians) between two vectors (with specified starting point):\n\n```python\nprint(a.angle(b)) # 0.21109333322274684\nprint(a.angle(b, (10, 10))) # 0.03190406448501816 (second argument specifies starting point (0, 0) by default)\nprint(a.angle()) # 1.1071487177940904 (angle between `a` and (1, 0) vector)\n\nprint(\n a.angle(\n a.rotate(radians=2)\n )\n) # 2\n```\n\n### Using as attributes\n\n`Point.as_` provides a convenient way of using points as tag attributes:\n\n```python\nfrom soda import Tag, Point\n\nsize = Point(100, 200)\n\nprint(size.as_()) # {\"x\": 100, \"y\": 200}\nprint(size.as_(\"width\", \"height\")) # {\"width\": 100, \"height\": 200}\n\n\nprint(\n Tag.rect(\n **size.as_(\"width\", \"height\")\n )\n) # <rect width=\"100\" height=\"200\"/>\n\n```\n\n### PointPath\n\nA version of [`Path`](#paths) accepting `Point` instead of some arguments.\nWhere Path.something(...) accepts coordinates (as two arguments) or some size (like `radius_x` and `radius_y` in `arc`), `PointPath` accepts a point-like object instead.\n\n## Custom components\n\nYou can build custom components, using different approaches:\n\n### Building a tree on init\n\nBuilds a tree on every component creation\n\n```python\nfrom soda import Tag, Fragment\n\nclass CustomComponent(Fragment):\n def __init__(self):\n children = Tag.g(\n Tag.anythingother,\n Tag.lalala(\n Tag.yes,\n Tag.no\n )\n )\n super().__init__(*children)\n\nCustomComponent().render()\n```\n\n### Functional approach\n\nBuilds a tree on every call\n\n```python\nfrom soda import Tag\n\ndef custom_component():\n return Tag.g(\n Tag.anythingother,\n Tag.lalala(\n Tag.yes,\n Tag.no\n )\n )\n\ncustom_component().render()\n```\n\n## Speed\n\nsoda is able to render tens of thousands tags per second, but if you wanna optimize your execution, there are some tips:\n\n### Building a tree efficiently\n\nIf you using the same structure many times (especially if it's a heavy one), avoid rebuilds. Rather than building a new tree every time, consider changing specific parts of it when needed. It won't speed up the render time, though (check Prerendering right below for that)\n\n### Prerendering\n\nIf you have some static tags, you can use `tag.prerender()` to get a prerendered `Literal`.\nThis could speed up your render significantly in some complex cases.\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Fast SVG generation tool",
"version": "2.0.3",
"project_urls": {
"Homepage": "https://github.com/evtn/soda",
"Repository": "https://github.com/evtn/soda"
},
"split_keywords": [
"soda",
" svg",
" xml"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "9bf1376caa06081658ab0acee5cbf2b020dc330a0f75fa76092d0ec145682421",
"md5": "da53581422710764acbb24cff0efc5eb",
"sha256": "ddd5b91539b7beba425b7b85efd082c018e12fb7d9cfaf5845ee8f5bc542cdc3"
},
"downloads": -1,
"filename": "soda_svg-2.0.3-py3-none-any.whl",
"has_sig": false,
"md5_digest": "da53581422710764acbb24cff0efc5eb",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": "<3.13,>=3.8",
"size": 16614,
"upload_time": "2024-10-10T18:11:05",
"upload_time_iso_8601": "2024-10-10T18:11:05.158884Z",
"url": "https://files.pythonhosted.org/packages/9b/f1/376caa06081658ab0acee5cbf2b020dc330a0f75fa76092d0ec145682421/soda_svg-2.0.3-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "eb8d9a2b7885caa68f1bf533f8748a7db9a6526d263579cdb1ae60ae4ce3be74",
"md5": "63b10919919f85c1b1b6b1a3f0905317",
"sha256": "376efa12a4115fd5d35b2394ea6b867ca61899716e170baebb20bdb98f5e0c35"
},
"downloads": -1,
"filename": "soda_svg-2.0.3.tar.gz",
"has_sig": false,
"md5_digest": "63b10919919f85c1b1b6b1a3f0905317",
"packagetype": "sdist",
"python_version": "source",
"requires_python": "<3.13,>=3.8",
"size": 18355,
"upload_time": "2024-10-10T18:11:06",
"upload_time_iso_8601": "2024-10-10T18:11:06.490911Z",
"url": "https://files.pythonhosted.org/packages/eb/8d/9a2b7885caa68f1bf533f8748a7db9a6526d263579cdb1ae60ae4ce3be74/soda_svg-2.0.3.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-10-10 18:11:06",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "evtn",
"github_project": "soda",
"travis_ci": false,
"coveralls": true,
"github_actions": true,
"lcname": "soda-svg"
}