Compare commits

..

3 Commits

Author SHA1 Message Date
6b61fe1e54 feat(fonts): add font endpoints
All checks were successful
build / Go-Build (push) Successful in 27s
2026-02-12 15:41:23 +01:00
35e873397d Merge pull request 'Add fonts storage and meta parsing' (#11) from feat/fonts into dev
Some checks failed
build / Go-Build (push) Failing after 12s
Reviewed-on: #11
2026-02-12 13:31:52 +00:00
7a9cf7ab93 feat(fonts): add fonts storage
All checks were successful
build / Go-Build (pull_request) Successful in 1m24s
build / Go-Build (push) Successful in 38s
2026-02-12 14:16:37 +01:00
8 changed files with 122 additions and 152 deletions

View File

@@ -5,13 +5,9 @@ import (
"tomatentum.net/svg-templater/internal/command" "tomatentum.net/svg-templater/internal/command"
"tomatentum.net/svg-templater/internal/database" "tomatentum.net/svg-templater/internal/database"
"tomatentum.net/svg-templater/pkg/format"
) )
func main() { func main() {
if !format.CheckInkscape() {
panic("Inkscape not found")
}
if err := database.OpenSQLite(); err != nil { if err := database.OpenSQLite(); err != nil {
log.Fatal("Failed opening DB:\n", err) log.Fatal("Failed opening DB:\n", err)
return return

4
go.mod
View File

@@ -4,11 +4,13 @@ go 1.24.9
require ( require (
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/glebarez/go-sqlite v1.22.0
github.com/google/uuid v1.5.0 // indirect github.com/google/uuid v1.5.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/image v0.36.0
golang.org/x/sys v0.15.0 // indirect golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.34.0 // indirect
modernc.org/libc v1.37.6 // indirect modernc.org/libc v1.37.6 // indirect
modernc.org/mathutil v1.6.0 // indirect modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect modernc.org/memory v1.7.2 // indirect

4
go.sum
View File

@@ -8,9 +8,13 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw= modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw=
modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=

View File

@@ -30,7 +30,7 @@ func HandleCommandline() {
} else if help { } else if help {
flag.PrintDefaults() flag.PrintDefaults()
} else { } else {
svg.Storage = svg.NewFileStorage(datapath, "public") svg.Storage = svg.NewFileStorage(datapath, "fonts")
server.PrepareHTTP() server.PrepareHTTP()
server.Start() server.Start()
} }

45
internal/routes/font.go Normal file
View File

@@ -0,0 +1,45 @@
package routes
import (
"encoding/json"
"log"
"net/http"
"strings"
"tomatentum.net/svg-templater/pkg/svg"
)
func AddFont(w http.ResponseWriter, r *http.Request) {
contentType := r.Header.Get("Content-Type")
if contentType != "font/ttf" && contentType != "font/otf" {
http.Error(w, "", http.StatusUnsupportedMediaType)
return
}
log.Println("Received font add request")
format := strings.TrimPrefix(contentType, "font/")
if err := svg.Storage.AddFont(r.Body, format); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}
}
func GetFonts(w http.ResponseWriter, r *http.Request) {
log.Println("Serving all available fonts")
fonts, err := svg.Storage.GetFonts()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}
json, err := json.Marshal(fonts)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}
w.Header().Add("Content-Type", "application/json")
w.Write(json)
}

View File

@@ -4,26 +4,30 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"path/filepath"
"tomatentum.net/svg-templater/internal/routes" "tomatentum.net/svg-templater/internal/routes"
"tomatentum.net/svg-templater/pkg/auth" "tomatentum.net/svg-templater/pkg/auth"
"tomatentum.net/svg-templater/pkg/svg"
) )
func PrepareHTTP() { func PrepareHTTP() {
registerAuthorizedFunc("/", func(w http.ResponseWriter, r *http.Request) { registerAuthorized("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "You are authorized!") fmt.Fprintln(w, "You are authorized!")
}) })
registerAuthorizedFunc("/svg/", func(w http.ResponseWriter, r *http.Request) { registerAuthorized("/svg/", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" { if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return return
} }
routes.CreateSVG(w, *r) routes.CreateSVG(w, *r)
}) })
registerAuthorized("/font/", func(w http.ResponseWriter, r *http.Request) {
registerAuthorized("/public/", http.StripPrefix("/public/", http.FileServer(svg.Storage.GetPublicDir()))) switch r.Method {
case "GET":
routes.GetFonts(w, r)
case "POST":
routes.AddFont(w, r)
}
})
} }
func Start() { func Start() {
@@ -33,15 +37,7 @@ func Start() {
} }
} }
func registerAuthorized(path string, handler http.Handler) { func registerAuthorized(path string, f func(w http.ResponseWriter, r *http.Request)) {
http.HandleFunc(path, auth.AuthMiddleware(handler)) http.HandleFunc(path, auth.AuthMiddleware(http.HandlerFunc(f)))
log.Println("Registered authorized handler for", path) log.Println("Registered authorized handler for", path)
} }
func registerAuthorizedFunc(path string, f func(w http.ResponseWriter, r *http.Request)) {
registerAuthorized(path, http.HandlerFunc(f))
}
func GetPublicPath(path string) string {
return filepath.Join("public", path)
}

