找回密码
 欢迎注册
查看: 2055|回复: 1

[讨论] Geogebra 导出的 svg 文件瘦身

[复制链接]
发表于 2022-12-14 14:59:14 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?欢迎注册

×
我们知道,Geogebra 导出的 svg 文件往往比较大,比如说下面的这个图形,导出生成的 svg 文件有 28k 之大。svgo (https://github.com/svg/svgo)   是一个第三方开源的 svg 优化(瘦身)软件,优化效果不错,它将这个 svg 文件由 28k 压缩成 9k。

命令:svgo 32765321.svg

32765321.svg:
Done in 95 ms!
28.279 KiB - 67.4% = 9.217 KiB

继续观察,会发现 geogebra 生成的 svg 文件过大在主要原因是因为它用 path 来显示文字,比如各个 label。我用 golang 写了一段代码,初步测试效果 ok, 结合 svgo 这个工具,将前面那个 28k 的 svg 文件,进一步压缩到 4k 。


下面是参考代码:

  1. package main

  2. import (
  3.         "fmt"
  4.         "io/ioutil"
  5.         "os"
  6.         "os/exec"
  7.         "regexp"
  8.         "strings"
  9.         "strconv"
  10.         "unsafe"

  11.         "github.com/PuerkitoBio/goquery"
  12. )

  13. // svgo compress,主要代码
  14. func sc(fname string) {
  15.                 // 读入文件
  16.                 input, err := ioutil.ReadFile(fname)
  17.                 content := string(input)

  18.                 // goquery 似乎不支持 dom.Find(`clipPath`)??? 故人为地改成 aaaaa
  19.                 content = strings.Replace(content, "clipPath", "aaaaa", -1)

  20.                 // 将 <g clip-path="...> 更改为 <g>,似乎没有影响,但不确认
  21.                 re, _ := regexp.Compile(` clip-path="[^"].*"`)
  22.                 content = re.ReplaceAllString(content, "")

  23.                 // 下面  30~60 行,读取 viewBox 的 width 和 height 参数,写入变量 v1 与 v2 之中
  24.                 // 当 svg 里的元素位置超出 viewBox 时,将之删除
  25.                 s := strings.Index(content, "viewBox=")
  26.                 i := s+1
  27.                 k := 0

  28.                 for k != 2 {
  29.                         if input[i] == ' ' {
  30.                                 k++
  31.                         }

  32.                         i++
  33.                 }

  34.                 n := i
  35.                 for content[n] != ' ' {
  36.                         n++
  37.                 }
  38.                 s1 := content[i:n]

  39.                 n++
  40.                 i = n
  41.                 for content[n] != '"' {
  42.                         n++
  43.                 }
  44.                 s2 := content[i:n]

  45.                 v1, _ := strconv.ParseFloat(string(s1), 32)
  46.                 v2, _ := strconv.ParseFloat(string(s2), 32)
  47.                 // fmt.Println(v1, v2)

  48.                 // 使用 goquery 解析 svg 文件
  49.                 dom, err := goquery.NewDocumentFromReader(strings.NewReader(content))
  50.                
  51.                 // 删除嵌入的图片
  52.                 dom.Find(`image`).Each(func(i int, s *goquery.Selection) {
  53.                                 parent := s.Parent()
  54.                                 parent.Get(0).RemoveChild(s.Get(0))
  55.                 })

  56.                 // 删除 <clipPath ...>xxxx</clipPath>,删除似乎没有影响,但不确认
  57.                 // goquery 似乎不支持 dom.Find(`clipPath`)??? 故人为地改成 aaaaa
  58.                 dom.Find(`aaaaa`).Each(func(i int, s *goquery.Selection) {
  59.                                 parent := s.Parent()
  60.                                 parent.Get(0).RemoveChild(s.Get(0))
  61.                 })

  62.                 dom.Find(`path`).Each(func(i int, s *goquery.Selection) {
  63.                         parent := s.Parent()
  64.                         str, _ := parent.Html()

  65.                         // 这段代码实现将用 path 来表现的文字(如点的 label)改成用 <text>xxx</text> 来表示
  66.                         // 其中的 x、y 位置由 path 的第一个参数 M xxx yyy 中的 xxx、yyy 得到
  67.                         // 实践证明多数情况下没问题,但某些情形时有约 10 个点位的偏差,原因未明
  68.                         if strings.Index(str, "<path d="M") >= 0 {
  69.                                 reg := regexp.MustCompile(`<path d="M ([\-0-9.]+) ([\-0-9.]+)`)
  70.                                 p := reg.FindStringSubmatch(str)
  71.                                 if len(p) > 1 {
  72.                                         d1, _ := strconv.ParseFloat(string(p[1]), 32)
  73.                                         d2, _ := strconv.ParseFloat(string(p[2]), 32)
  74.                                         if d1 > v1 || d2 > v2 {
  75.                                                 // 这种情况下的元素位于 viewBox 之外,删除
  76.                                                 parent.Get(0).RemoveChild(s.Get(0))
  77.                                         } else if len(str) > 300 && strings.Index(str, "Q ") >0 {
  78.                                                 // 根据 title 或 desc 来获得当前元素的标签
  79.                                                 reg := regexp.MustCompile(`<title>(.*)</title>.*\n.*<desc>(.*)</desc>`)
  80.                                                 p := reg.FindStringSubmatch(str)
  81.                                                 if len(p) > 0 {
  82.                                                         label := p[1]
  83.                                                         i := strings.Index(label, " ")
  84.                                                         if i >= 0 {
  85.                                                                 label = label[i+1:]
  86.                                                                 //fmt.Println("#" + p[1][:i] + "#")

  87.                                                         if p[1][:i] == "点" {
  88.                                                                         st := fmt.Sprintf(`<text x="%.2f" y="%.2f">%s</text>`, d1, d2, label)
  89.                                                                         parent.SetHtml(st)
  90.                                                                         //fmt.Println(p[1])
  91.                                                                 }
  92.                                                         } else {
  93.                                                                         st := fmt.Sprintf(`<text x="%.2f" y="%.2f">%s</text>`, d1, d2, p[2])
  94.                                                                         parent.SetHtml(st)
  95.                                                         }
  96.                                                 }
  97.                                         }
  98.                                 }
  99.                         }


  100.                 })

  101.                 // 写回文件
  102.                 dom.Find(`html`).Each(func(i int, selection *goquery.Selection) {
  103.                         outstr, _ := selection.Html()
  104.                         ioutil.WriteFile(fname, []byte(outstr[19:len(outstr)-29]), 0666)
  105.                 })
  106. }

  107. // 用法:sc.exe xxx.svg
  108. func main() {
  109.         fname := "C:\\Temp\\geogebra.svg"
  110.         if len(os.Args) > 1 {
  111.                         fname = os.Args[1]
  112.                         fmt.Println(fname)
  113.         }        

  114.         // 调用压缩函数
  115.         sc(fname)

  116.         // 调第三方工具 svgo 进行第二轮优化
  117.         cmd := exec.Command("C:\\...\\npm\\svgo.cmd", fname)
  118.         cmd.Run()       
  119. }
复制代码
Snipaste_2022-12-13_21-47-16.png
毋因群疑而阻独见  毋任己意而废人言
毋私小惠而伤大体  毋借公论以快私情
 楼主| 发表于 2022-12-15 06:57:30 | 显示全部楼层
本帖最后由 uk702 于 2022-12-15 07:16 编辑

声明:这里的优化专用于 geogebra 导出的 svg 优化,不适用其它 svg 文件,也不能重复应用于已经优化过的 geogebra 导出的 svg 。

更进一步,画各个点的代码改用 <circle> 语句,原来的 28k 文件现在进一步压缩为 2k,而整个图形(近乎)保持不变。

这里说的近乎保持不变(已知问题),主要有以下几点:
1)小数点精度这里始终保持 2 位;
2)不再区分自由点和非自由点,每个点始终用 blue 填充;
3)  某些点的位置及大小(font-size)发生微小变化。(bug,一般,暂时视作可忽略)
4)中文文本或 latex 文本内容可能显示会有些问题。(bug, 严重,暂时视作规避并待改进)

