add throughput, format code, and begin to add MUC preview window
@@ -115,6 +115,10 @@ var commentB64 string = base64.StdEncoding.EncodeToString(commentBytes)
|
|||||||
var informationBytes []byte
|
var informationBytes []byte
|
||||||
var informationB64 string = base64.StdEncoding.EncodeToString(informationBytes)
|
var informationB64 string = base64.StdEncoding.EncodeToString(informationBytes)
|
||||||
|
|
||||||
|
//go:embed assets/car.png
|
||||||
|
var carBytes []byte
|
||||||
|
var carB64 string = base64.StdEncoding.EncodeToString(carBytes)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
||||||
loader := gdkpixbuf.NewPixbufLoader()
|
loader := gdkpixbuf.NewPixbufLoader()
|
||||||
@@ -331,4 +335,12 @@ func init() {
|
|||||||
loader.Close()
|
loader.Close()
|
||||||
|
|
||||||
clientAssets["status_"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
clientAssets["status_"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
||||||
|
|
||||||
|
loader = gdkpixbuf.NewPixbufLoader()
|
||||||
|
|
||||||
|
carData, _ := base64.StdEncoding.DecodeString(carB64)
|
||||||
|
loader.Write(carData)
|
||||||
|
loader.Close()
|
||||||
|
|
||||||
|
clientAssets["car"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 671 B |
|
After Width: | Height: | Size: 593 B |
|
After Width: | Height: | Size: 935 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 970 B |
|
After Width: | Height: | Size: 508 B |
|
After Width: | Height: | Size: 612 B |
|
After Width: | Height: | Size: 806 B |
|
After Width: | Height: | Size: 622 B |
|
After Width: | Height: | Size: 924 B |
|
After Width: | Height: | Size: 882 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 903 B |
@@ -55,8 +55,8 @@ func switchToTab(jid string, w *gtk.Window) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
typed_tab := tab.(*chatTab)
|
typed_tab := tab.(*chatTab)
|
||||||
|
|
||||||
scroller.SetChild(typed_tab.msgs)
|
scroller.SetChild(typed_tab.msgs)
|
||||||
|
typingStatus.SetText("")
|
||||||
if typed_tab.isMuc {
|
if typed_tab.isMuc {
|
||||||
|
|
||||||
m, ok := mucmembers.Load(jid)
|
m, ok := mucmembers.Load(jid)
|
||||||
@@ -490,7 +490,7 @@ func showErrorDialog(err error) {
|
|||||||
func createIdenticon(word string) *gtk.Image { // This function generates an identicon
|
func createIdenticon(word string) *gtk.Image { // This function generates an identicon
|
||||||
if !loadedConfig.Identicons {
|
if !loadedConfig.Identicons {
|
||||||
i := gtk.NewImageFromPaintable(clientAssets["DefaultAvatar"])
|
i := gtk.NewImageFromPaintable(clientAssets["DefaultAvatar"])
|
||||||
i.AddCSSClass(loadedConfig.CVD.String()+"_CVD")
|
i.AddCSSClass(loadedConfig.CVD.String() + "_CVD")
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter {
|
|||||||
if ok {
|
if ok {
|
||||||
b := gtk.NewBox(gtk.OrientationHorizontal, 0)
|
b := gtk.NewBox(gtk.OrientationHorizontal, 0)
|
||||||
b.Append(gtk.NewLabel(fmt.Sprintf("%s is typing...", JidMustParse(m.From).Resource)))
|
b.Append(gtk.NewLabel(fmt.Sprintf("%s is typing...", JidMustParse(m.From).Resource)))
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.Error.Type != "" {
|
if m.Error.Type != "" {
|
||||||
@@ -120,7 +120,7 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter {
|
|||||||
quote.ConnectClicked(func() {
|
quote.ConnectClicked(func() {
|
||||||
lines := strings.Split(m.Body, "\n")
|
lines := strings.Split(m.Body, "\n")
|
||||||
for i, line := range lines {
|
for i, line := range lines {
|
||||||
quoteline:= "> " + line
|
quoteline := "> " + line
|
||||||
lines[i] = quoteline
|
lines[i] = quoteline
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter {
|
|||||||
im.AddCSSClass("author_img")
|
im.AddCSSClass("author_img")
|
||||||
authorBox.Append(im)
|
authorBox.Append(im)
|
||||||
} else {
|
} else {
|
||||||
im := createIdenticon(m.From)
|
im := createIdenticon(m.From)
|
||||||
im.SetPixelSize(40)
|
im.SetPixelSize(40)
|
||||||
im.AddCSSClass("author_img")
|
im.AddCSSClass("author_img")
|
||||||
authorBox.Append(im)
|
authorBox.Append(im)
|
||||||
@@ -260,18 +260,18 @@ func getAvatar(j, hash string) *gtk.Image { // TODO: This function probably shou
|
|||||||
oghash := hash
|
oghash := hash
|
||||||
p, err := ensureCache()
|
p, err := ensureCache()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return createIdenticon(j)
|
return createIdenticon(j)
|
||||||
}
|
}
|
||||||
|
|
||||||
if hash == "" {
|
if hash == "" {
|
||||||
fmt.Println("Hash is nil!")
|
fmt.Println("Hash is nil!")
|
||||||
return createIdenticon(j)
|
return createIdenticon(j)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, ok := invalidImages[hash]
|
_, ok := invalidImages[hash]
|
||||||
if ok {
|
if ok {
|
||||||
fmt.Println("Image is invalid")
|
fmt.Println("Image is invalid")
|
||||||
return createIdenticon(j)
|
return createIdenticon(j)
|
||||||
}
|
}
|
||||||
|
|
||||||
hash = filepath.Join(p, sanitizefilename.Sanitize(hash))
|
hash = filepath.Join(p, sanitizefilename.Sanitize(hash))
|
||||||
@@ -279,7 +279,7 @@ func getAvatar(j, hash string) *gtk.Image { // TODO: This function probably shou
|
|||||||
_, err = os.ReadFile(hash)
|
_, err = os.ReadFile(hash)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
i := newImageFromPath(hash)
|
i := newImageFromPath(hash)
|
||||||
i.AddCSSClass(loadedConfig.CVD.String()+"_CVD")
|
i.AddCSSClass(loadedConfig.CVD.String() + "_CVD")
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,14 +305,14 @@ func getAvatar(j, hash string) *gtk.Image { // TODO: This function probably shou
|
|||||||
result := <-mychan
|
result := <-mychan
|
||||||
card, ok := result.Payload.(*VCard)
|
card, ok := result.Payload.(*VCard)
|
||||||
if !ok {
|
if !ok {
|
||||||
return createIdenticon(j)
|
return createIdenticon(j)
|
||||||
}
|
}
|
||||||
|
|
||||||
base64_data := card.Photo.Binval
|
base64_data := card.Photo.Binval
|
||||||
if card.Photo.Binval == "" || ((card.Photo.Type == "image/svg+xml" || card.Photo.Type == "image/webp") && (runtime.GOOS == "windows" || runtime.GOOS == "netbsd")) {
|
if card.Photo.Binval == "" || ((card.Photo.Type == "image/svg+xml" || card.Photo.Type == "image/webp") && (runtime.GOOS == "windows" || runtime.GOOS == "netbsd")) {
|
||||||
fmt.Println("Blocking image")
|
fmt.Println("Blocking image")
|
||||||
invalidImages[oghash] = true
|
invalidImages[oghash] = true
|
||||||
return createIdenticon(j)
|
return createIdenticon(j)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := base64.StdEncoding.DecodeString(base64_data)
|
data, err := base64.StdEncoding.DecodeString(base64_data)
|
||||||
@@ -326,6 +326,6 @@ func getAvatar(j, hash string) *gtk.Image { // TODO: This function probably shou
|
|||||||
}
|
}
|
||||||
|
|
||||||
i := newImageFromPath(hash)
|
i := newImageFromPath(hash)
|
||||||
i.AddCSSClass(loadedConfig.CVD.String()+"_CVD")
|
i.AddCSSClass(loadedConfig.CVD.String() + "_CVD")
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,25 +2,24 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
|
||||||
"sort"
|
"sort"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
func rangeOrdered(m *sync.Map, fn func(k, v any) bool) {
|
func rangeOrdered(m *sync.Map, fn func(k, v any) bool) {
|
||||||
var keys []string
|
var keys []string
|
||||||
|
|
||||||
m.Range(func(k, v any) bool {
|
m.Range(func(k, v any) bool {
|
||||||
keys = append(keys, k.(string))
|
keys = append(keys, k.(string))
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
sort.Strings(keys)
|
sort.Strings(keys)
|
||||||
|
|
||||||
for _, k := range keys {
|
for _, k := range keys {
|
||||||
v, _ := m.Load(k)
|
v, _ := m.Load(k)
|
||||||
if !fn(k, v) {
|
if !fn(k, v) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ var connectionIcon *gtk.Image
|
|||||||
var mStatus *gtk.Label
|
var mStatus *gtk.Label
|
||||||
var mIcon *gtk.Image
|
var mIcon *gtk.Image
|
||||||
|
|
||||||
|
var sStatus *gtk.Label
|
||||||
|
var sIcon *gtk.Image
|
||||||
|
|
||||||
var typingStatus *gtk.Label
|
var typingStatus *gtk.Label
|
||||||
|
|
||||||
var pingStatus *gtk.Label
|
var pingStatus *gtk.Label
|
||||||
@@ -91,6 +94,14 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Setup log
|
||||||
|
xmlLog, err := os.CreateTemp("", "xmpp-log")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer os.Remove(xmlLog.Name())
|
||||||
|
|
||||||
pingTimes = append(pingTimes, []float64{})
|
pingTimes = append(pingTimes, []float64{})
|
||||||
p, err := ensureConfig()
|
p, err := ensureConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -120,12 +131,14 @@ func main() {
|
|||||||
CharsetReader: func(c string, input io.Reader) (io.Reader, error) {
|
CharsetReader: func(c string, input io.Reader) (io.Reader, error) {
|
||||||
return charset.NewReaderLabel(c, input)
|
return charset.NewReaderLabel(c, input)
|
||||||
},
|
},
|
||||||
|
ConnectTimeout: 300,
|
||||||
},
|
},
|
||||||
Jid: loadedConfig.Username + "/" + loadedConfig.Resource,
|
Jid: loadedConfig.Username + "/" + loadedConfig.Resource,
|
||||||
Credential: xmpp.Password(loadedConfig.Password),
|
Credential: xmpp.Password(loadedConfig.Password),
|
||||||
Insecure: loadedConfig.Insecure,
|
Insecure: loadedConfig.Insecure,
|
||||||
// StreamLogger: os.Stdout,
|
|
||||||
StreamManagementEnable: true,
|
StreamManagementEnable: true,
|
||||||
|
ConnectTimeout: 300,
|
||||||
|
StreamLogger: xmlLog,
|
||||||
}
|
}
|
||||||
router := xmpp.NewRouter()
|
router := xmpp.NewRouter()
|
||||||
|
|
||||||
@@ -276,7 +289,9 @@ func main() {
|
|||||||
|
|
||||||
if ok {
|
if ok {
|
||||||
typed_tab.msgs.Append(b)
|
typed_tab.msgs.Append(b)
|
||||||
scrollToBottomAfterUpdate(scroller)
|
if current == JidMustParse(m.From).Bare() {
|
||||||
|
scrollToBottomAfterUpdate(scroller)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Got message when the tab does not exist!")
|
fmt.Println("Got message when the tab does not exist!")
|
||||||
}
|
}
|
||||||
@@ -340,7 +355,9 @@ func main() {
|
|||||||
|
|
||||||
if ok {
|
if ok {
|
||||||
typed_tab.msgs.Append(b)
|
typed_tab.msgs.Append(b)
|
||||||
scrollToBottomAfterUpdate(scroller)
|
if current == muc {
|
||||||
|
scrollToBottomAfterUpdate(scroller)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Got message when the tab does not exist!")
|
fmt.Println("Got message when the tab does not exist!")
|
||||||
}
|
}
|
||||||
@@ -426,28 +443,49 @@ func main() {
|
|||||||
|
|
||||||
cm := xmpp.NewStreamManager(c, func(c xmpp.Sender) {
|
cm := xmpp.NewStreamManager(c, func(c xmpp.Sender) {
|
||||||
fmt.Println("XMPP client connected")
|
fmt.Println("XMPP client connected")
|
||||||
|
// Ping
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
pingStatus.AddCSSClass("pending")
|
go func() {
|
||||||
before := time.Now()
|
pingStatus.AddCSSClass("pending")
|
||||||
iq := new(stanza.IQ)
|
before := time.Now()
|
||||||
iq.From = clientroot.Session.BindJid
|
iq := new(stanza.IQ)
|
||||||
iq.To = iq.From
|
iq.From = clientroot.Session.BindJid
|
||||||
iq.Type = "get"
|
iq.To = iq.From
|
||||||
|
iq.Type = "get"
|
||||||
|
|
||||||
ctx, _ := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, _ := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
mychan, err := client.SendIQ(ctx, iq)
|
mychan, err := client.SendIQ(ctx, iq)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = <-mychan
|
||||||
|
|
||||||
|
pingStatus.RemoveCSSClass("pending")
|
||||||
|
delay := time.Since(before) / time.Millisecond
|
||||||
|
pingStatus.SetText(fmt.Sprintf("%d ms", delay))
|
||||||
|
pingTimes[0] = append(pingTimes[0], float64(delay))
|
||||||
|
}()
|
||||||
|
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Throughput
|
||||||
|
var oldsize int64
|
||||||
|
var newsize int64
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
stat, err := xmlLog.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
panic(err)
|
||||||
}
|
}
|
||||||
_ = <-mychan
|
|
||||||
|
|
||||||
pingStatus.RemoveCSSClass("pending")
|
|
||||||
delay := time.Since(before) / time.Millisecond
|
|
||||||
pingStatus.SetText(fmt.Sprintf("%d ms", delay))
|
|
||||||
pingTimes[0] = append(pingTimes[0], float64(delay))
|
|
||||||
|
|
||||||
|
newsize = stat.Size()
|
||||||
|
diff := float64(newsize-oldsize) / 1000
|
||||||
|
sStatus.SetText(fmt.Sprintf("%.2fKB/s", diff))
|
||||||
|
oldsize = stat.Size()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
connectionStatus.SetText(fmt.Sprintf("Connected as %s", JidMustParse(clientroot.Session.BindJid).Bare()))
|
connectionStatus.SetText(fmt.Sprintf("Connected as %s", JidMustParse(clientroot.Session.BindJid).Bare()))
|
||||||
@@ -473,42 +511,55 @@ func main() {
|
|||||||
res, ok := result.Payload.(*stanza.PubSubGeneric)
|
res, ok := result.Payload.(*stanza.PubSubGeneric)
|
||||||
if ok {
|
if ok {
|
||||||
for _, item := range res.Items.List {
|
for _, item := range res.Items.List {
|
||||||
go func() {
|
jid := item.Id
|
||||||
jid := item.Id
|
node := item.Any
|
||||||
node := item.Any
|
autojoin := false
|
||||||
autojoin := false
|
for _, attr := range node.Attrs {
|
||||||
nick := loadedConfig.Nick
|
if attr.Name.Local == "autojoin" {
|
||||||
for _, attr := range node.Attrs {
|
autojoin = attr.Value == "true"
|
||||||
if attr.Name.Local == "autojoin" {
|
|
||||||
autojoin = attr.Value == "true"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, node := range node.Nodes {
|
_, ok := tabs.Load(jid)
|
||||||
if node.XMLName.Local == "nick" {
|
if !ok && autojoin {
|
||||||
nick = node.Content
|
createTab(jid, true)
|
||||||
}
|
b := gtk.NewLabel(jid)
|
||||||
|
gesture1 := gtk.NewGestureClick()
|
||||||
|
gesture1.SetButton(1)
|
||||||
|
gesture1.Connect("pressed", func() {
|
||||||
|
switchToTab(jid, &window.Window)
|
||||||
|
})
|
||||||
|
|
||||||
|
b.AddController(gesture1)
|
||||||
|
menu.Append(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range res.Items.List {
|
||||||
|
|
||||||
|
jid := item.Id
|
||||||
|
node := item.Any
|
||||||
|
autojoin := false
|
||||||
|
nick := loadedConfig.Nick
|
||||||
|
for _, attr := range node.Attrs {
|
||||||
|
if attr.Name.Local == "autojoin" {
|
||||||
|
autojoin = attr.Value == "true"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_, ok := tabs.Load(jid)
|
for _, node := range node.Nodes {
|
||||||
if !ok && autojoin {
|
if node.XMLName.Local == "nick" {
|
||||||
err := joinMuc(client, clientroot.Session.BindJid, jid, nick)
|
nick = node.Content
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
createTab(jid, true)
|
|
||||||
b := gtk.NewLabel(jid)
|
|
||||||
gesture1 := gtk.NewGestureClick()
|
|
||||||
gesture1.SetButton(1)
|
|
||||||
gesture1.Connect("pressed", func() {
|
|
||||||
switchToTab(jid, &window.Window)
|
|
||||||
})
|
|
||||||
|
|
||||||
b.AddController(gesture1)
|
|
||||||
menu.Append(b)
|
|
||||||
}
|
}
|
||||||
}()
|
}
|
||||||
|
|
||||||
|
if autojoin {
|
||||||
|
err := joinMuc(client, clientroot.Session.BindJid, jid, nick)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -693,21 +744,21 @@ func activate(app *gtk.Application) {
|
|||||||
_, ok := tabs.Load(t)
|
_, ok := tabs.Load(t)
|
||||||
jm := func() {
|
jm := func() {
|
||||||
|
|
||||||
err := joinMuc(client, clientroot.Session.BindJid, t, nick_entry.Text())
|
err := joinMuc(client, clientroot.Session.BindJid, t, nick_entry.Text())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
createTab(t, true)
|
createTab(t, true)
|
||||||
b := gtk.NewLabel(t)
|
b := gtk.NewLabel(t)
|
||||||
gesture1 := gtk.NewGestureClick()
|
gesture1 := gtk.NewGestureClick()
|
||||||
gesture1.SetButton(1)
|
gesture1.SetButton(1)
|
||||||
gesture1.Connect("pressed", func() {
|
gesture1.Connect("pressed", func() {
|
||||||
switchToTab(t, &window.Window)
|
switchToTab(t, &window.Window)
|
||||||
})
|
})
|
||||||
|
|
||||||
b.AddController(gesture1)
|
b.AddController(gesture1)
|
||||||
menu.Append(b)
|
menu.Append(b)
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
// First check the MUC's disco and see if it's semianon
|
// First check the MUC's disco and see if it's semianon
|
||||||
@@ -907,6 +958,15 @@ func activate(app *gtk.Application) {
|
|||||||
pBox.Append(pingStatus)
|
pBox.Append(pingStatus)
|
||||||
statBar.Append(pBox)
|
statBar.Append(pBox)
|
||||||
|
|
||||||
|
sBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
|
||||||
|
sIcon = gtk.NewImageFromPaintable(clientAssets["car"])
|
||||||
|
sIcon.AddCSSClass("icon")
|
||||||
|
sStatus = gtk.NewLabel("-")
|
||||||
|
sBox.Append(sIcon)
|
||||||
|
sBox.Append(sStatus)
|
||||||
|
sStatus.SetTooltipText("Throughput of your XMPP connection in KB/s")
|
||||||
|
statBar.Append(sBox)
|
||||||
|
|
||||||
scrollerStatBar := gtk.NewScrolledWindow()
|
scrollerStatBar := gtk.NewScrolledWindow()
|
||||||
scrollerStatBar.SetChild(statBar)
|
scrollerStatBar.SetChild(statBar)
|
||||||
box.Append(scrollerStatBar)
|
box.Append(scrollerStatBar)
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
|
||||||
"image/png"
|
|
||||||
"github.com/srwiley/oksvg"
|
"github.com/srwiley/oksvg"
|
||||||
"github.com/srwiley/rasterx"
|
"github.com/srwiley/rasterx"
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SVGToPNG(svgData []byte) ([]byte, error) {
|
func SVGToPNG(svgData []byte) ([]byte, error) {
|
||||||
|
|||||||