View File

@@ -1,97 +0,0 @@
package format
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"tomatentum.net/svg-templater/pkg/svg"
)
type ConversionParameters struct {
Format string
Width, Height int
}
func ConvertByte(svgblob []byte, param ConversionParameters) ([]byte, error) {
reader, err := ConvertReader(svgblob, param)
if err != nil {
return nil, err
}
defer reader.Close()
result, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
return result, nil
}
func ConvertReader(svgblob []byte, param ConversionParameters) (io.ReadCloser, error) {
if param.Format == "svg" {
return io.NopCloser(bytes.NewReader(svgblob)), nil
}
file, err := svg.CreateTemp(bytes.NewReader(svgblob), "svg")
if err != nil {
return nil, err
}
defer os.Remove(file)
return runCommand(file, param)
}
func runCommand(input string, param ConversionParameters) (io.ReadCloser, error) {
var args []string
var outExt string
switch param.Format {
case "png":
args = append(args, "--export-type=png")
outExt = ".png"
case "jpg":
args = append(args, "--export-type=png") // inkscape doesn't export jpg directly
outExt = ".png"
case "pdf":
args = append(args, "--export-type=pdf")
outExt = ".pdf"
default:
return nil, fmt.Errorf("format not supported: %s", param.Format)
}
if param.Width > 0 {
args = append(args, "-w", strconv.Itoa(param.Width))
}
if param.Height > 0 {
args = append(args, "-h", strconv.Itoa(param.Height))
}
outFile := strings.TrimSuffix(input, filepath.Ext(input)) + outExt
args = append(args, "-o", outFile)
args = append(args, input)
cmd := exec.Command("/opt/homebrew/bin/inkscape", args...)
cmd.Dir = svg.TempDir
out, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("inkscape failed: %w: %s", err, string(out))
}
file, err := os.Open(outFile)
if err != nil {
return nil, err
}
return file, nil
}
func CheckInkscape() bool {
_, err := exec.LookPath("inkscape")
return err == nil
}

View File

