This commit is contained in:
Dev
2025-09-12 16:33:52 +03:00
commit d2314f6934
9 changed files with 939 additions and 0 deletions

323
internal/meme/generator.go Normal file
View File

@@ -0,0 +1,323 @@
package meme
import (
"fmt"
"image"
"image/color"
"image/png"
"log"
"math/rand"
"os"
"path/filepath"
"sync"
"time"
"golang.org/x/image/font"
"golang.org/x/image/font/basicfont"
"golang.org/x/image/math/fixed"
)
// Template represents a meme template
type Template struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Width int `json:"width"`
Height int `json:"height"`
}
// GenerateRequest represents a meme generation request
type GenerateRequest struct {
Template string `json:"template"`
TopText string `json:"top_text"`
BottomText string `json:"bottom_text"`
}
// GenerateResult represents the result of meme generation
type GenerateResult struct {
ID int `json:"id"`
Filename string `json:"filename"`
ShareURL string `json:"share_url"`
Template string `json:"template"`
}
// Meme represents a generated meme
type Meme struct {
ID int `json:"id"`
Template string `json:"template"`
TopText string `json:"top_text"`
BottomText string `json:"bottom_text"`
Filename string `json:"filename"`
ShareURL string `json:"share_url"`
CreatedAt time.Time `json:"created_at"`
}
// Generator handles meme generation with concurrent processing
type Generator struct {
templates map[string]Template
memes map[int]*Meme
memeID int
mutex sync.RWMutex
jobQueue chan GenerateRequest
workers int
}
// NewGenerator creates a new meme generator
func NewGenerator() *Generator {
g := &Generator{
templates: make(map[string]Template),
memes: make(map[int]*Meme),
memeID: 0,
jobQueue: make(chan GenerateRequest, 100),
workers: 5, // Number of concurrent workers
}
g.initializeTemplates()
g.ensureDirectories()
g.startWorkers()
return g
}
// initializeTemplates sets up available meme templates
func (g *Generator) initializeTemplates() {
templates := []Template{
{ID: "drake", Name: "Drake Pointing", Description: "The classic Drake approval/disapproval format", Width: 400, Height: 400},
{ID: "distracted", Name: "Distracted Boyfriend", Description: "Guy looking at another girl while girlfriend disapproves", Width: 500, Height: 333},
{ID: "woman_yelling", Name: "Woman Yelling at Cat", Description: "Woman pointing and yelling, confused cat at dinner table", Width: 480, Height: 438},
{ID: "change_my_mind", Name: "Change My Mind", Description: "Steven Crowder sitting at table with sign", Width: 482, Height: 361},
{ID: "this_is_fine", Name: "This is Fine", Description: "Dog sitting in burning room saying this is fine", Width: 400, Height: 400},
}
for _, template := range templates {
g.templates[template.ID] = template
}
}
// ensureDirectories creates necessary directories
func (g *Generator) ensureDirectories() {
dirs := []string{
"web/static/generated",
"assets/templates",
}
for _, dir := range dirs {
if err := os.MkdirAll(dir, 0755); err != nil {
log.Printf("Failed to create directory %s: %v", dir, err)
}
}
}
// startWorkers starts background workers for concurrent processing
func (g *Generator) startWorkers() {
for i := 0; i < g.workers; i++ {
go g.worker(i)
}
log.Printf("Started %d concurrent meme generation workers", g.workers)
}
// worker processes meme generation jobs
func (g *Generator) worker(id int) {
for job := range g.jobQueue {
log.Printf("Worker %d processing meme generation for template: %s", id, job.Template)
// Simulate some processing time for demonstration
time.Sleep(time.Millisecond * time.Duration(100+rand.Intn(400)))
log.Printf("Worker %d completed meme generation", id)
}
}
// Generate creates a new meme with the given parameters
func (g *Generator) Generate(req GenerateRequest) (*GenerateResult, error) {
g.mutex.Lock()
g.memeID++
id := g.memeID
g.mutex.Unlock()
// Add job to queue for background processing
go func() {
g.jobQueue <- req
}()
// Generate filename
filename := fmt.Sprintf("meme_%d_%s_%d.png", id, req.Template, time.Now().Unix())
// Create the meme image
if err := g.createMemeImage(req, filename); err != nil {
return nil, fmt.Errorf("failed to create meme image: %v", err)
}
// Store meme metadata
meme := &Meme{
ID: id,
Template: req.Template,
TopText: req.TopText,
BottomText: req.BottomText,
Filename: filename,
ShareURL: fmt.Sprintf("/api/v1/memes/%d", id),
CreatedAt: time.Now(),
}
g.mutex.Lock()
g.memes[id] = meme
g.mutex.Unlock()
return &GenerateResult{
ID: id,
Filename: filename,
ShareURL: meme.ShareURL,
Template: req.Template,
}, nil
}
// createMemeImage generates the actual meme image
func (g *Generator) createMemeImage(req GenerateRequest, filename string) error {
template, exists := g.templates[req.Template]
if !exists {
return fmt.Errorf("template %s not found", req.Template)
}
// Create a new image
img := image.NewRGBA(image.Rect(0, 0, template.Width, template.Height))
// Fill with a gradient background (since we don't have actual template images)
g.fillGradientBackground(img, req.Template)
// Add text to the image
g.addTextToImage(img, req.TopText, 20, true) // Top text
g.addTextToImage(img, req.BottomText, img.Bounds().Dy()-40, false) // Bottom text
// Save the image
outputPath := filepath.Join("web/static/generated", filename)
file, err := os.Create(outputPath)
if err != nil {
return err
}
defer file.Close()
return png.Encode(file, img)
}
// fillGradientBackground creates a themed background based on template
func (g *Generator) fillGradientBackground(img *image.RGBA, templateID string) {
bounds := img.Bounds()
// Different color schemes for different templates
var startColor, endColor color.RGBA
switch templateID {
case "drake":
startColor = color.RGBA{255, 107, 107, 255} // Red-ish
endColor = color.RGBA{78, 205, 196, 255} // Teal-ish
case "distracted":
startColor = color.RGBA{255, 195, 113, 255} // Orange
endColor = color.RGBA{196, 229, 56, 255} // Green
case "woman_yelling":
startColor = color.RGBA{255, 154, 162, 255} // Pink
endColor = color.RGBA{255, 206, 84, 255} // Yellow
case "change_my_mind":
startColor = color.RGBA{108, 92, 231, 255} // Purple
endColor = color.RGBA{255, 107, 107, 255} // Red
default:
startColor = color.RGBA{200, 200, 200, 255} // Gray
endColor = color.RGBA{100, 100, 100, 255} // Dark gray
}
// Create gradient
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
// Simple linear gradient
ratio := float64(y-bounds.Min.Y) / float64(bounds.Dy())
r := uint8(float64(startColor.R)*(1-ratio) + float64(endColor.R)*ratio)
g := uint8(float64(startColor.G)*(1-ratio) + float64(endColor.G)*ratio)
b := uint8(float64(startColor.B)*(1-ratio) + float64(endColor.B)*ratio)
img.Set(x, y, color.RGBA{r, g, b, 255})
}
}
}
// addTextToImage adds text to the image at specified position
func (g *Generator) addTextToImage(img *image.RGBA, text string, y int, isTop bool) {
if text == "" {
return
}
// Use basic font (in a real implementation, you'd use a better font)
face := basicfont.Face7x13
// Calculate text position (center horizontally)
bounds := img.Bounds()
textWidth := len(text) * 7 // Approximate width with basic font
x := (bounds.Dx() - textWidth) / 2
if x < 10 {
x = 10
}
// Create text drawer
d := &font.Drawer{
Dst: img,
Src: image.NewUniform(color.RGBA{255, 255, 255, 255}), // White text
Face: face,
Dot: fixed.Point26_6{X: fixed.I(x), Y: fixed.I(y)},
}
// Add black outline for better readability
for dx := -1; dx <= 1; dx++ {
for dy := -1; dy <= 1; dy++ {
if dx == 0 && dy == 0 {
continue
}
d.Src = image.NewUniform(color.RGBA{0, 0, 0, 255}) // Black outline
d.Dot = fixed.Point26_6{X: fixed.I(x + dx), Y: fixed.I(y + dy)}
d.DrawString(text)
}
}
// Draw white text on top
d.Src = image.NewUniform(color.RGBA{255, 255, 255, 255})
d.Dot = fixed.Point26_6{X: fixed.I(x), Y: fixed.I(y)}
d.DrawString(text)
}
// GetMeme retrieves a meme by ID
func (g *Generator) GetMeme(id int) (*Meme, error) {
g.mutex.RLock()
defer g.mutex.RUnlock()
meme, exists := g.memes[id]
if !exists {
return nil, fmt.Errorf("meme with ID %d not found", id)
}
return meme, nil
}
// GetTemplates returns all available templates
func (g *Generator) GetTemplates() []Template {
templates := make([]Template, 0, len(g.templates))
for _, template := range g.templates {
templates = append(templates, template)
}
return templates
}
// GetRandomQuote returns a random meme-worthy quote
func (g *Generator) GetRandomQuote() string {
quotes := []string{
"When you realize it's Monday again",
"Me pretending to understand the requirements",
"Debugging code at 3 AM like",
"When the client changes their mind again",
"Coffee: the developer's best friend",
"When your code works on the first try",
"That feeling when you fix a bug",
"When someone says 'it should be easy'",
"Me explaining why I need more time",
"When the production server goes down",
}
return quotes[rand.Intn(len(quotes))]
}

223
internal/server/server.go Normal file
View File

@@ -0,0 +1,223 @@
package server
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/iwasforcedtobehere/gomeme/internal/meme"
)
// Server represents our meme server
type Server struct {
generator *meme.Generator
}
// New creates a new server instance
func New() *Server {
return &Server{
generator: meme.NewGenerator(),
}
}
// Routes sets up all the HTTP routes
func (s *Server) Routes() http.Handler {
r := mux.NewRouter()
// Static files
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("web/static/"))))
// Web interface
r.HandleFunc("/", s.homeHandler).Methods("GET")
r.HandleFunc("/generate", s.generateWebHandler).Methods("POST")
// API endpoints
api := r.PathPrefix("/api/v1").Subrouter()
api.HandleFunc("/memes", s.generateAPIHandler).Methods("POST")
api.HandleFunc("/memes/{id}", s.getMemeHandler).Methods("GET")
api.HandleFunc("/templates", s.getTemplatesHandler).Methods("GET")
api.HandleFunc("/health", s.healthHandler).Methods("GET")
return r
}
// homeHandler serves the main page
func (s *Server) homeHandler(w http.ResponseWriter, r *http.Request) {
html := `<!DOCTYPE html>
<html>
<head>
<title>GoMeme - Because Manual Meme Making is for Peasants</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { font-family: 'Comic Sans MS', cursive; background: linear-gradient(45deg, #ff6b6b, #4ecdc4); margin: 0; padding: 20px; }
.container { max-width: 800px; margin: 0 auto; background: white; border-radius: 15px; padding: 30px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); }
h1 { color: #333; text-align: center; font-size: 2.5em; margin-bottom: 10px; }
.subtitle { text-align: center; color: #666; font-size: 1.2em; margin-bottom: 30px; }
.form-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 5px; font-weight: bold; color: #333; }
input, select, textarea { width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 5px; font-size: 16px; }
button { background: #ff6b6b; color: white; padding: 15px 30px; border: none; border-radius: 5px; font-size: 18px; cursor: pointer; width: 100%; }
button:hover { background: #ff5252; }
.footer { text-align: center; margin-top: 30px; color: #666; font-style: italic; }
</style>
</head>
<body>
<div class="container">
<h1>🎭 GoMeme Generator</h1>
<p class="subtitle">Automated meme production because who has time for manual labor?</p>
<form action="/generate" method="POST">
<div class="form-group">
<label for="template">Meme Template:</label>
<select id="template" name="template" required>
<option value="drake">Drake Pointing</option>
<option value="distracted">Distracted Boyfriend</option>
<option value="woman_yelling">Woman Yelling at Cat</option>
<option value="change_my_mind">Change My Mind</option>
<option value="this_is_fine">This is Fine</option>
</select>
</div>
<div class="form-group">
<label for="top_text">Top Text:</label>
<textarea id="top_text" name="top_text" rows="2" placeholder="Enter your brilliant top text here..."></textarea>
</div>
<div class="form-group">
<label for="bottom_text">Bottom Text:</label>
<textarea id="bottom_text" name="bottom_text" rows="2" placeholder="Enter your even more brilliant bottom text here..."></textarea>
</div>
<button type="submit">Generate This Fucking Masterpiece</button>
</form>
<div class="footer">
<p>Built with Go, goroutines, and a healthy dose of sarcasm 🚀</p>
<p><a href="/api/v1/health" style="color: #666;">API Health Check</a> | <a href="/api/v1/templates" style="color: #666;">Available Templates</a></p>
</div>
</div>
</body>
</html>`
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, html)
}
// generateWebHandler handles web form submissions
func (s *Server) generateWebHandler(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Failed to parse form", http.StatusBadRequest)
return
}
req := meme.GenerateRequest{
Template: r.FormValue("template"),
TopText: r.FormValue("top_text"),
BottomText: r.FormValue("bottom_text"),
}
result, err := s.generator.Generate(req)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to generate meme: %v", err), http.StatusInternalServerError)
return
}
// Return HTML with the generated meme
html := fmt.Sprintf(`<!DOCTYPE html>
<html>
<head>
<title>Your Meme is Ready!</title>
<meta charset="UTF-8">
<style>
body { font-family: 'Comic Sans MS', cursive; background: linear-gradient(45deg, #ff6b6b, #4ecdc4); margin: 0; padding: 20px; text-align: center; }
.container { max-width: 600px; margin: 0 auto; background: white; border-radius: 15px; padding: 30px; }
img { max-width: 100%%; border-radius: 10px; box-shadow: 0 5px 15px rgba(0,0,0,0.3); }
.buttons { margin-top: 20px; }
.btn { display: inline-block; padding: 10px 20px; margin: 5px; background: #4ecdc4; color: white; text-decoration: none; border-radius: 5px; }
.btn:hover { background: #26a69a; }
</style>
</head>
<body>
<div class="container">
<h1>🎉 Your Meme is Ready!</h1>
<img src="/static/generated/%s" alt="Generated Meme">
<div class="buttons">
<a href="/" class="btn">Create Another Masterpiece</a>
<a href="/static/generated/%s" class="btn" download>Download This Beauty</a>
</div>
<p><strong>Share URL:</strong> <code>%s</code></p>
</div>
</body>
</html>`, result.Filename, result.Filename, result.ShareURL)
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, html)
}
// generateAPIHandler handles API meme generation requests
func (s *Server) generateAPIHandler(w http.ResponseWriter, r *http.Request) {
var req meme.GenerateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
result, err := s.generator.Generate(req)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to generate meme: %v", err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
// getMemeHandler retrieves a specific meme by ID
func (s *Server) getMemeHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
// Convert string ID to int for demo purposes
memeID, err := strconv.Atoi(id)
if err != nil {
http.Error(w, "Invalid meme ID", http.StatusBadRequest)
return
}
meme, err := s.generator.GetMeme(memeID)
if err != nil {
http.Error(w, "Meme not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(meme)
}
// getTemplatesHandler returns available meme templates
func (s *Server) getTemplatesHandler(w http.ResponseWriter, r *http.Request) {
templates := s.generator.GetTemplates()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"templates": templates,
"count": len(templates),
})
}
// healthHandler provides health check endpoint
func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {
response := map[string]interface{}{
"status": "healthy",
"message": "GoMeme is alive and ready to generate some fucking awesome memes!",
"timestamp": "2025-09-12T00:00:00Z",
"version": "1.0.0",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}