event.go raw
1 package variants
2
3 import (
4 "fmt"
5 "regexp"
6 "strconv"
7 "strings"
8 )
9
10 // UploadedVariant represents a variant after upload to Blossom.
11 type UploadedVariant struct {
12 Variant VariantName
13 URL string
14 SHA256 string
15 Width int
16 Height int
17 MimeType string
18 Size int64
19 Blurhash string
20 }
21
22 // ImetaTag builds an imeta tag array for a variant.
23 // Format: ["imeta", "url <url>", "x <sha256>", "m <mime>", "dim <WxH>", "variant <name>", ...]
24 func (v *UploadedVariant) ImetaTag() []string {
25 tag := []string{"imeta"}
26
27 tag = append(tag, fmt.Sprintf("url %s", v.URL))
28 tag = append(tag, fmt.Sprintf("x %s", v.SHA256))
29 tag = append(tag, fmt.Sprintf("m %s", v.MimeType))
30 tag = append(tag, fmt.Sprintf("dim %dx%d", v.Width, v.Height))
31 tag = append(tag, fmt.Sprintf("variant %s", v.Variant))
32
33 if v.Size > 0 {
34 tag = append(tag, fmt.Sprintf("size %d", v.Size))
35 }
36
37 if v.Blurhash != "" {
38 tag = append(tag, fmt.Sprintf("blurhash %s", v.Blurhash))
39 }
40
41 return tag
42 }
43
44 // ParseImetaTag parses an imeta tag array into an UploadedVariant.
45 func ParseImetaTag(tag []string) (*UploadedVariant, error) {
46 if len(tag) < 2 || tag[0] != "imeta" {
47 return nil, fmt.Errorf("not an imeta tag")
48 }
49
50 fields := make(map[string]string)
51 for i := 1; i < len(tag); i++ {
52 part := tag[i]
53 idx := strings.Index(part, " ")
54 if idx > 0 {
55 key := part[:idx]
56 value := part[idx+1:]
57 fields[key] = value
58 }
59 }
60
61 url := fields["url"]
62 sha256 := fields["x"]
63 mimeType := fields["m"]
64 dim := fields["dim"]
65
66 if url == "" || sha256 == "" || mimeType == "" || dim == "" {
67 return nil, fmt.Errorf("missing required fields")
68 }
69
70 // Parse dimensions
71 dimRe := regexp.MustCompile(`^(\d+)x(\d+)$`)
72 match := dimRe.FindStringSubmatch(dim)
73 if match == nil {
74 return nil, fmt.Errorf("invalid dimension format: %s", dim)
75 }
76
77 width, _ := strconv.Atoi(match[1])
78 height, _ := strconv.Atoi(match[2])
79
80 variant := VariantName(fields["variant"])
81 if variant == "" {
82 variant = Original
83 }
84
85 v := &UploadedVariant{
86 Variant: variant,
87 URL: url,
88 SHA256: sha256,
89 Width: width,
90 Height: height,
91 MimeType: mimeType,
92 }
93
94 if sizeStr := fields["size"]; sizeStr != "" {
95 size, _ := strconv.ParseInt(sizeStr, 10, 64)
96 v.Size = size
97 }
98
99 v.Blurhash = fields["blurhash"]
100
101 return v, nil
102 }
103
104 // ParseBindingEvent parses a kind 1063 event's tags into UploadedVariants.
105 func ParseBindingEvent(tags [][]string) ([]UploadedVariant, error) {
106 var variants []UploadedVariant
107
108 for _, tag := range tags {
109 if len(tag) < 2 || tag[0] != "imeta" {
110 continue
111 }
112
113 v, err := ParseImetaTag(tag)
114 if err != nil {
115 continue // Skip invalid tags
116 }
117
118 variants = append(variants, *v)
119 }
120
121 return variants, nil
122 }
123
124 // SelectVariantForViewport selects the best variant for a given viewport.
125 // Selection rule: Pick the smallest variant >= target width.
126 func SelectVariantForViewport(variants []UploadedVariant, targetWidth int, pixelRatio float64) *UploadedVariant {
127 if len(variants) == 0 {
128 return nil
129 }
130
131 effectiveWidth := int(float64(targetWidth) * pixelRatio)
132
133 // Sort by width ascending (simple bubble sort for small slices)
134 sorted := make([]UploadedVariant, len(variants))
135 copy(sorted, variants)
136 for i := 0; i < len(sorted)-1; i++ {
137 for j := i + 1; j < len(sorted); j++ {
138 if sorted[i].Width > sorted[j].Width {
139 sorted[i], sorted[j] = sorted[j], sorted[i]
140 }
141 }
142 }
143
144 // Find smallest variant >= target width
145 for i := range sorted {
146 if sorted[i].Width >= effectiveWidth {
147 return &sorted[i]
148 }
149 }
150
151 // If none large enough, return largest
152 return &sorted[len(sorted)-1]
153 }
154
155 // IsValidBlobHash checks if a string is a valid 64-character hex hash.
156 func IsValidBlobHash(s string) bool {
157 if len(s) != 64 {
158 return false
159 }
160 for _, c := range s {
161 if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
162 return false
163 }
164 }
165 return true
166 }
167