返回 DeepSeek-Reasonix
theme_image.go
根目录 / desktop / theme_image.go
1 package main
2
3 import (
4 "bytes"
5 "encoding/base64"
6 "fmt"
7 "image"
8 _ "image/jpeg"
9 _ "image/png"
10 "io"
11 "os"
12 "path/filepath"
13 "strings"
14
15 "golang.org/x/image/webp"
16 )
17
18 func decodeBase64Payload(payload string) ([]byte, error) {
19 return base64.StdEncoding.DecodeString(payload)
20 }
21
22 func validateThemeImageFile(path string) error {
23 info, err := os.Lstat(path)
24 if err != nil {
25 return err
26 }
27 if info.Mode()&os.ModeSymlink != 0 {
28 return fmt.Errorf("background image must not be a symlink")
29 }
30 if !info.Mode().IsRegular() {
31 return fmt.Errorf("background image must be a regular file")
32 }
33 if info.Size() > themePackMaxImageBytes {
34 return fmt.Errorf("background image exceeds %d bytes", themePackMaxImageBytes)
35 }
36 name := filepath.Base(path)
37 if !themePackImageRe.MatchString(name) {
38 return fmt.Errorf("unsupported background image name %q", name)
39 }
40 f, err := os.Open(path)
41 if err != nil {
42 return err
43 }
44 defer f.Close()
45
46 head := make([]byte, 512)
47 n, err := io.ReadFull(f, head)
48 if err != nil && err != io.ErrUnexpectedEOF && err != io.EOF {
49 return err
50 }
51 head = head[:n]
52 mime := sniffThemeImageMIME(head, name)
53 if mime == "" {
54 return fmt.Errorf("background image must be PNG, JPEG, or WebP")
55 }
56
57 // Re-open for full config decode (sniff consumed some bytes).
58 if _, err := f.Seek(0, io.SeekStart); err != nil {
59 return err
60 }
61 cfg, format, err := decodeThemeImageConfig(f, mime)
62 if err != nil {
63 return fmt.Errorf("invalid background image: %w", err)
64 }
65 switch format {
66 case "png", "jpeg", "webp":
67 default:
68 return fmt.Errorf("unsupported image format %q", format)
69 }
70 if cfg.Width <= 0 || cfg.Height <= 0 {
71 return fmt.Errorf("invalid image dimensions")
72 }
73 if cfg.Width > themePackMaxImageEdge || cfg.Height > themePackMaxImageEdge {
74 return fmt.Errorf("background image must be at most %d×%d", themePackMaxImageEdge, themePackMaxImageEdge)
75 }
76 return nil
77 }
78
79 func decodeThemeImageConfig(r io.Reader, mime string) (image.Config, string, error) {
80 if mime == "image/webp" {
81 cfg, err := webp.DecodeConfig(r)
82 return cfg, "webp", err
83 }
84 return image.DecodeConfig(r)
85 }
86
87 func sniffThemeImageMIME(head []byte, name string) string {
88 ext := strings.ToLower(filepath.Ext(name))
89 if len(head) >= 8 && bytes.Equal(head[:8], []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}) {
90 if ext == ".png" || ext == "" {
91 return "image/png"
92 }
93 }
94 if len(head) >= 3 && head[0] == 0xff && head[1] == 0xd8 && head[2] == 0xff {
95 if ext == ".jpg" || ext == ".jpeg" || ext == "" {
96 return "image/jpeg"
97 }
98 }
99 if len(head) >= 12 && string(head[:4]) == "RIFF" && string(head[8:12]) == "WEBP" {
100 if ext == ".webp" || ext == "" {
101 return "image/webp"
102 }
103 }
104 // Extension/MIME mismatch or unsupported signature.
105 return ""
106 }
107
108 func themeImageMIMEFromName(name string) string {
109 switch strings.ToLower(filepath.Ext(name)) {
110 case ".png":
111 return "image/png"
112 case ".jpg", ".jpeg":
113 return "image/jpeg"
114 case ".webp":
115 return "image/webp"
116 default:
117 return "application/octet-stream"
118 }
119 }
120
121 // decodeDataURLImage extracts image bytes from a data:image/...;base64,... URL.
122 func decodeDataURLImage(dataURL string) (filename string, data []byte, err error) {
123 dataURL = strings.TrimSpace(dataURL)
124 if dataURL == "" {
125 return "", nil, fmt.Errorf("empty image data")
126 }
127 const prefix = "data:"
128 if !strings.HasPrefix(dataURL, prefix) {
129 return "", nil, fmt.Errorf("image must be a data URL")
130 }
131 // data:[<mediatype>][;base64],<data>
132 rest := dataURL[len(prefix):]
133 comma := strings.IndexByte(rest, ',')
134 if comma < 0 {
135 return "", nil, fmt.Errorf("invalid data URL")
136 }
137 meta := rest[:comma]
138 payload := rest[comma+1:]
139 parts := strings.Split(meta, ";")
140 mime := strings.TrimSpace(parts[0])
141 base64Enc := false
142 for _, p := range parts[1:] {
143 if strings.EqualFold(strings.TrimSpace(p), "base64") {
144 base64Enc = true
145 }
146 }
147 if !base64Enc {
148 return "", nil, fmt.Errorf("image data URL must be base64-encoded")
149 }
150 var ext string
151 switch strings.ToLower(mime) {
152 case "image/png":
153 ext = ".png"
154 case "image/jpeg", "image/jpg":
155 ext = ".jpg"
156 case "image/webp":
157 ext = ".webp"
158 default:
159 return "", nil, fmt.Errorf("unsupported image MIME %q", mime)
160 }
161 // Prefer stdlib base64 via encoding/base64 in caller path — import here.
162 raw, err := decodeBase64Payload(payload)
163 if err != nil {
164 return "", nil, fmt.Errorf("decode image data: %w", err)
165 }
166 if int64(len(raw)) > themePackMaxImageBytes {
167 return "", nil, fmt.Errorf("background image exceeds %d bytes", themePackMaxImageBytes)
168 }
169 return "background" + ext, raw, nil
170 }
171
172 func taskBackgroundImageName(decodedName string) string {
173 ext := strings.ToLower(filepath.Ext(filepath.Base(decodedName)))
174 return "background-task" + ext
175 }
176
176 lines GO