Coverage for gws-app/gws/lib/svg/element.py: 43%
30 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
1# sanitizer
3from typing import Optional
5import gws
6import gws.lib.xmlx as xmlx
7import gws.lib.mime
8import gws.lib.image
10_SVG_TAG_ATTS = {
11 'xmlns': 'http://www.w3.org/2000/svg',
12}
15def fragment_to_element(fragment: list[gws.XmlElement], atts: dict = None) -> gws.XmlElement:
16 """Convert an SVG fragment to an SVG element."""
18 fr = sorted(fragment, key=lambda el: el.attrib.get('z-index', 0))
19 return xmlx.tag('svg', _SVG_TAG_ATTS, atts, *fr)
22def fragment_to_image(fragment: list[gws.XmlElement], size: gws.Size, mime=gws.lib.mime.PNG) -> gws.lib.image.Image:
23 """Convert an SVG fragment to a raster image."""
25 el = fragment_to_element(fragment)
26 return gws.lib.image.from_svg(el.to_string(), size, mime)
29def sanitize_element(el: gws.XmlElement) -> Optional[gws.XmlElement]:
30 """Remove unsafe stuff from an SVG element."""
32 children = gws.u.compact(_sanitize(c) for c in el)
33 if children:
34 return xmlx.tag('svg', _sanitize_atts(el.attrib), *children)
37##
39_ALLOWED_TAGS = {
40 'circle',
41 'clippath',
42 'defs',
43 'ellipse',
44 'g',
45 'hatch',
46 'hatchpath',
47 'line',
48 'lineargradient',
49 'marker',
50 'mask',
51 'mesh',
52 'meshgradient',
53 'meshpatch',
54 'meshrow',
55 'mpath',
56 'path',
57 'pattern',
58 'polygon',
59 'polyline',
60 'radialgradient',
61 'rect',
62 'solidcolor',
63 'symbol',
64 'text',
65 'textpath',
66 'title',
67 'tspan',
68 'use',
69}
71_ALLOWED_ATTRIBUTES = {
72 'alignment-baseline',
73 'baseline-shift',
74 'clip',
75 'clip-path',
76 'clip-rule',
77 'color',
78 'color-interpolation',
79 'color-interpolation-filters',
80 'color-profile',
81 'color-rendering',
82 'cursor',
83 'd',
84 'direction',
85 'display',
86 'dominant-baseline',
87 'enable-background',
88 'fill',
89 'fill-opacity',
90 'fill-rule',
91 'filter',
92 'flood-color',
93 'flood-opacity',
94 'font-family',
95 'font-size',
96 'font-size-adjust',
97 'font-stretch',
98 'font-style',
99 'font-variant',
100 'font-weight',
101 'glyph-orientation-horizontal',
102 'glyph-orientation-vertical',
103 'image-rendering',
104 'kerning',
105 'letter-spacing',
106 'lighting-color',
107 'marker-end',
108 'marker-mid',
109 'marker-start',
110 'mask',
111 'opacity',
112 'overflow',
113 'pointer-events',
114 'shape-rendering',
115 'stop-color',
116 'stop-opacity',
117 'stroke',
118 'stroke-dasharray',
119 'stroke-dashoffset',
120 'stroke-linecap',
121 'stroke-linejoin',
122 'stroke-miterlimit',
123 'stroke-opacity',
124 'stroke-width',
125 'text-anchor',
126 'text-decoration',
127 'text-rendering',
128 'transform',
129 'transform-origin',
130 'unicode-bidi',
131 'vector-effect',
132 'visibility',
133 'word-spacing',
134 'writing-mode',
135 'width',
136 'height',
137 'viewBox',
138}
141def _sanitize(el: gws.XmlElement) -> Optional[gws.XmlElement]:
142 if el.name in _ALLOWED_TAGS:
143 return xmlx.tag(
144 el.name,
145 _sanitize_atts(el.attrib),
146 gws.u.compact(_sanitize(c) for c in el.children()))
149def _sanitize_atts(atts: dict) -> dict:
150 res = {}
151 for k, v in atts.items():
152 if k not in _ALLOWED_ATTRIBUTES:
153 continue
154 if v.strip().startswith(('http:', 'https:', 'data:')):
155 continue
156 res[k] = v
157 return res