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