187 lines
4.9 KiB
Go
187 lines
4.9 KiB
Go
package theme_files
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"image"
|
|
"image/jpeg"
|
|
_ "image/png"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
|
|
"autolibrary.top/webserver/backend/models/themes_model"
|
|
)
|
|
|
|
func CompressPreviewImage(data []byte) ([]byte, error) {
|
|
config, _, err := image.DecodeConfig(bytes.NewReader(data))
|
|
if err != nil || config.Width <= 0 || config.Height <= 0 || int64(config.Width)*int64(config.Height) > 40_000_000 {
|
|
return nil, fmt.Errorf("invalid image or image dimensions exceed the limit")
|
|
}
|
|
img, format, err := image.Decode(bytes.NewReader(data))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot decode image: %v", err)
|
|
}
|
|
|
|
// Resize if width exceeds 800px
|
|
bounds := img.Bounds()
|
|
w := bounds.Dx()
|
|
h := bounds.Dy()
|
|
maxW := 800
|
|
if w > maxW {
|
|
h = h * maxW / w
|
|
if h < 1 {
|
|
h = 1
|
|
}
|
|
w = maxW
|
|
// Simple resize using standard library
|
|
scaled := image.NewRGBA(image.Rect(0, 0, w, h))
|
|
for y := 0; y < h; y++ {
|
|
for x := 0; x < w; x++ {
|
|
srcX := x * bounds.Dx() / w
|
|
srcY := y * bounds.Dy() / h
|
|
scaled.Set(x, y, img.At(srcX, srcY))
|
|
}
|
|
}
|
|
img = scaled
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err = jpeg.Encode(&buf, img, &jpeg.Options{Quality: 80})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot encode JPEG: %v", err)
|
|
}
|
|
|
|
log.Printf("[I] image compressed: %s %dx%d, %d -> %d bytes", format, bounds.Dx(), bounds.Dy(), len(data), buf.Len())
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// GET /api/themes/featured?limit=6
|
|
|
|
func ValidateAlthemeFile(data []byte) (map[string]interface{}, error) {
|
|
reader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid zip file: %v", err)
|
|
}
|
|
|
|
var hasInfoJSON, hasThemeQSS bool
|
|
var info map[string]interface{}
|
|
|
|
for _, f := range reader.File {
|
|
if f.Name == "info.json" {
|
|
hasInfoJSON = true
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot open info.json: %v", err)
|
|
}
|
|
decoder := json.NewDecoder(rc)
|
|
if err := decoder.Decode(&info); err != nil {
|
|
rc.Close()
|
|
return nil, fmt.Errorf("invalid info.json: %v", err)
|
|
}
|
|
rc.Close()
|
|
}
|
|
if f.Name == "theme.qss" {
|
|
hasThemeQSS = true
|
|
}
|
|
}
|
|
|
|
if !hasInfoJSON {
|
|
return nil, fmt.Errorf("missing info.json in .altheme file")
|
|
}
|
|
if !hasThemeQSS {
|
|
return nil, fmt.Errorf("missing theme.qss in .altheme file")
|
|
}
|
|
|
|
required := []string{"name", "author", "need_theme", "brief"}
|
|
for _, key := range required {
|
|
if _, ok := info[key]; !ok {
|
|
return nil, fmt.Errorf("info.json missing required key: %s", key)
|
|
}
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
// buildInfoJSON constructs the info.json map from a Theme DB record.
|
|
// It merges Brief and Description with a newline separator into the "brief" field.
|
|
|
|
func BuildInfoJSON(theme themes_model.Theme) map[string]interface{} {
|
|
brief := theme.Brief
|
|
if theme.Description != "" {
|
|
brief = brief + "\n" + theme.Description
|
|
}
|
|
return map[string]interface{}{
|
|
"qss": "theme.qss",
|
|
"name": theme.Name,
|
|
"author": theme.Author,
|
|
"need_theme": theme.NeedTheme,
|
|
"brief": brief,
|
|
"version": theme.Version,
|
|
"compatible_version": theme.CompatVersion,
|
|
}
|
|
}
|
|
|
|
// createAlthemeArchive creates a .altheme ZIP archive in memory
|
|
// containing theme.qss (from qssData) and info.json (from info map).
|
|
|
|
func CreateAlthemeArchive(qssData []byte, info map[string]interface{}) ([]byte, error) {
|
|
var buf bytes.Buffer
|
|
w := zip.NewWriter(&buf)
|
|
|
|
// Add theme.qss
|
|
qssWriter, err := w.Create("theme.qss")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot create theme.qss in archive: %v", err)
|
|
}
|
|
if _, err := qssWriter.Write(qssData); err != nil {
|
|
return nil, fmt.Errorf("cannot write theme.qss to archive: %v", err)
|
|
}
|
|
|
|
// Add info.json
|
|
infoWriter, err := w.Create("info.json")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot create info.json in archive: %v", err)
|
|
}
|
|
infoBytes, err := json.MarshalIndent(info, "", " ")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot marshal info.json: %v", err)
|
|
}
|
|
if _, err := infoWriter.Write(infoBytes); err != nil {
|
|
return nil, fmt.Errorf("cannot write info.json to archive: %v", err)
|
|
}
|
|
|
|
if err := w.Close(); err != nil {
|
|
return nil, fmt.Errorf("cannot close archive: %v", err)
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// extractQSSFromAltheme reads an existing .altheme ZIP file from disk
|
|
// and returns the raw bytes of its theme.qss entry.
|
|
|
|
func ExtractQSSFromAltheme(filePath string) ([]byte, error) {
|
|
data, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot read .altheme file: %v", err)
|
|
}
|
|
reader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot open .altheme as zip: %v", err)
|
|
}
|
|
for _, f := range reader.File {
|
|
if f.Name == "theme.qss" {
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot open theme.qss in archive: %v", err)
|
|
}
|
|
defer rc.Close()
|
|
return io.ReadAll(rc)
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("theme.qss not found in .altheme archive")
|
|
}
|