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 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]