uk702 发表于 2022-12-14 14:59:14

Geogebra 导出的 svg 文件瘦身

我们知道,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 。


下面是参考代码:

package main

import (
        "fmt"
        "io/ioutil"
        "os"
        "os/exec"
        "regexp"
        "strings"
        "strconv"
        "unsafe"

        "github.com/PuerkitoBio/goquery"
)

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

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

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

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

                for k != 2 {
                        if input == ' ' {
                                k++
                        }

                        i++
                }

                n := i
                for content != ' ' {
                        n++
                }
                s1 := content

                n++
                i = n
                for content != '"' {
                        n++
                }
                s2 := content

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

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

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

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

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

                                                        if p[:i] == "点" {
                                                                        st := fmt.Sprintf(`<text x="%.2f" y="%.2f">%s</text>`, d1, d2, label)
                                                                        parent.SetHtml(st)
                                                                        //fmt.Println(p)
                                                                }
                                                        } else {
                                                                        st := fmt.Sprintf(`<text x="%.2f" y="%.2f">%s</text>`, d1, d2, p)
                                                                        parent.SetHtml(st)
                                                        }
                                                }
                                        }
                                }
                        }


                })

                // 写回文件
                dom.Find(`html`).Each(func(i int, selection *goquery.Selection) {
                        outstr, _ := selection.Html()
                        ioutil.WriteFile(fname, []byte(outstr), 0666)
                })
}

// 用法:sc.exe xxx.svg
func main() {
        fname := "C:\\Temp\\geogebra.svg"
        if len(os.Args) > 1 {
                        fname = os.Args
                        fmt.Println(fname)
        }        

        // 调用压缩函数
        sc(fname)

        // 调第三方工具 svgo 进行第二轮优化
        cmd := exec.Command("C:\\...\\npm\\svgo.cmd", fname)
        cmd.Run()       
}

uk702 发表于 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 不变就不贴出了)

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

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

        for k != 2 {
                if input == ' ' {
                        k++
                }

                i++
        }
       
        n := i
        for content != ' ' {
                n++
        }
        s1 := content

        n++
        i = n
        for content != '"' {
                n++
        }
        s2 := content
        //fmt.Println(s1, s2)

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

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

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

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


        dom, err := goquery.NewDocumentFromReader(strings.NewReader(content))
       
        // 筛选含有transform属性的g元素并删除
        // dom.Find(`g`).Each(func(i int, s *goquery.Selection) {
        //   parent := s.Parent()
        //   parent.Get(0).RemoveChild(s.Get(0))
        // })
       
        dom.Find(`image`).Each(func(i int, s *goquery.Selection) {
                parent := s.Parent()
                parent.Get(0).RemoveChild(s.Get(0))
        })

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

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

                if strings.Index(str, "<path d=\"M") >= 0 {
                        dstr, _ := s.Attr("d")
                       
                        reg := regexp.MustCompile(`<path d="M ([\-0-9.]+) ([\-0-9.]+)`)
                        p := reg.FindStringSubmatch(str)
                        if len(p) > 1 {
                                d1, _ := strconv.ParseFloat(string(p), 32)
                                d2, _ := strconv.ParseFloat(string(p), 32)
                                if d1 > v1 || d2 > v2 {
                                        //fmt.Println(d1, v1, d2, v2)
                                        parent.Get(0).RemoveChild(s.Get(0))
                                } else if strings.Index(dstr, "Q ") > 0 {
                                        reg := regexp.MustCompile(`<title>(.*)</title>.*\n.*<desc>(.*)</desc>`)
                                        p := reg.FindStringSubmatch(str)
                                        if len(p) > 0 {
                                                label := p
                                                i := strings.Index(label, " ")
                                                if i >= 0 {
                                                        label = label
                                                        //fmt.Println("#" + p[:i] + "#")

                                                        if p[:i] == "点" {
                                                                // 若标签如 M_2 之类的,表示带下标的 $M_2$ 格式
                                                                if strings.Index(label, "_") < 0 {
                                                                        st := fmt.Sprintf(`<text x="%.2f" y="%.2f">%s</text>`, d1, d2, label)
                                                                        parent.SetHtml(st)
                                                                        //fmt.Println(p)
                                                                }
                                                        }
                                                } else {
                                                                st := fmt.Sprintf(`<text x="%.2f" y="%.2f">%s</text>`, d1, d2, p)
                                                                parent.SetHtml(st)
                                                }
                                        }
                                } else if strings.Index(dstr, "C ") > 0 {
                                        // 将画点的代码改为用 <circle> 指令
                                        reg := regexp.MustCompile(`<title>(.*)</title>.*\n.*<desc>(.*)</desc>`)
                                        p := reg.FindStringSubmatch(str)
                                        if len(p) > 0 {
                                                label := p
                                                i := strings.Index(label, " ")
                                                if i >= 0 {
                                                        label = label
                                                        //fmt.Println("#" + p[:i] + "#")

                                                        if p[:i] == "点" {
                                                                if d1 == prevx && d2 == prevy {
                                                                        parent.Get(0).RemoveChild(s.Get(0))
                                                                } else {       
                                                                        prevx, prevy = d1, d2
                                                                        st := fmt.Sprintf(`<circle cx="%.2f" cy="%.2f" r="5" fill="blue"/> `, d1-3.0, d2)
                                                                        parent.SetHtml(st)
                                                                }       
                                                        }
                                                } else {
                                                        //这种情况需要进一步跟踪
                                                        // st := fmt.Sprintf(`<text x="%.2f" y="%.2f">%s</text>`, d1, d2, p)
                                                        // parent.SetHtml(st)
                                                }
                                        }
                                }

                        }
                }


        })

        dom.Find(`html`).Each(func(i int, selection *goquery.Selection) {
                outstr, _ := selection.Html()
                ioutil.WriteFile(fname, []byte(outstr), 0666)
        })
}
页: [1]
查看完整版本: Geogebra 导出的 svg 文件瘦身