Programmatically generate SVG (vector) images, animations, and interactive Jupyter widgets

Compare changes

Choose any two refs to compare.

+17
README.md
···
Note: The above example currently only works in `jupyter notebook`, not `jupyter lab`.
+
### Embed custom fonts
+
```python
+
import drawsvg as draw
+
+
d = draw.Drawing(400, 100, origin='center')
+
d.embed_google_font('Permanent Marker', text=set('Text with custom font'))
+
+
d.append(draw.Text('Text with custom font', 35, 0, 0, center=True,
+
font_family='Permanent Marker', font_style='italic'))
+
+
d.save_svg('font.svg')
+
d # Custom fonts work in most browsers but not in rasterize(), save_png(), or save_video()
+
```
+
+
[![Example output image](https://raw.githubusercontent.com/cduck/drawsvg/master/examples/font.svg?sanitize=true)](https://github.com/cduck/drawsvg/blob/master/examples/font.svg)
+
+
---
# Full-feature install
+3 -3
docs/img/04_align.svg
···
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
-
width="250" height="120" viewBox="0 0 250 120">
+
width="250" height="120" viewBox="0 0 250 120">
<defs>
</defs>
<path d="M75,100 L75,0" stroke="gray" />
···
<text x="75" y="30" font-size="24" text-anchor="start">Start</text>
<text x="75" y="60" font-size="24" text-anchor="middle">Middle</text>
<text x="75" y="90" font-size="24" text-anchor="end">End</text>
-
<text x="150" y="30" font-size="24" dominant-baseline="bottom">Auto</text>
+
<text x="150" y="30" font-size="24" dominant-baseline="auto">Auto</text>
<text x="150" y="60" font-size="24" dominant-baseline="middle">Middle</text>
-
<text x="150" y="90" font-size="24" dominant-baseline="top">Hanging</text>
+
<text x="150" y="90" font-size="24" dominant-baseline="hanging">Hanging</text>
</svg>
+2 -2
docs/index.md
···
d.append(dw.Text('Start', 24, 75, 30, text_anchor='start'))
d.append(dw.Text('Middle', 24, 75, 60, text_anchor='middle'))
d.append(dw.Text('End', 24, 75, 90, text_anchor='end'))
-
d.append(dw.Text('Auto', 24, 150, 30, dominant_baseline='bottom'))
+
d.append(dw.Text('Auto', 24, 150, 30, dominant_baseline='auto'))
d.append(dw.Text('Middle', 24, 150, 60, dominant_baseline='middle'))
-
d.append(dw.Text('Hanging', 24, 150, 90, dominant_baseline='top'))
+
d.append(dw.Text('Hanging', 24, 150, 90, dominant_baseline='hanging'))
```
![svg](img/04_align.svg)
+29 -4
drawsvg/drawing.py
···
import xml.sax.saxutils as xml
from . import (
-
types, elements as elements_module, raster, video, jupyter, native_animation
+
types, elements as elements_module, raster, video, jupyter,
+
native_animation, font_embed,
)
···
else:
self.elements.extend(iterable)
def insert(self, i, element):
+
'''Inserts a top-level element at the given array index.'''
self.elements.insert(i, element)
def remove(self, element):
+
'''Removes a top-level element (except those with a z-index).'''
self.elements.remove(element)
def clear(self):
+
'''Clears all drawing elements, with or without a z-index, but keeps
+
defs-type elements added with `append_def()`.
+
'''
self.elements.clear()
+
self.ordered_elements.clear()
def index(self, *args, **kwargs):
+
'''Finds the array-index of a top-level element (except those with a
+
z-index).
+
'''
return self.elements.index(*args, **kwargs)
def count(self, element):
+
'''Counts the number of top-level elements (except those with a z-index
+
).
+
'''
return self.elements.count(element)
def reverse(self):
+
'''Reverses the order of all elements (except those with a z-index).'''
self.elements.reverse()
def draw_def(self, obj, **kwargs):
if not hasattr(obj, 'write_svg_element'):
···
self.append(elements_module.Title(text, **kwargs))
def append_css(self, css_text):
self.css_list.append(css_text)
+
def embed_google_font(self, family, text=None, display='swap', **kwargs):
+
'''Download SVG-embeddable CSS from Google fonts.
+
+
Args:
+
family: Name of font family or list of font families.
+
text: The set of characters required from the font. Only a font
+
subset with these characters will be downloaded.
+
display: The font-display CSS value.
+
**kwargs: Other URL parameters sent to
+
https://fonts.googleapis.com/css?...
+
'''
+
self.append_css(font_embed.download_google_font_css(
+
family, text=text, display=display, **kwargs))
def append_javascript(self, js_text, onload=None):
if onload:
if self.svg_args.get('onload'):
···
id_index += 1
return id_str
id_map = defaultdict(id_gen)
-
prev_set = set((id(defn) for defn in self.other_defs))
-
prev_list = []
+
prev_set = set()
def is_duplicate(obj):
nonlocal prev_set
dup = id(obj) in prev_set
prev_set.add(id(obj))
-
prev_list.append(obj)
return dup
for element in self.other_defs:
if hasattr(element, 'write_svg_element'):
+25 -1
drawsvg/elements.py
···
'''
TAG_NAME = 'text'
has_content = True
-
def __new__(cls, text, *args, path=None, id=None, _skip_check=False,
+
def __new__(cls, text='', *args, path=None, id=None, _skip_check=False,
**kwargs):
# Check for the special case of multi-line text on a path
# This is inconsistently implemented by renderers so we return a group
···
def __init__(self, cx, cy, r, start_deg, end_deg, cw=False, **kwargs):
super().__init__(d='', **kwargs)
self.arc(cx, cy, r, start_deg, end_deg, cw=cw, include_m=True)
+
+
class ForeignObject(DrawingBasicElement):
+
'''A foreign object, i.e. for embedding HTML.
+
+
In the context of SVG embedded in an HTML document, the XHTML namespace
+
could be omitted in the foreignObject content, but it is mandatory in the
+
context of an SVG document.
+
+
This element works best when viewing the SVG in a browser. Drawing.rasterize()
+
and Drawing.save_png() are unable to display this element.
+
+
Additional keyword arguments are output as additional arguments to the SVG
+
node e.g. fill="red", stroke="#ff4477", stroke_width=2.
+
'''
+
TAG_NAME = 'foreignObject'
+
has_content = True
+
def __init__(self, content, **kwargs):
+
super().__init__(**kwargs)
+
self.content = content
+
def write_content(self, id_map, is_duplicate, output_file, lcontext,
+
dry_run):
+
if dry_run:
+
return
+
output_file.write(self.content)
+48
drawsvg/font_embed.py
···
+
import urllib.request, urllib.parse
+
import re
+
+
from . import url_encode
+
+
+
def download_url(url):
+
with urllib.request.urlopen(url) as r:
+
return r.read()
+
+
def download_url_to_data_uri(url, mime='application/octet-stream'):
+
data = download_url(url)
+
return url_encode.bytes_as_data_uri(data, strip_chars='', mime=mime)
+
+
def embed_css_resources(css):
+
'''Replace all URLs in the CSS string with downloaded data URIs.'''
+
regex = re.compile(r'url\((https?://[^)]*)\)')
+
def repl(match):
+
url = match[1]
+
uri = download_url_to_data_uri(url)
+
return f'url({uri})'
+
embedded, _ = regex.subn(repl, css)
+
return embedded
+
+
def download_google_font_css(family, text=None, display='swap', **kwargs):
+
'''Download SVG-embeddable CSS from Google fonts.
+
+
Args:
+
family: Name of font family or list of font families.
+
text: The set of characters required from the font. Only a font subset
+
with these characters will be downloaded.
+
display: The font-display CSS value.
+
**kwargs: Other URL parameters sent to
+
https://fonts.googleapis.com/css?...
+
'''
+
if not isinstance(family, str):
+
family = '|'.join(family) # Request a list of families
+
args = dict(family=family, display=display)
+
if text is not None:
+
if not isinstance(text, str):
+
text = ''.join(text)
+
args['text'] = text
+
args.update(kwargs)
+
params = urllib.parse.urlencode(args)
+
url = f'https://fonts.googleapis.com/css?{params}'
+
with urllib.request.urlopen(url) as r:
+
css = r.read().decode('utf-8')
+
return embed_css_resources(css)
+11
drawsvg/widgets/drawing_javascript.py
···
var svg_pt = this.cursor_point.matrixTransform(
this.svg_view.getScreenCTM().inverse());
+
var target_parents = [];
+
var target = e.target;
+
while(target && target != this.svg_view)
+
{
+
if (target.id) {
+
target_parents.push(target.id);
+
}
+
target = target.parentNode;
+
}
+
this.send({
name: name,
x: svg_pt.x,
···
movementY: e.movementY,
timeStamp: e.timeStamp,
targetId: e.target ? e.target.id : null,
+
targetParentIds: target_parents,
currentTargetId: e.currentTarget ? e.currentTarget.id : null,
relatedTargetId: e.relatedTarget ? e.relatedTarget.id : null,
});
+15
examples/font.svg
···
+
<?xml version="1.0" encoding="UTF-8"?>
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+
width="400" height="100" viewBox="-200.0 -50.0 400 100">
+
<style>/*<![CDATA[*/@font-face {
+
font-family: 'Permanent Marker';
+
font-style: normal;
+
font-weight: 400;
+
font-display: swap;
+
src: url(data:application/octet-stream;base64,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) format('truetype');
+
}
+
/*]]>*/</style>
+
<defs>
+
</defs>
+
<text x="0" y="0" font-size="35" font-family="Permanent Marker" font-style="italic" text-anchor="middle" dominant-baseline="central">Text with custom font</text>
+
</svg>
+1 -1
setup.py
···
import logging
logger = logging.getLogger(__name__)
-
version = '2.0.2'
+
version = '2.4.0'
try:
with open('README.md', 'r') as f: