github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/pkg/avatar/service.go (about)

     1  package avatar
     2  
     3  import (
     4  	"strings"
     5  	"time"
     6  	"unicode"
     7  	"unicode/utf8"
     8  
     9  	"github.com/cozy/cozy-stack/pkg/cache"
    10  )
    11  
    12  // Options can be used to give options for the generated image
    13  type Options int
    14  
    15  const (
    16  	cacheTTL    = 30 * 24 * time.Hour // 1 month
    17  	contentType = "image/png"
    18  
    19  	// GreyBackground is an option to force a grey background
    20  	GreyBackground Options = 1 + iota
    21  )
    22  
    23  // Initials is able to generate initial avatar.
    24  type Initials interface {
    25  	// Generate will create a new avatar with the given initials and color.
    26  	Generate(initials, color string) ([]byte, error)
    27  	ContentType() string
    28  }
    29  
    30  // Service handle all the interactions with the initials images.
    31  type Service struct {
    32  	cache    cache.Cache
    33  	initials Initials
    34  }
    35  
    36  // NewService instantiate a new [Service].
    37  func NewService(cache cache.Cache, cmd string) *Service {
    38  	initials := NewPNGInitials(cmd)
    39  	return &Service{cache, initials}
    40  }
    41  
    42  // GenerateInitials an image with the initials for the given name (and the
    43  // content-type to use for the HTTP response).
    44  func (s *Service) GenerateInitials(publicName string, opts ...Options) ([]byte, string, error) {
    45  	name := strings.TrimSpace(publicName)
    46  	info := extractInfo(name)
    47  	for _, opt := range opts {
    48  		if opt == GreyBackground {
    49  			info.color = charcoalGrey
    50  		}
    51  	}
    52  
    53  	key := "initials:" + info.initials + info.color
    54  	if bytes, ok := s.cache.Get(key); ok {
    55  		return bytes, contentType, nil
    56  	}
    57  
    58  	bytes, err := s.initials.Generate(info.initials, info.color)
    59  	if err != nil {
    60  		return nil, "", err
    61  	}
    62  	s.cache.Set(key, bytes, cacheTTL)
    63  	return bytes, s.initials.ContentType(), nil
    64  }
    65  
    66  // See https://github.com/cozy/cozy-ui/blob/master/react/Avatar/index.jsx#L9-L26
    67  // and https://docs.cozy.io/cozy-ui/styleguide/section-settings.html#kssref-settings-colors
    68  var colors = []string{
    69  	"#1FA8F1",
    70  	"#FD7461",
    71  	"#FC6D00",
    72  	"#F52D2D",
    73  	"#FF962F",
    74  	"#FF7F1B",
    75  	"#6984CE",
    76  	"#7F6BEE",
    77  	"#B449E7",
    78  	"#40DE8E",
    79  	"#0DCBCF",
    80  	"#35CE68",
    81  	"#3DA67E",
    82  	"#C2ADF4",
    83  	"#FFC644",
    84  	"#FC4C83",
    85  }
    86  
    87  var charcoalGrey = "#32363F"
    88  
    89  type info struct {
    90  	initials string
    91  	color    string
    92  }
    93  
    94  func extractInfo(name string) info {
    95  	initials := strings.ToUpper(getInitials(name))
    96  	color := getColor(name)
    97  	return info{initials: initials, color: color}
    98  }
    99  
   100  func getInitials(name string) string {
   101  	parts := strings.Split(name, " ")
   102  	initials := make([]rune, 0, len(parts))
   103  	for _, part := range parts {
   104  		r, size := utf8.DecodeRuneInString(part)
   105  		if size > 0 && unicode.IsLetter(r) {
   106  			initials = append(initials, r)
   107  		}
   108  	}
   109  	switch len(initials) {
   110  	case 0:
   111  		return "?"
   112  	case 1:
   113  		return string(initials)
   114  	default:
   115  		return string(initials[0]) + string(initials[len(initials)-1])
   116  	}
   117  }
   118  
   119  func getColor(name string) string {
   120  	sum := 0
   121  	for i := 0; i < len(name); i++ {
   122  		sum += int(name[i])
   123  	}
   124  	return colors[sum%len(colors)]
   125  }