Coverage for gws-app/gws/lib/style/icon.py: 26%

73 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-17 01:37 +0200

1from typing import Optional 

2 

3import base64 

4import re 

5 

6import gws 

7import gws.lib.net 

8import gws.lib.svg 

9import gws.lib.osx 

10import gws.lib.xmlx as xmlx 

11 

12 

13class Error(gws.Error): 

14 pass 

15 

16 

17class ParsedIcon(gws.Data): 

18 """Svg data.""" 

19 svg: gws.XmlElement 

20 """Structure and attributes of the svg.""" 

21 

22 

23def to_data_url(icon: ParsedIcon) -> str: 

24 """Converts a svg to url. 

25 

26 Args: 

27 icon: Svg object to convert. 

28 

29 Returns: 

30 The url, or an empty string if ``icon`` is not a svg. 

31 

32 """ 

33 

34 if icon.svg: 

35 xml = icon.svg.to_string() 

36 return 'data:image/svg+xml;base64,' + base64.standard_b64encode(xml.encode('utf8')).decode('utf8') 

37 return '' 

38 

39 

40def parse(val: str, opts) -> Optional[ParsedIcon]: 

41 """Parses an url or directory path to a svg. 

42 

43 Args: 

44 val: An url or directory path containing a svg. 

45 opts: Url or directory path options. 

46 

47 Returns: 

48 The svg, if the url is trusted, or if the path is in a trusted directory. 

49 

50 """ 

51 if not val: 

52 return 

53 

54 val = str(val).strip() 

55 m = re.match(r'^url\((.+?)\)$', val) 

56 if m: 

57 val = m.group(1) 

58 

59 val = val.strip('\'\"') 

60 

61 bs = _get_bytes(val, opts) 

62 if not bs: 

63 return 

64 

65 if bs.startswith(b'<'): 

66 svg = _parse_svg(bs.decode('utf8')) 

67 if svg: 

68 return ParsedIcon(svg=svg) 

69 

70 # @TODO other icon formats? 

71 

72 

73## 

74 

75 

76def _get_bytes(val, opts) -> Optional[bytes]: 

77 if val.startswith('data:'): 

78 return _decode_data_url(val, opts) 

79 

80 # if not trusted, looks in provided public dirs 

81 

82 for img_dir in opts.get('imageDirs', []): 

83 path = gws.lib.osx.abs_web_path(val, img_dir) 

84 if path: 

85 return gws.u.read_file_b(path) 

86 

87 # network and aribtrary files only in the trusted mode 

88 

89 if not opts.get('trusted'): 

90 raise Error('untrusted value', val) 

91 

92 if re.match(r'^https?:', val): 

93 try: 

94 return gws.lib.net.http_request(val).content 

95 except Exception as exc: 

96 raise Error('network error', val) from exc 

97 

98 try: 

99 return gws.u.read_file_b(val) 

100 except Exception as exc: 

101 raise Error('file error', val) from exc 

102 

103 

104_PREFIXES = [ 

105 'data:image/svg+xml;base64,', 

106 'data:image/svg+xml;utf8,', 

107 'data:image/svg;base64,', 

108 'data:image/svg;utf8,', 

109] 

110 

111 

112def _decode_data_url(val, trusted) -> Optional[bytes]: 

113 for pfx in _PREFIXES: 

114 if val.startswith(pfx): 

115 s = val[len(pfx):] 

116 try: 

117 if 'base64' in pfx: 

118 return base64.standard_b64decode(s) 

119 else: 

120 return s.encode('utf8') 

121 except Exception as exc: 

122 raise Error('decode error', val) from exc 

123 

124 

125def _parse_svg(val): 

126 try: 

127 el = xmlx.from_string(val) 

128 except Exception as exc: 

129 raise Error('parse error', val) from exc 

130 

131 el_clean = gws.lib.svg.sanitize_element(el) 

132 

133 w = el_clean.get('width') 

134 h = el_clean.get('height') 

135 

136 if not w or not h: 

137 raise Error('missing width or height', val) 

138 

139 return el_clean