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