This commit is contained in:
Tomas Dvorak
2025-10-22 20:35:22 +02:00
parent e47059385c
commit e6bc2eedb3
55 changed files with 1751 additions and 1378 deletions
Binary file not shown.
+3
View File
@@ -7,6 +7,8 @@ require (
github.com/gin-gonic/gin v1.9.1
github.com/google/uuid v1.5.0
github.com/mattn/go-sqlite3 v1.14.19
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef
)
require (
@@ -31,6 +33,7 @@ require (
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.7.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
+8
View File
@@ -63,6 +63,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -82,14 +86,18 @@ golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
+65 -12
View File
@@ -7,6 +7,7 @@ import (
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
@@ -369,13 +370,43 @@ func getLogoWithMetadata(c *gin.Context) {
// List all logos
func listLogos(c *gin.Context) {
rows, err := db.Query(`
SELECT id, club_name, club_city, club_type, club_website,
has_svg, has_png, primary_format,
created_at, updated_at
FROM logos
ORDER BY club_name
`)
q := strings.TrimSpace(c.Query("q"))
sortParam := c.DefaultQuery("sort", "name")
limitStr := c.Query("limit")
pageStr := c.Query("page")
base := "SELECT id, club_name, club_city, club_type, club_website, has_svg, has_png, primary_format, created_at, updated_at FROM logos"
where := ""
args := []interface{}{}
if q != "" {
where = " WHERE LOWER(club_name) LIKE ? OR LOWER(club_city) LIKE ? OR id LIKE ?"
like := "%" + strings.ToLower(q) + "%"
args = append(args, like, like, "%"+q+"%")
}
order := " ORDER BY club_name"
if sortParam == "recent" {
order = " ORDER BY datetime(updated_at) DESC, datetime(created_at) DESC"
}
limitClause := ""
if limitStr != "" {
if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 {
limitClause = " LIMIT ?"
args = append(args, limit)
if pageStr != "" {
if page, err := strconv.Atoi(pageStr); err == nil {
if page < 1 {
page = 1
}
offset := (page - 1) * limit
limitClause += " OFFSET ?"
args = append(args, offset)
}
}
}
}
query := base + where + order + limitClause
rows, err := db.Query(query, args...)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"})
return
@@ -392,8 +423,7 @@ func listLogos(c *gin.Context) {
for rows.Next() {
var logo LogoMetadata
var hasSVG, hasPNG int
err := rows.Scan(
if err := rows.Scan(
&logo.ID,
&logo.ClubName,
&logo.ClubCity,
@@ -404,14 +434,12 @@ func listLogos(c *gin.Context) {
&logo.PrimaryFormat,
&logo.CreatedAt,
&logo.UpdatedAt,
)
if err != nil {
); err != nil {
continue
}
logo.HasSVG = hasSVG == 1
logo.HasPNG = hasPNG == 1
if logo.HasPNG {
logo.LogoURL = fmt.Sprintf("%s/logos/%s?format=png", baseURL, logo.ID)
} else if logo.HasSVG {
@@ -424,6 +452,31 @@ func listLogos(c *gin.Context) {
c.JSON(http.StatusOK, logos)
}
func deleteLogo(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "logo ID is required"})
return
}
if _, err := uuid.Parse(id); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid UUID format"})
return
}
_, err := db.Exec("DELETE FROM logos WHERE id = ?", id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"})
return
}
pngPath := filepath.Join("./logos/png", id+".png")
svgPath := filepath.Join("./logos/svg", id+".svg")
os.Remove(pngPath)
os.Remove(svgPath)
c.JSON(http.StatusOK, gin.H{"success": true, "id": id})
}
func uploadLogo(c *gin.Context) {
id := c.Param("id")
if id == "" {
+67 -10
View File
@@ -9,6 +9,9 @@ import (
"os"
"os/exec"
"path/filepath"
"github.com/srwiley/oksvg"
"github.com/srwiley/rasterx"
)
// ConvertSVGToPNG converts an SVG file to PNG format
@@ -24,8 +27,12 @@ func ConvertSVGToPNG(svgPath, pngPath string, width int) error {
return nil
}
// If no converter available, copy SVG as fallback and log warning
return fmt.Errorf("no SVG converter available (install ImageMagick or Inkscape)")
// Try pure-Go conversion
if err := convertWithGoRenderer(svgPath, pngPath, width); err == nil {
return nil
}
return fmt.Errorf("no SVG converter available (install ImageMagick or Inkscape, or ensure Go renderer deps)")
}
// ConvertPDFToPNG converts a PDF file to PNG format
@@ -39,14 +46,14 @@ func ConvertPDFToPNG(pdfPath, pngPath string, width int) error {
fmt.Sprintf("%s[0]", pdfPath), // Only first page
pngPath,
)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("PDF conversion failed (install ImageMagick and Ghostscript): %v - %s", err, stderr.String())
}
return nil
}
@@ -58,14 +65,14 @@ func convertWithImageMagick(svgPath, pngPath string, width int) error {
svgPath,
pngPath,
)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("imagemagick conversion failed: %v - %s", err, stderr.String())
}
return nil
}
@@ -76,10 +83,10 @@ func convertWithInkscape(svgPath, pngPath string, width int) error {
fmt.Sprintf("--export-width=%d", width),
svgPath,
)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("inkscape conversion failed: %v - %s", err, stderr.String())
}
@@ -87,6 +94,56 @@ func convertWithInkscape(svgPath, pngPath string, width int) error {
return nil
}
func convertWithGoRenderer(svgPath, pngPath string, width int) error {
f, err := os.Open(svgPath)
if err != nil {
return fmt.Errorf("open svg: %w", err)
}
defer f.Close()
icon, err := oksvg.ReadIconStream(f)
if err != nil {
return fmt.Errorf("parse svg: %w", err)
}
vb := icon.ViewBox
targetW := width
if targetW <= 0 {
targetW = int(vb.W)
if targetW <= 0 {
targetW = 512
}
}
var targetH int
if vb.W != 0 {
targetH = int(float64(targetW) * (vb.H / vb.W))
} else {
targetH = targetW
}
if targetH <= 0 {
targetH = targetW
}
icon.SetTarget(0, 0, float64(targetW), float64(targetH))
rgba := image.NewRGBA(image.Rect(0, 0, targetW, targetH))
scanner := rasterx.NewScannerGV(targetW, targetH, rgba, rgba.Bounds())
raster := rasterx.NewDasher(targetW, targetH, scanner)
icon.Draw(raster, 1.0)
out, err := os.Create(pngPath)
if err != nil {
return fmt.Errorf("create png: %w", err)
}
defer out.Close()
if err := png.Encode(out, rgba); err != nil {
os.Remove(pngPath)
return fmt.Errorf("encode png: %w", err)
}
return nil
}
// OptimizePNG optimizes a PNG file (basic implementation)
func OptimizePNG(pngPath string) error {
// Open the file
+1
View File
@@ -88,6 +88,7 @@ func setupRoutes(r *gin.Engine) {
logos.GET("/:id", getLogo)
logos.GET("/:id/json", getLogoWithMetadata)
logos.POST("/:id", uploadLogo)
logos.DELETE("/:id", deleteLogo)
}
}