mirror of
https://github.com/Dvorinka/ClubLogos.git
synced 2026-06-03 19:42:58 +00:00
enhance
This commit is contained in:
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user