Coverage for gws-app/gws/lib/style/icon.py: 76%
63 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +0200
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +0200
1"""SVG icon parser."""
3from typing import Optional
5import re
7import gws
8import gws.lib.mime
9import gws.lib.net
10import gws.lib.svg
11import gws.lib.osx
12import gws.lib.text
13import gws.lib.xmlx as xmlx
16class Error(gws.Error):
17 """Raised when an icon cannot be loaded or parsed."""
19 pass
22def to_data_url(svg: gws.XmlElement) -> str:
23 """Convert an SVG icon to a base64 data URL.
25 Args:
26 svg: SVG element.
28 Returns:
29 A ``data:image/svg+xml;base64,...`` URL.
30 """
32 xml = svg.to_string()
33 return gws.lib.text.to_data_url(xml, gws.lib.mime.SVG)
36def parse(val: str, opts) -> Optional[gws.XmlElement]:
37 """Load and parse an SVG icon.
39 The value can be wrapped in CSS ``url(...)`` and quotes. It is resolved as follows:
41 - a ``data:`` URL is decoded,
42 - a path is looked up in the image directories (``opts.imageDirs``),
43 - in the trusted mode only, an ``http(s)`` URL is fetched, or any other value is read as a file path.
45 The SVG is normalized (unsafe elements and attributes are removed) and must have a width and a height.
47 Args:
48 val: Data URL, URL or path.
49 opts: Parser options (``gws.lib.style.parser.Options``).
51 Returns:
52 The normalized SVG element, or ``None`` if the value is empty or does not contain an SVG.
54 Raises:
55 ``Error``: If the value is untrusted, cannot be loaded or decoded, or the SVG is invalid.
56 """
57 if not val:
58 return
60 val = str(val).strip()
61 m = re.match(r'^url\((.+?)\)$', val)
62 if m:
63 val = m.group(1)
65 val = val.strip('\'\"')
67 bs = _get_bytes(val, opts)
68 if not bs:
69 return
71 if bs.startswith(b'<'):
72 return _parse_svg(bs.decode('utf8'))
74 # @TODO other icon formats?
77##
80def _get_bytes(val, opts) -> Optional[bytes]:
81 if val.startswith('data:'):
82 return _decode_data_url(val)
84 # if not trusted, looks in provided public dirs
86 for img_dir in opts.get('imageDirs', []):
87 path = gws.lib.osx.abs_web_path(val, img_dir)
88 if path:
89 return gws.u.read_file_b(path)
91 # network and aribtrary files only in the trusted mode
93 if not opts.get('trusted'):
94 raise Error('untrusted value', val)
96 if re.match(r'^https?:', val):
97 try:
98 return gws.lib.net.http_request(val).content
99 except Exception as exc:
100 raise Error('network error', val) from exc
102 try:
103 return gws.u.read_file_b(val)
104 except Exception as exc:
105 raise Error('file error', val) from exc
108def _decode_data_url(val) -> Optional[bytes]:
109 try:
110 mime, content = gws.lib.text.parse_data_url(val)
111 except gws.lib.text.Error as exc:
112 raise Error('decode error', val) from exc
113 if gws.lib.mime.get_base(mime) == gws.lib.mime.SVG:
114 return content
117def _parse_svg(val):
118 try:
119 el = xmlx.from_string(val, gws.XmlOptions(removeNamespaces=True))
120 except Exception as exc:
121 raise Error('parse error', val) from exc
123 el_clean = gws.lib.svg.normalize_element(el)
125 w = el_clean.get('width')
126 h = el_clean.get('height')
128 if not w or not h:
129 raise Error('missing width or height', val)
131 return el_clean