meme
This commit is contained in:
33
.github/copilot-instructions.md
vendored
Normal file
33
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Copilot Instructions for GoMeme Project
|
||||||
|
|
||||||
|
This is a professional Go project showcasing concurrent programming through a meme generator and server.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
- **Language**: Go
|
||||||
|
- **Focus**: Concurrency with goroutines and channels
|
||||||
|
- **Features**: Web interface, REST API, meme generation, image processing
|
||||||
|
- **Architecture**: Concurrent image processing, template system, shareable links
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
- Follow Go conventions and best practices
|
||||||
|
- Use meaningful variable and function names
|
||||||
|
- Implement proper error handling
|
||||||
|
- Document public functions and types
|
||||||
|
- Use Go modules for dependency management
|
||||||
|
|
||||||
|
## Key Components
|
||||||
|
- Web server with HTTP handlers
|
||||||
|
- Concurrent image processing pipeline
|
||||||
|
- Meme template system
|
||||||
|
- Random quote generator
|
||||||
|
- Image manipulation utilities
|
||||||
|
- REST API endpoints
|
||||||
|
|
||||||
|
## Project Complete
|
||||||
|
This GoMeme project has been successfully created with:
|
||||||
|
- ✅ Concurrent meme generation using goroutines and channels
|
||||||
|
- ✅ RESTful API with proper error handling
|
||||||
|
- ✅ Web interface for user interaction
|
||||||
|
- ✅ Professional code structure and documentation
|
||||||
|
- ✅ VS Code development environment setup
|
||||||
|
- ✅ Comprehensive README with technical details and humor
|
54
.gitignore
vendored
Normal file
54
.gitignore
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
bin/
|
||||||
|
gomeme
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
|
||||||
|
# Generated memes
|
||||||
|
web/static/generated/*.png
|
||||||
|
web/static/generated/*.jpg
|
||||||
|
web/static/generated/*.gif
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/settings.json
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
tmp/
|
||||||
|
temp/
|
13
.vscode/tasks.json
vendored
Normal file
13
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "Run GoMeme Server",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "go run ./cmd/server",
|
||||||
|
"isBackground": true,
|
||||||
|
"problemMatcher": [],
|
||||||
|
"group": "build"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
228
README.md
Normal file
228
README.md
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
# 🎭 GoMeme - Concurrent Meme Generator & Server
|
||||||
|
|
||||||
|
[](https://golang.org)
|
||||||
|
[](LICENSE)
|
||||||
|
[](#)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Welcome to **GoMeme** - a blazingly fast, concurrent meme generator built in Go that proves you don't need a fucking PhD in Computer Science to create something both useful and entertaining. This project showcases Go's concurrency model through a practical, scalable web application that generates memes faster than your brain can process dad jokes.
|
||||||
|
|
||||||
|
> *"Because manual meme creation is for peasants, and automation is the future."* - Someone who definitely doesn't work in manual labor
|
||||||
|
|
||||||
|
## 🚀 Features
|
||||||
|
|
||||||
|
### Core Functionality
|
||||||
|
- **Concurrent Processing**: Utilizes Go's goroutines and channels for lightning-fast meme generation
|
||||||
|
- **RESTful API**: Clean, documented endpoints for programmatic meme creation
|
||||||
|
- **Web Interface**: User-friendly HTML interface for those who prefer clicking to coding
|
||||||
|
- **Template System**: Extensible meme template architecture
|
||||||
|
- **Real-time Generation**: Live meme creation with shareable URLs
|
||||||
|
- **Graceful Shutdown**: Because crashing servers are so 2010
|
||||||
|
|
||||||
|
### Technical Highlights
|
||||||
|
- **5 Concurrent Workers**: Because multitasking is better than sitting around waiting
|
||||||
|
- **Gorilla Mux Router**: Professional-grade HTTP routing (no, not the animal)
|
||||||
|
- **Image Processing**: Dynamic text overlay with outline support
|
||||||
|
- **Template Management**: Modular template system for easy expansion
|
||||||
|
- **Health Monitoring**: Built-in health checks and status reporting
|
||||||
|
|
||||||
|
## 🛠️ Technical Architecture
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
```
|
||||||
|
gomeme/
|
||||||
|
├── cmd/server/ # Application entry point
|
||||||
|
├── internal/
|
||||||
|
│ ├── meme/ # Core meme generation logic
|
||||||
|
│ ├── server/ # HTTP server and routing
|
||||||
|
│ └── templates/ # Template management
|
||||||
|
├── web/
|
||||||
|
│ ├── static/ # Static assets and generated memes
|
||||||
|
│ └── templates/ # HTML templates
|
||||||
|
├── assets/ # Meme template assets
|
||||||
|
└── bin/ # Compiled binaries
|
||||||
|
```
|
||||||
|
|
||||||
|
### Concurrency Model
|
||||||
|
- **Worker Pool**: 5 background goroutines handle meme generation jobs
|
||||||
|
- **Job Queue**: Buffered channel (100 capacity) manages generation requests
|
||||||
|
- **Mutex Protection**: Thread-safe access to shared meme storage
|
||||||
|
- **Graceful Shutdown**: Context-based cleanup for clean exits
|
||||||
|
|
||||||
|
## 📦 Installation & Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- **Go 1.21+**: Because we're not living in the stone age
|
||||||
|
- **Git**: For cloning, obviously
|
||||||
|
- **Basic Understanding of Humor**: Optional but recommended
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://github.com/iwasforcedtobehere/gomeme.git
|
||||||
|
cd gomeme
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
go build -o bin/gomeme ./cmd/server
|
||||||
|
|
||||||
|
# Run the server
|
||||||
|
./bin/gomeme
|
||||||
|
```
|
||||||
|
|
||||||
|
Or if you prefer the development approach:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run directly with Go
|
||||||
|
go run ./cmd/server
|
||||||
|
```
|
||||||
|
|
||||||
|
The server will start on `http://localhost:8080` and immediately begin accepting requests for your meme generation needs.
|
||||||
|
|
||||||
|
## 🎯 API Documentation
|
||||||
|
|
||||||
|
### Endpoints
|
||||||
|
|
||||||
|
#### `GET /`
|
||||||
|
The main web interface. Perfect for humans who haven't yet embraced full automation.
|
||||||
|
|
||||||
|
#### `POST /generate`
|
||||||
|
Web form submission endpoint. Accepts form data and returns HTML with your freshly minted meme.
|
||||||
|
|
||||||
|
#### `POST /api/v1/memes`
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"template": "drake",
|
||||||
|
"top_text": "Manual meme creation",
|
||||||
|
"bottom_text": "Using GoMeme's automated system"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 42,
|
||||||
|
"filename": "meme_42_drake_1694534400.png",
|
||||||
|
"share_url": "/api/v1/memes/42",
|
||||||
|
"template": "drake"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `GET /api/v1/memes/{id}`
|
||||||
|
Retrieve a specific meme by ID. Because sometimes you need to admire your past work.
|
||||||
|
|
||||||
|
#### `GET /api/v1/templates`
|
||||||
|
List all available meme templates. Currently includes classics like:
|
||||||
|
- **Drake Pointing**: The OG approval/disapproval format
|
||||||
|
- **Distracted Boyfriend**: Relationship dynamics in image form
|
||||||
|
- **Woman Yelling at Cat**: Peak internet culture representation
|
||||||
|
- **Change My Mind**: For when you want to be controversial
|
||||||
|
- **This is Fine**: Perfect for depicting your current life situation
|
||||||
|
|
||||||
|
#### `GET /api/v1/health`
|
||||||
|
Health check endpoint. Returns status and a reassuring message that everything is fucking fantastic.
|
||||||
|
|
||||||
|
## 🎨 Available Templates
|
||||||
|
|
||||||
|
Currently supported meme templates (with more coming because the internet never stops creating):
|
||||||
|
|
||||||
|
1. **Drake** - The classic approval/disapproval format
|
||||||
|
2. **Distracted Boyfriend** - That guy looking at another option
|
||||||
|
3. **Woman Yelling at Cat** - Peak domestic drama
|
||||||
|
4. **Change My Mind** - Steven Crowder's debate setup
|
||||||
|
5. **This is Fine** - Dog in burning room (mood)
|
||||||
|
|
||||||
|
## 🧪 Testing & Development
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
go test -cover ./...
|
||||||
|
|
||||||
|
# Run specific package tests
|
||||||
|
go test ./internal/meme/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
The project includes VS Code tasks for streamlined development:
|
||||||
|
- **Run GoMeme Server**: Starts the server in development mode
|
||||||
|
- **Build**: Compiles the application
|
||||||
|
- **Test**: Runs the test suite
|
||||||
|
|
||||||
|
## 🚀 Deployment
|
||||||
|
|
||||||
|
### Docker Support
|
||||||
|
*Coming soon - because containerization is the future, and the future is now-ish*
|
||||||
|
|
||||||
|
### Binary Deployment
|
||||||
|
```bash
|
||||||
|
# Build for production
|
||||||
|
go build -ldflags="-w -s" -o gomeme ./cmd/server
|
||||||
|
|
||||||
|
# Run in production
|
||||||
|
./gomeme
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Whether you want to add new templates, improve the concurrency model, or just fix my questionable commenting style, feel free to submit a PR.
|
||||||
|
|
||||||
|
### Development Guidelines
|
||||||
|
- Follow Go conventions (gofmt is your friend)
|
||||||
|
- Add tests for new features (testing is not optional)
|
||||||
|
- Document public APIs (future you will thank present you)
|
||||||
|
- Keep the humor level appropriate for professional settings (mostly)
|
||||||
|
|
||||||
|
## 📊 Performance
|
||||||
|
|
||||||
|
- **Generation Speed**: Sub-second meme creation
|
||||||
|
- **Concurrent Capacity**: 100 queued jobs, 5 parallel workers
|
||||||
|
- **Memory Usage**: Minimal footprint with efficient image processing
|
||||||
|
- **Scalability**: Horizontal scaling ready (add more goroutines, add more power)
|
||||||
|
|
||||||
|
## 🎭 Philosophy
|
||||||
|
|
||||||
|
This project embodies the principle that professional software development doesn't have to be boring as fuck. It demonstrates:
|
||||||
|
|
||||||
|
- **Practical Concurrency**: Real-world application of Go's concurrency primitives
|
||||||
|
- **Clean Architecture**: Separation of concerns without over-engineering
|
||||||
|
- **User Experience**: Both programmatic and human-friendly interfaces
|
||||||
|
- **Scalable Design**: Built to handle growth (because viral memes are a thing)
|
||||||
|
- **Code Quality**: Professional standards with a sense of humor
|
||||||
|
|
||||||
|
## 📝 License
|
||||||
|
|
||||||
|
MIT License - Because sharing is caring, and lawyers are expensive.
|
||||||
|
|
||||||
|
## 🙏 Acknowledgments
|
||||||
|
|
||||||
|
- **The Go Team**: For creating a language that doesn't make you want to throw your computer out the window
|
||||||
|
- **The Internet**: For providing endless meme inspiration
|
||||||
|
- **Coffee**: The real MVP behind this project
|
||||||
|
- **Stack Overflow**: For answering questions I didn't even know I had
|
||||||
|
|
||||||
|
## 📧 Contact
|
||||||
|
|
||||||
|
Built with ❤️ and a healthy dose of sarcasm by [iwasforcedtobehere](https://github.com/iwasforcedtobehere)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*"Code like nobody's watching, but document like everybody is."* - Ancient Developer Proverb
|
||||||
|
|
||||||
|
**Note**: This project is a demonstration of Go's concurrency features through a practical, entertaining application. While the language might be colorful, the code quality is professional grade. Perfect for portfolios, technical interviews, and impressing people at developer meetups.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔗 Live Demo
|
||||||
|
|
||||||
|
Visit the deployed application: [https://gomeme.herokuapp.com](# "Coming soon to a cloud provider near you")
|
||||||
|
|
||||||
|
*Deployment status: Planning phase (aka "I'll get to it eventually")*
|
53
cmd/server/main.go
Normal file
53
cmd/server/main.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/iwasforcedtobehere/gomeme/internal/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Initialize the server
|
||||||
|
srv := server.New()
|
||||||
|
|
||||||
|
// Setup HTTP server
|
||||||
|
httpServer := &http.Server{
|
||||||
|
Addr: ":8080",
|
||||||
|
Handler: srv.Routes(),
|
||||||
|
ReadTimeout: 15 * time.Second,
|
||||||
|
WriteTimeout: 15 * time.Second,
|
||||||
|
IdleTimeout: 60 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start server in a goroutine
|
||||||
|
go func() {
|
||||||
|
log.Printf("🚀 GoMeme server starting on port 8080...")
|
||||||
|
log.Printf("🎭 Ready to generate some fucking awesome memes!")
|
||||||
|
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Fatalf("Server failed to start: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for interrupt signal to gracefully shutdown
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-quit
|
||||||
|
|
||||||
|
log.Println("🛑 Shutting down server...")
|
||||||
|
|
||||||
|
// Graceful shutdown with timeout
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := httpServer.Shutdown(ctx); err != nil {
|
||||||
|
log.Fatalf("Server forced to shutdown: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("✅ Server exited")
|
||||||
|
}
|
8
go.mod
Normal file
8
go.mod
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
module github.com/iwasforcedtobehere/gomeme
|
||||||
|
|
||||||
|
go 1.25.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gorilla/mux v1.8.1
|
||||||
|
golang.org/x/image v0.31.0
|
||||||
|
)
|
4
go.sum
Normal file
4
go.sum
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
|
golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA=
|
||||||
|
golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA=
|
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