meme
This commit is contained in:
323
internal/meme/generator.go
Normal file
323
internal/meme/generator.go
Normal 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
223
internal/server/server.go
Normal 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)
|
||||
}
|
Reference in New Issue
Block a user