@@ -1,35 +1,40 @@
package svg package svg
import ( import (
"errors"
"io" "io"
"log" "log"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"golang.org/x/image/font/sfnt"
) )
type SvgStorage interface { type SvgStorage interface {
Create(id string, svg io.Reader) (string, error) Create(id string, svg io.Reader) (string, error)
Get(id string) (io.ReadCloser, error) Get(id string) (io.Reader, error)
CreatePublic(data io.Reader, filetype string) (string, error) AddFont(reader io.Reader, format string) error
GetPublic(path string) (io.ReadCloser, error) GetFonts() ([]string, error)
GetPublicDir() http.Dir GetFontsDir() (string, error)
} }
var _ SvgStorage = FileSvgStorage{}
var TempDir string = ""
type FileSvgStorage struct { type FileSvgStorage struct {
basepath, publicSubPath string basepath string
fontssubpath string
} }
func NewFileStorage(path, publicSubPath string) *FileSvgStorage { func NewFileStorage(path string, fontssubpath string) *FileSvgStorage {
err := os.MkdirAll(path, 0755) err := os.MkdirAll(path, 0755)
if err != nil { if err != nil {
panic(err) panic(err)
} }
err = os.MkdirAll(filepath.Join(path, fontssubpath), 0755)
if err != nil {
panic(err)
}
log.Println("Initialized file storage handler") log.Println("Initialized file storage handler")
return &FileSvgStorage{path, publicSubPath} return &FileSvgStorage{path, fontssubpath}
} }
func (f FileSvgStorage) Create(id string, svg io.Reader) (string, error) { func (f FileSvgStorage) Create(id string, svg io.Reader) (string, error) {
@@ -49,51 +54,70 @@ func (f FileSvgStorage) Create(id string, svg io.Reader) (string, error) {
return file.Name(), nil return file.Name(), nil
} }
func (f FileSvgStorage) Get(id string) (io.ReadCloser, error) { func (f FileSvgStorage) Get(id string) (io.Reader, error) {
file, err := os.Open(filepath.Join(f.basepath, id+".svg")) file, err := os.Open(filepath.Join(f.basepath, "svg", id+".svg"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return file, nil return file, nil
} }
func (f FileSvgStorage) CreatePublic(data io.Reader, filetype string) (string, error) { func (f FileSvgStorage) AddFont(fontreader io.Reader, format string) error {
path := filepath.Join(f.basepath, f.publicSubPath) if format != "ttf" && format != "otf" {
if err := os.Mkdir(path, 0755); err != nil { return errors.New("Format not supported")
return "", err
} }
file, err := os.CreateTemp(filepath.Join(f.basepath, f.fontssubpath), "*."+format)
file, err := os.CreateTemp(path, "*."+filetype)
if err != nil { if err != nil {
return "", err return err
} }
defer file.Close() defer file.Close()
log.Printf("Writing %s font to %s\n", format, file.Name())
return file.Name(), nil _, err = io.Copy(file, fontreader)
if err != nil {
return err
}
return nil
} }
func (f FileSvgStorage) GetPublic(path string) (io.ReadCloser, error) { func (f FileSvgStorage) GetFonts() ([]string, error) {
file, err := os.Open(filepath.Join(f.basepath, f.publicSubPath, path)) path := filepath.Join(f.basepath, f.fontssubpath)
entries, err := os.ReadDir(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return file, nil fonts := make([]string, len(entries))
for i, entry := range entries {
if entry.IsDir() {
continue
}
if strings.HasSuffix(entry.Name(), ".ttf") || strings.HasSuffix(entry.Name(), ".otf") {
fontblob, err := os.ReadFile(filepath.Join(path, entry.Name()))
if err != nil {
return nil, err
}
fontname, err := getFontName(fontblob)
if err != nil {
return nil, err
}
fonts[i] = fontname
}
}
log.Printf("Got fonts \n%s\n", strings.Join(fonts, "\n"))
return fonts, nil
} }
func (f FileSvgStorage) GetPublicDir() http.Dir { func (f FileSvgStorage) GetFontsDir() (string, error) {
return http.Dir(filepath.Join(f.basepath, f.publicSubPath)) return filepath.Join(f.basepath, f.fontssubpath), nil
} }
func CreateTemp(data io.Reader, filetype string) (string, error) { func getFontName(svgblob []byte) (string, error) {
file, err := os.CreateTemp(TempDir, "*."+filetype) font, err := sfnt.Parse(svgblob)
if err != nil { if err != nil {
return "", err return "", err
} }
defer file.Close() name, err := font.Name(nil, sfnt.NameIDFull)
_, err = io.Copy(file, data)
if err != nil { if err != nil {
return "", err return "", err
} }
return name, nil
return file.Name(), nil
} }