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") }