新代码如下(func main 不变就不贴出了)

  1. func sc(fname string) {
  2.         input, err := ioutil.ReadFile(fname)
  3.         content := string(input)

  4.         prevx, prevy := 0.0, 0.0
  5.        
  6.         // 这里 content 和 input 能保持一致,但后面对 content 进行 replace 之后,就不一致了
  7.         s := strings.Index(content, "viewBox=")
  8.         i := s+1
  9.         k := 0

  10.         for k != 2 {
  11.                 if input[i] == ' ' {
  12.                         k++
  13.                 }

  14.                 i++
  15.         }
  16.        
  17.         n := i
  18.         for content[n] != ' ' {
  19.                 n++
  20.         }
  21.         s1 := content[i:n]

  22.         n++
  23.         i = n
  24.         for content[n] != '"' {
  25.                 n++
  26.         }
  27.         s2 := content[i:n]
  28.         //fmt.Println(s1, s2)

  29.         v1, _ := strconv.ParseFloat(string(s1), 32)
  30.         v2, _ := strconv.ParseFloat(string(s2), 32)
  31.         // fmt.Println(v1, v2)

  32.         content = strings.Replace(content, "clipPath", "aaaaa", -1)
  33.         content = strings.Replace(content, `stroke-linecap="round"`, "", -1)
  34.         content = strings.Replace(content, `stroke-linejoin="round"`, "", -1)

  35.         re, _ := regexp.Compile(` clip-path="[^"].*"`)
  36.         content = re.ReplaceAllString(content, "")

  37.         re, _ = regexp.Compile(`\.(\d\d)(\d+)`)
  38.         content = re.ReplaceAllString(content, ".$1")


  39.         dom, err := goquery.NewDocumentFromReader(strings.NewReader(content))
  40.        
  41.         // 筛选含有transform属性的g元素并删除
  42.         // dom.Find(`g[transform]`).Each(func(i int, s *goquery.Selection) {
  43.         //   parent := s.Parent()
  44.         //   parent.Get(0).RemoveChild(s.Get(0))
  45.         // })
  46.        
  47.         dom.Find(`image`).Each(func(i int, s *goquery.Selection) {
  48.                 parent := s.Parent()
  49.                 parent.Get(0).RemoveChild(s.Get(0))
  50.         })

  51.         // 似乎不支持 dom.Find(`clipPath`),故先人为改成 aaaaa
  52.         dom.Find(`aaaaa`).Each(func(i int, s *goquery.Selection) {
  53.                 parent := s.Parent()
  54.                 parent.Get(0).RemoveChild(s.Get(0))
  55.         })

  56.         dom.Find(`path`).Each(func(i int, s *goquery.Selection) {
  57.                 parent := s.Parent()
  58.                 str, _ := parent.Html()

  59.                 if strings.Index(str, "<path d="M") >= 0 {
  60.                         dstr, _ := s.Attr("d")
  61.                        
  62.                         reg := regexp.MustCompile(`<path d="M ([\-0-9.]+) ([\-0-9.]+)`)
  63.                         p := reg.FindStringSubmatch(str)
  64.                         if len(p) > 1 {
  65.                                 d1, _ := strconv.ParseFloat(string(p[1]), 32)
  66.                                 d2, _ := strconv.ParseFloat(string(p[2]), 32)
  67.                                 if d1 > v1 || d2 > v2 {
  68.                                         //fmt.Println(d1, v1, d2, v2)
  69.                                         parent.Get(0).RemoveChild(s.Get(0))
  70.                                 } else if strings.Index(dstr, "Q ") > 0 {
  71.                                         reg := regexp.MustCompile(`<title>(.*)</title>.*\n.*<desc>(.*)</desc>`)
  72.                                         p := reg.FindStringSubmatch(str)
  73.                                         if len(p) > 0 {
  74.                                                 label := p[1]
  75.                                                 i := strings.Index(label, " ")
  76.                                                 if i >= 0 {
  77.                                                         label = label[i+1:]
  78.                                                         //fmt.Println("#" + p[1][:i] + "#")

  79.                                                         if p[1][:i] == "点" {
  80.                                                                 // 若标签如 M_2 之类的,表示带下标的 $M_2$ 格式
  81.                                                                 if strings.Index(label, "_") < 0 {
  82.                                                                         st := fmt.Sprintf(`<text x="%.2f" y="%.2f">%s</text>`, d1, d2, label)
  83.                                                                         parent.SetHtml(st)
  84.                                                                         //fmt.Println(p[1])
  85.                                                                 }
  86.                                                         }
  87.                                                 } else {
  88.                                                                 st := fmt.Sprintf(`<text x="%.2f" y="%.2f">%s</text>`, d1, d2, p[2])
  89.                                                                 parent.SetHtml(st)
  90.                                                 }
  91.                                         }
  92.                                 } else if strings.Index(dstr, "C ") > 0 {
  93.                                         // 将画点的代码改为用 <circle> 指令
  94.                                         reg := regexp.MustCompile(`<title>(.*)</title>.*\n.*<desc>(.*)</desc>`)
  95.                                         p := reg.FindStringSubmatch(str)
  96.                                         if len(p) > 0 {
  97.                                                 label := p[1]
  98.                                                 i := strings.Index(label, " ")
  99.                                                 if i >= 0 {
  100.                                                         label = label[i+1:]
  101.                                                         //fmt.Println("#" + p[1][:i] + "#")

  102.                                                         if p[1][:i] == "点" {
  103.                                                                 if d1 == prevx && d2 == prevy {
  104.                                                                         parent.Get(0).RemoveChild(s.Get(0))
  105.                                                                 } else {       
  106.                                                                         prevx, prevy = d1, d2
  107.                                                                         st := fmt.Sprintf(`<circle cx="%.2f" cy="%.2f" r="5" fill="blue"/> `, d1-3.0, d2)
  108.                                                                         parent.SetHtml(st)
  109.                                                                 }       
  110.                                                         }
  111.                                                 } else {
  112.                                                         //这种情况需要进一步跟踪
  113.                                                         // st := fmt.Sprintf(`<text x="%.2f" y="%.2f">%s</text>`, d1, d2, p[2])
  114.                                                         // parent.SetHtml(st)
  115.                                                 }
  116.                                         }
  117.                                 }

  118.                         }
  119.                 }


  120.         })

  121.         dom.Find(`html`).Each(func(i int, selection *goquery.Selection) {
  122.                 outstr, _ := selection.Html()
  123.                 ioutil.WriteFile(fname, []byte(outstr[19:len(outstr)-29]), 0666)
  124.         })
  125. }
复制代码
毋因群疑而阻独见  毋任己意而废人言
毋私小惠而伤大体  毋借公论以快私情
您需要登录后才可以回帖 登录 | 欢迎注册

本版积分规则

小黑屋|手机版|数学研发网 ( 苏ICP备07505100号 )

GMT+8, 2024-4-28 07:24 , Processed in 0.054083 second(s), 19 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表