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

1# sanitizer 

2 

3from typing import Optional 

4 

5import gws 

6import gws.lib.xmlx as xmlx 

7import gws.lib.mime 

8import gws.lib.image 

9 

10_SVG_TAG_ATTS = { 

11 'xmlns': 'http://www.w3.org/2000/svg', 

12} 

13 

14 

15def fragment_to_element(fragment: list[gws.XmlElement], atts: dict = None) -> gws.XmlElement: 

16 """Convert an SVG fragment to an SVG element.""" 

17 

18 fr = sorted(fragment, key=lambda el: el.attrib.get('z-index', 0)) 

19 return xmlx.tag('svg', _SVG_TAG_ATTS, atts, *fr) 

20 

21 

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.""" 

24 

25 el = fragment_to_element(fragment) 

26 return gws.lib.image.from_svg(el.to_string(), size, mime) 

27 

28 

29def sanitize_element(el: gws.XmlElement) -> Optional[gws.XmlElement]: 

30 """Remove unsafe stuff from an SVG element.""" 

31 

32 children = gws.u.compact(_sanitize(c) for c in el) 

33 if children: 

34 return xmlx.tag('svg', _sanitize_atts(el.attrib), *children) 

35 

36 

37## 

38 

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} 

70 

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} 

139 

140 

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())) 

147 

148 

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