/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see .
*/
package main
import (
"bytes"
"encoding/json"
"fmt"
"image"
"image/draw"
"image/png"
"io"
"math"
"math/rand"
"net/http"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/srwiley/oksvg"
"github.com/srwiley/rasterx"
)
type EmojiSpritesConfig struct {
NonDiversityPerRow int
DiversityPerRow int
PickerPerRow int
PickerCount int
}
var EMOJI_SPRITES = EmojiSpritesConfig{
NonDiversityPerRow: 42,
DiversityPerRow: 10,
PickerPerRow: 11,
PickerCount: 50,
}
const (
EMOJI_SIZE = 32
TWEMOJI_CDN = "https://fluxerstatic.com/emoji"
)
var SPRITE_SCALES = []int{1, 2}
type EmojiObject struct {
Surrogates string `json:"surrogates"`
Skins []struct {
Surrogates string `json:"surrogates"`
} `json:"skins,omitempty"`
}
type EmojiEntry struct {
Surrogates string
}
type httpResp struct {
Status int
Body string
}
func main() {
rand.Seed(time.Now().UnixNano())
cwd, _ := os.Getwd()
appDir := filepath.Join(cwd, "..")
outputDir := filepath.Join(appDir, "src", "assets", "emoji-sprites")
if err := os.MkdirAll(outputDir, 0o755); err != nil {
fmt.Fprintln(os.Stderr, "Failed to ensure output dir:", err)
os.Exit(1)
}
emojiData, err := loadEmojiData(filepath.Join(appDir, "src", "data", "emojis.json"))
if err != nil {
fmt.Fprintln(os.Stderr, "Error loading emoji data:", err)
os.Exit(1)
}
client := &http.Client{
Timeout: 30 * time.Second,
}
svgCache := newSVGCache()
if err := generateMainSpriteSheet(client, svgCache, emojiData, outputDir); err != nil {
fmt.Fprintln(os.Stderr, "Error generating main sprite sheet:", err)
os.Exit(1)
}
if err := generateDiversitySpriteSheets(client, svgCache, emojiData, outputDir); err != nil {
fmt.Fprintln(os.Stderr, "Error generating diversity sprite sheets:", err)
os.Exit(1)
}
if err := generatePickerSpriteSheet(client, svgCache, outputDir); err != nil {
fmt.Fprintln(os.Stderr, "Error generating picker sprite sheet:", err)
os.Exit(1)
}
fmt.Println("Emoji sprites generated successfully.")
}
func loadEmojiData(path string) (map[string][]EmojiObject, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var data map[string][]EmojiObject
if err := json.Unmarshal(b, &data); err != nil {
return nil, err
}
return data, nil
}
// --- SVG fetching + caching ---
type svgCache struct {
m map[string]*string
}
func newSVGCache() *svgCache {
return &svgCache{m: make(map[string]*string)}
}
func (c *svgCache) get(codepoint string) (*string, bool) {
v, ok := c.m[codepoint]
return v, ok
}
func (c *svgCache) set(codepoint string, v *string) {
c.m[codepoint] = v
}
func downloadSVG(client *http.Client, url string) (httpResp, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return httpResp{}, err
}
req.Header.Set("User-Agent", "fluxer-emoji-sprites/1.0")
resp, err := client.Do(req)
if err != nil {
return httpResp{}, err
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return httpResp{}, err
}
return httpResp{
Status: resp.StatusCode,
Body: string(bodyBytes),
}, nil
}
func fetchTwemojiSVG(client *http.Client, cache *svgCache, codepoint string) *string {
if v, ok := cache.get(codepoint); ok {
return v
}
url := fmt.Sprintf("%s/%s.svg", TWEMOJI_CDN, codepoint)
r, err := downloadSVG(client, url)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to fetch Twemoji %s: %v\n", codepoint, err)
cache.set(codepoint, nil)
return nil
}
if r.Status != 200 {
fmt.Fprintf(os.Stderr, "Twemoji %s returned %d\n", codepoint, r.Status)
cache.set(codepoint, nil)
return nil
}
body := r.Body
cache.set(codepoint, &body)
return &body
}
// --- Emoji -> codepoint ---
func emojiToCodepoint(s string) string {
parts := make([]string, 0, len(s))
for _, r := range s {
if r == 0xFE0F {
continue
}
parts = append(parts, strings.ToLower(strconv.FormatInt(int64(r), 16)))
}
return strings.Join(parts, "-")
}
// --- Rendering ---
var svgOpenTagRe = regexp.MustCompile(`(?i)