intelligent_test_svg_preview.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. package service
  2. import (
  3. "bytes"
  4. "encoding/xml"
  5. "errors"
  6. "io"
  7. "regexp"
  8. "sort"
  9. "strconv"
  10. "strings"
  11. )
  12. var (
  13. intelligentSVGElements = strings.Fields("svg g defs title desc path rect circle ellipse line polyline polygon text tspan linearGradient radialGradient stop clipPath mask pattern")
  14. intelligentSVGAttributes = strings.Fields("id viewBox width height x y x1 x2 y1 y2 cx cy r rx ry d points fill fill-opacity fill-rule stroke stroke-width stroke-opacity stroke-linecap stroke-linejoin stroke-dasharray opacity transform font-size font-family font-weight text-anchor dominant-baseline offset stop-color stop-opacity gradientUnits gradientTransform spreadMethod clip-path clip-rule mask maskUnits patternUnits patternTransform preserveAspectRatio dx dy")
  15. svgCSSRule = regexp.MustCompile(`(?s)([^{}]+)\{([^{}]*)\}`)
  16. svgCSSSelector = regexp.MustCompile(`^(?:[.#][A-Za-z_][A-Za-z0-9_-]*|[A-Za-z][A-Za-z0-9]*)$`)
  17. svgLocalURL = regexp.MustCompile(`(?i)^url\(#[A-Za-z_][A-Za-z0-9_.-]*\)$`)
  18. svgDrawingPath = regexp.MustCompile(`[LlHhVvCcSsQqTtAa]`)
  19. )
  20. type intelligentSVGNode struct {
  21. start xml.StartElement
  22. children []*intelligentSVGNode
  23. text string
  24. }
  25. type intelligentSVGRule struct {
  26. selector string
  27. values map[string]string
  28. }
  29. func svgStaticValue(value string) bool {
  30. lower := strings.ToLower(value)
  31. if strings.ContainsAny(value, "<>\x00\\") || strings.Contains(lower, "javascript:") || strings.Contains(lower, "data:") || strings.Contains(lower, "http:") || strings.Contains(lower, "https:") || strings.Contains(lower, "//") || strings.Contains(lower, "expression(") || strings.Contains(lower, "var(") {
  32. return false
  33. }
  34. return !strings.Contains(lower, "url(") || svgLocalURL.MatchString(strings.TrimSpace(value))
  35. }
  36. func svgPaintStyles(raw string) map[string]string {
  37. allowed := map[string]bool{}
  38. for _, key := range strings.Fields("fill fill-opacity fill-rule stroke stroke-width stroke-opacity stroke-linecap stroke-linejoin stroke-dasharray opacity font-size font-family font-weight text-anchor dominant-baseline stop-color stop-opacity") {
  39. allowed[key] = true
  40. }
  41. values := map[string]string{}
  42. for _, declaration := range strings.Split(raw, ";") {
  43. pair := strings.SplitN(declaration, ":", 2)
  44. if len(pair) != 2 {
  45. continue
  46. }
  47. key, value := strings.TrimSpace(pair[0]), strings.TrimSpace(pair[1])
  48. if key == "display" && value == "none" {
  49. values["opacity"] = "0"
  50. continue
  51. }
  52. if allowed[key] && svgStaticValue(value) {
  53. values[key] = value
  54. }
  55. }
  56. return values
  57. }
  58. // PrepareIntelligentSVGPreview creates an inert SVG image. CSS is reduced to
  59. // whitelisted static paint attributes; scripts, links, external resources and
  60. // unsupported subtrees are never carried into the returned artifact.
  61. func PrepareIntelligentSVGPreview(output string) (string, bool, error) {
  62. start, end := strings.Index(output, "<svg"), strings.LastIndex(output, "</svg>")
  63. if start < 0 || end < start {
  64. return "", false, errors.New("未返回完整 SVG 图像")
  65. }
  66. raw := output[start : end+len("</svg>")]
  67. if len(raw) > 128<<10 {
  68. return "", false, errors.New("SVG 超过 128 KiB 上限")
  69. }
  70. _, strictErr := SanitizeIntelligentTestSVG(raw)
  71. decoder := xml.NewDecoder(strings.NewReader(raw))
  72. var root *intelligentSVGNode
  73. var stack []*intelligentSVGNode
  74. nodes := 0
  75. for {
  76. token, err := decoder.Token()
  77. if err == io.EOF {
  78. break
  79. }
  80. if err != nil {
  81. return "", false, errors.New("SVG XML 格式错误")
  82. }
  83. switch value := token.(type) {
  84. case xml.StartElement:
  85. nodes++
  86. if nodes > 6000 || len(stack) >= 40 {
  87. return "", false, errors.New("SVG 结构过于复杂")
  88. }
  89. node := &intelligentSVGNode{start: value}
  90. if len(stack) == 0 {
  91. if root != nil || value.Name.Local != "svg" {
  92. return "", false, errors.New("SVG 根节点错误")
  93. }
  94. root = node
  95. } else {
  96. parent := stack[len(stack)-1]
  97. parent.children = append(parent.children, node)
  98. }
  99. stack = append(stack, node)
  100. case xml.EndElement:
  101. if len(stack) == 0 {
  102. return "", false, errors.New("SVG XML 格式错误")
  103. }
  104. stack = stack[:len(stack)-1]
  105. case xml.CharData:
  106. if len(stack) > 0 {
  107. node := stack[len(stack)-1]
  108. node.children = append(node.children, &intelligentSVGNode{text: string(value)})
  109. }
  110. case xml.Comment:
  111. default:
  112. return "", false, errors.New("SVG 不允许指令或实体声明")
  113. }
  114. }
  115. if root == nil || len(stack) != 0 {
  116. return "", false, errors.New("SVG 不完整")
  117. }
  118. allowed, attrsAllowed := map[string]bool{}, map[string]bool{}
  119. for _, key := range intelligentSVGElements {
  120. allowed[key] = true
  121. }
  122. for _, key := range intelligentSVGAttributes {
  123. attrsAllowed[key] = true
  124. }
  125. var rules []intelligentSVGRule
  126. ids := map[string]*intelligentSVGNode{}
  127. var collect func(*intelligentSVGNode)
  128. collect = func(node *intelligentSVGNode) {
  129. name := node.start.Name.Local
  130. if node.start.Name.Space != "" && node.start.Name.Space != "http://www.w3.org/2000/svg" {
  131. return
  132. }
  133. if name == "style" {
  134. var css strings.Builder
  135. for _, child := range node.children {
  136. css.WriteString(child.text)
  137. }
  138. rawCSS := css.String()
  139. if len(rawCSS) >= 32<<10 || strings.Contains(rawCSS, "@") {
  140. return
  141. }
  142. for _, match := range svgCSSRule.FindAllStringSubmatch(rawCSS, 128) {
  143. for _, selector := range strings.Split(match[1], ",") {
  144. selector = strings.TrimSpace(selector)
  145. if svgCSSSelector.MatchString(selector) && len(rules) < 512 {
  146. rules = append(rules, intelligentSVGRule{selector, svgPaintStyles(match[2])})
  147. }
  148. }
  149. }
  150. return
  151. }
  152. if !allowed[name] && name != "use" {
  153. return
  154. }
  155. for _, attr := range node.start.Attr {
  156. if attr.Name.Space == "" && attr.Name.Local == "id" {
  157. ids[attr.Value] = node
  158. }
  159. }
  160. for _, child := range node.children {
  161. collect(child)
  162. }
  163. }
  164. collect(root)
  165. specificity := func(s string) int {
  166. if strings.HasPrefix(s, "#") {
  167. return 2
  168. }
  169. if strings.HasPrefix(s, ".") {
  170. return 1
  171. }
  172. return 0
  173. }
  174. sort.SliceStable(rules, func(i, j int) bool { return specificity(rules[i].selector) < specificity(rules[j].selector) })
  175. var out bytes.Buffer
  176. encoder := xml.NewEncoder(&out)
  177. drawable := 0
  178. rendered, depth := 0, 0
  179. active := map[*intelligentSVGNode]bool{}
  180. var render func(*intelligentSVGNode, bool) error
  181. render = func(node *intelligentSVGNode, visible bool) error {
  182. rendered++
  183. if rendered > 12000 || depth >= 50 || active[node] {
  184. return errors.New("SVG 引用循环或结构过于复杂")
  185. }
  186. depth++
  187. active[node] = true
  188. defer func() { depth--; delete(active, node) }()
  189. if node.start.Name.Local == "" {
  190. return encoder.EncodeToken(xml.CharData(node.text))
  191. }
  192. isUse := node.start.Name.Local == "use"
  193. if (!allowed[node.start.Name.Local] && !isUse) || (node.start.Name.Space != "" && node.start.Name.Space != "http://www.w3.org/2000/svg") {
  194. return nil
  195. }
  196. values := map[string]string{}
  197. id, classes, inline := "", "", ""
  198. for _, attr := range node.start.Attr {
  199. switch attr.Name.Local {
  200. case "id":
  201. id = attr.Value
  202. case "class":
  203. classes = " " + attr.Value + " "
  204. case "style":
  205. inline = attr.Value
  206. }
  207. if attr.Name.Space == "" && attrsAllowed[attr.Name.Local] && svgStaticValue(attr.Value) {
  208. values[attr.Name.Local] = attr.Value
  209. }
  210. }
  211. for _, rule := range rules {
  212. matches := rule.selector == node.start.Name.Local || (strings.HasPrefix(rule.selector, "#") && rule.selector[1:] == id) || (strings.HasPrefix(rule.selector, ".") && strings.Contains(classes, " "+rule.selector[1:]+" "))
  213. if matches {
  214. for key, value := range rule.values {
  215. values[key] = value
  216. }
  217. }
  218. }
  219. for key, value := range svgPaintStyles(inline) {
  220. values[key] = value
  221. }
  222. if opacity, err := strconv.ParseFloat(values["opacity"], 64); err == nil && opacity <= 0 {
  223. visible = false
  224. }
  225. switch node.start.Name.Local {
  226. case "defs", "clipPath", "mask", "pattern", "linearGradient", "radialGradient":
  227. visible = false
  228. }
  229. if visible && svgBasicDrawable(node.start.Name.Local, values) {
  230. drawable++
  231. }
  232. element := xml.StartElement{Name: xml.Name{Local: node.start.Name.Local}}
  233. var reference *intelligentSVGNode
  234. if isUse {
  235. for _, attr := range node.start.Attr {
  236. if attr.Name.Local == "href" && (attr.Name.Space == "" || attr.Name.Space == "http://www.w3.org/1999/xlink") && strings.HasPrefix(attr.Value, "#") {
  237. reference = ids[strings.TrimPrefix(attr.Value, "#")]
  238. }
  239. }
  240. if reference == nil {
  241. return nil
  242. }
  243. element.Name.Local = "g"
  244. x, y := values["x"], values["y"]
  245. if x == "" {
  246. x = "0"
  247. }
  248. if y == "" {
  249. y = "0"
  250. }
  251. if _, err := strconv.ParseFloat(x, 64); err != nil {
  252. return nil
  253. }
  254. if _, err := strconv.ParseFloat(y, 64); err != nil {
  255. return nil
  256. }
  257. values["transform"] = strings.TrimSpace(values["transform"] + " translate(" + x + " " + y + ")")
  258. delete(values, "x")
  259. delete(values, "y")
  260. }
  261. if node == root {
  262. element.Attr = append(element.Attr, xml.Attr{Name: xml.Name{Local: "xmlns"}, Value: "http://www.w3.org/2000/svg"})
  263. }
  264. for _, key := range intelligentSVGAttributes {
  265. if value, ok := values[key]; ok {
  266. element.Attr = append(element.Attr, xml.Attr{Name: xml.Name{Local: key}, Value: value})
  267. }
  268. }
  269. if err := encoder.EncodeToken(element); err != nil {
  270. return err
  271. }
  272. if reference != nil {
  273. if err := render(reference, visible); err != nil {
  274. return err
  275. }
  276. }
  277. for _, child := range node.children {
  278. if err := render(child, visible); err != nil {
  279. return err
  280. }
  281. }
  282. return encoder.EncodeToken(element.End())
  283. }
  284. if err := render(root, true); err != nil {
  285. return "", false, err
  286. }
  287. if err := encoder.Flush(); err != nil {
  288. return "", false, err
  289. }
  290. if out.Len() > 128<<10 {
  291. return "", false, errors.New("SVG 展开后超过 128 KiB 上限")
  292. }
  293. if drawable == 0 {
  294. return "", false, errors.New("SVG 未包含可显示的基本图形")
  295. }
  296. safe, err := SanitizeIntelligentTestSVG(out.String())
  297. return safe, strictErr != nil, err
  298. }
  299. func svgBasicDrawable(name string, values map[string]string) bool {
  300. positive := func(key string) bool {
  301. value, err := strconv.ParseFloat(strings.TrimSuffix(values[key], "%"), 64)
  302. return err == nil && value > 0
  303. }
  304. switch name {
  305. case "rect":
  306. return positive("width") && positive("height")
  307. case "circle":
  308. return positive("r")
  309. case "ellipse":
  310. return positive("rx") && positive("ry")
  311. case "path":
  312. return svgDrawingPath.MatchString(values["d"])
  313. case "line":
  314. return values["x1"] != values["x2"] || values["y1"] != values["y2"]
  315. case "polyline", "polygon":
  316. return len(strings.Fields(strings.ReplaceAll(values["points"], ",", " "))) >= 4
  317. }
  318. return false
  319. }
  320. func evaluateIntelligentSVG(output string) IntelligentTestEvaluation {
  321. detail := newIntelligentAssessment("svg_structure")
  322. detail["answer_verdict"] = "not_evaluated"
  323. detail["limitation"] = "图像已按安全 SVG 展示;鹈鹕、自行车和画面质量需要内容复核,结构检查不提供模型能力分数。"
  324. svg, adjusted, err := PrepareIntelligentSVGPreview(output)
  325. if err != nil {
  326. detail["reason"], detail["format_reason"], detail["image_state"] = err.Error(), err.Error(), "unavailable"
  327. return IntelligentTestEvaluation{Status: "completed", Detail: detail}
  328. }
  329. detail["image_state"], detail["format_verdict"], detail["reason"] = "ready", "compliant", "SVG 结构可显示,图像内容尚未评估"
  330. if adjusted {
  331. detail["image_state"], detail["format_verdict"], detail["format_reason"] = "sanitized", "non_compliant", "已将静态样式转换为安全属性,并移除不支持的内容;预览不执行原始 SVG 代码"
  332. }
  333. return IntelligentTestEvaluation{Status: "completed", Image: svg, Detail: detail}
  334. }