hrefspace

 找回密码
 立即注册
搜索
热搜: PHP PS 程序设计
查看: 479|回复: 1

Geogebra 导出的 svg 文件瘦身

[复制链接]

585

主题

769

帖子

2007

积分

大司空

Rank: 5Rank: 5

积分
2007
发表于 2023-10-3 08:39:48 | 显示全部楼层 |阅读模式
我们知道,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 mainimport (        "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[i] == ' ' {                                k++                        }                         i++                }                n := i                for content[n] != ' ' {                        n++                }                s1 := content[i:n]                n++                i = n                for content[n] != '"' {                        n++                }                s2 := content[i:n]                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[1]), 32)                                        d2, _ := strconv.ParseFloat(string(p[2]), 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[1]                                                        i := strings.Index(label, " ")                                                        if i >= 0 {                                                                label = label[i+1:]                                                                //fmt.Println("#" + p[1][:i] + "#")                                                        if p[1][:i] == "点" {                                                                        st := fmt.Sprintf(`<text x="%.2f" y="%.2f">%s</text>`, d1, d2, label)                                                                        parent.SetHtml(st)                                                                        //fmt.Println(p[1])                                                                }                                                        } else {                                                                        st := fmt.Sprintf(`<text x="%.2f" y="%.2f">%s</text>`, d1, d2, p[2])                                                                        parent.SetHtml(st)                                                        }                                                }                                        }                                }                        }                })                // 写回文件                dom.Find(`html`).Each(func(i int, selection *goquery.Selection) {                        outstr, _ := selection.Html()                        ioutil.WriteFile(fname, []byte(outstr[19:len(outstr)-29]), 0666)                })}// 用法:sc.exe xxx.svgfunc main() {        fname := "C:\\Temp\\geogebra.svg"        if len(os.Args) > 1 {                        fname = os.Args[1]                        fmt.Println(fname)        }                 // 调用压缩函数        sc(fname)        // 调第三方工具 svgo 进行第二轮优化        cmd := exec.Command("C:\\...\\npm\\svgo.cmd", fname)        cmd.Run()        }
复制代码

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

0

主题

201

帖子

2

积分

新手上路

Rank: 1

积分
2
发表于 2023-10-3 08:40:06 | 显示全部楼层
声明:这里的优化专用于 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) {        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[i] == ' ' {                        k++                }                 i++        }                n := i        for content[n] != ' ' {                n++        }        s1 := content[i:n]        n++        i = n        for content[n] != '"' {                n++        }        s2 := content[i:n]        //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[transform]`).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[1]), 32)                                d2, _ := strconv.ParseFloat(string(p[2]), 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[1]                                                i := strings.Index(label, " ")                                                if i >= 0 {                                                        label = label[i+1:]                                                        //fmt.Println("#" + p[1][:i] + "#")                                                        if p[1][: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[1])                                                                }                                                        }                                                } else {                                                                st := fmt.Sprintf(`<text x="%.2f" y="%.2f">%s</text>`, d1, d2, p[2])                                                                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[1]                                                i := strings.Index(label, " ")                                                if i >= 0 {                                                        label = label[i+1:]                                                        //fmt.Println("#" + p[1][:i] + "#")                                                        if p[1][: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[2])                                                        // parent.SetHtml(st)                                                }                                        }                                }                        }                }        })        dom.Find(`html`).Each(func(i int, selection *goquery.Selection) {                outstr, _ := selection.Html()                ioutil.WriteFile(fname, []byte(outstr[19:len(outstr)-29]), 0666)        })}
复制代码
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|hrefspace

GMT+8, 2024-11-24 20:45 , Processed in 0.061135 second(s), 23 queries .

Powered by hrefspace X3.4 Licensed

Copyright © 2022, hrefspace.

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