253 lines
6.5 KiB
Go
253 lines
6.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"image"
|
|
"image/jpeg"
|
|
"image/png"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/Thomasorus/Ronbun-CMS/internal/models"
|
|
"github.com/Thomasorus/Ronbun-CMS/ui"
|
|
"github.com/chai2010/webp"
|
|
"github.com/disintegration/imaging"
|
|
"github.com/fogleman/gg"
|
|
"golang.org/x/image/font/opentype"
|
|
)
|
|
|
|
type ImageProcessor struct {
|
|
Width int
|
|
Height int
|
|
Quality int
|
|
UploadDir string
|
|
OutputDir string
|
|
OutputFormats []string
|
|
}
|
|
|
|
func NewImageProcessor() *ImageProcessor {
|
|
return &ImageProcessor{
|
|
Width: 800,
|
|
Height: 0,
|
|
Quality: 80,
|
|
UploadDir: "./uploaded",
|
|
OutputDir: "./processed",
|
|
OutputFormats: []string{"jpg", "webp"},
|
|
}
|
|
}
|
|
|
|
func (p *ImageProcessor) ProcessImage(inputPath string) error {
|
|
img, err := imaging.Open(inputPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open %s: %w", inputPath, err)
|
|
}
|
|
|
|
if err := os.MkdirAll(p.OutputDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create output directory: %w", err)
|
|
}
|
|
|
|
baseName := strings.TrimSuffix(filepath.Base(inputPath), filepath.Ext(inputPath))
|
|
resized := p.resizeImage(img)
|
|
basePath := filepath.Join(p.OutputDir, baseName)
|
|
|
|
// Determine output formats based on input format
|
|
outputFormats := p.getOutputFormats(inputPath)
|
|
|
|
if err := p.saveFormats(resized, basePath, outputFormats); err != nil {
|
|
return fmt.Errorf("failed to save formats: %w", err)
|
|
}
|
|
|
|
for _, format := range outputFormats {
|
|
fmt.Printf("Saved: %s.%s\n", basePath, format)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *ImageProcessor) resizeImage(img image.Image) image.Image {
|
|
if p.Width == 0 && p.Height == 0 {
|
|
return img
|
|
}
|
|
|
|
if p.Width == 0 {
|
|
return imaging.Resize(img, 0, p.Height, imaging.Lanczos)
|
|
}
|
|
if p.Height == 0 {
|
|
return imaging.Resize(img, p.Width, 0, imaging.Lanczos)
|
|
}
|
|
|
|
return imaging.Resize(img, p.Width, p.Height, imaging.Lanczos)
|
|
}
|
|
|
|
func (p *ImageProcessor) saveFormats(img image.Image, basePath string, formats []string) error {
|
|
var errors []string
|
|
|
|
for _, format := range formats {
|
|
path := fmt.Sprintf("%s.%s", basePath, format)
|
|
if err := p.saveFormat(img, path, format); err != nil {
|
|
errors = append(errors, fmt.Sprintf("%s: %v", format, err))
|
|
}
|
|
}
|
|
|
|
if len(errors) > 0 {
|
|
return fmt.Errorf("save errors: %s", strings.Join(errors, "; "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *ImageProcessor) saveFormat(img image.Image, path, format string) error {
|
|
file, err := os.Create(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
switch strings.ToLower(format) {
|
|
case "jpg", "jpeg":
|
|
// JPEG doesn't support transparency - convert to opaque background
|
|
bounds := img.Bounds()
|
|
opaque := image.NewRGBA(bounds)
|
|
// Fill with white background
|
|
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
|
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
|
opaque.Set(x, y, img.At(x, y))
|
|
}
|
|
}
|
|
return jpeg.Encode(file, opaque, &jpeg.Options{Quality: p.Quality})
|
|
case "png":
|
|
// PNG encoder preserves transparency automatically
|
|
encoder := &png.Encoder{CompressionLevel: png.BestCompression}
|
|
return encoder.Encode(file, img)
|
|
case "webp":
|
|
// WebP with lossless encoding to preserve transparency
|
|
options := &webp.Options{
|
|
Quality: float32(p.Quality),
|
|
Lossless: p.Quality >= 90, // Use lossless for high quality
|
|
}
|
|
return webp.Encode(file, img, options)
|
|
case "gif":
|
|
// GIF encoding would require additional library
|
|
// For now, save as PNG to preserve transparency
|
|
encoder := &png.Encoder{CompressionLevel: png.BestCompression}
|
|
return encoder.Encode(file, img)
|
|
default:
|
|
return fmt.Errorf("unsupported format: %s", format)
|
|
}
|
|
}
|
|
|
|
func (p *ImageProcessor) ProcessBatch(inputDir string) error {
|
|
files, err := filepath.Glob(filepath.Join(inputDir, "*"))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read directory: %w", err)
|
|
}
|
|
|
|
imageExts := map[string]bool{
|
|
".jpg": true, ".jpeg": true, ".png": true,
|
|
".gif": true, ".bmp": true, ".tiff": true,
|
|
".heif": true, ".hevc": true,
|
|
}
|
|
|
|
var processed int
|
|
for _, file := range files {
|
|
ext := strings.ToLower(filepath.Ext(file))
|
|
if !imageExts[ext] {
|
|
continue
|
|
}
|
|
|
|
if err := p.ProcessImage(file); err != nil {
|
|
fmt.Printf("Error processing %s: %v\n", file, err)
|
|
continue
|
|
}
|
|
processed++
|
|
}
|
|
|
|
fmt.Printf("Processed %d images\n", processed)
|
|
return nil
|
|
}
|
|
|
|
// getOutputFormats determines output formats based on input file extension
|
|
func (p *ImageProcessor) getOutputFormats(inputPath string) []string {
|
|
ext := strings.ToLower(filepath.Ext(inputPath))
|
|
|
|
// Preserve transparency formats (PNG/GIF) - keep original format
|
|
switch ext {
|
|
case ".png":
|
|
return []string{"png"}
|
|
case ".gif":
|
|
return []string{"gif"}
|
|
default:
|
|
// For other formats (JPG, BMP, TIFF, etc.), use default formats
|
|
return p.OutputFormats
|
|
}
|
|
}
|
|
|
|
func generateOpenGraphImage(page models.Page) (bytes.Buffer, error) {
|
|
|
|
var categoryColors = map[string]string{
|
|
"knowledge": "#9094fe",
|
|
"projects": "#70eba7",
|
|
"journal": "#f2d336",
|
|
"about": "#ffadc9",
|
|
"portfolio": "#f27149",
|
|
"hire": "#ffffff",
|
|
"none": "#eeeeee",
|
|
}
|
|
|
|
color, ok := categoryColors[page.Category]
|
|
if !ok {
|
|
color = categoryColors["none"]
|
|
}
|
|
|
|
imageBytes, err := ui.Files.Open("static/og-" + page.Category + ".png")
|
|
if err != nil {
|
|
return bytes.Buffer{}, err
|
|
}
|
|
imageBytes.Close()
|
|
|
|
catImg, _, err := image.Decode(imageBytes)
|
|
if err != nil {
|
|
return bytes.Buffer{}, err
|
|
}
|
|
|
|
dc := gg.NewContext(1200, 630)
|
|
dc.SetHexColor(color)
|
|
dc.DrawRectangle(0, 0, 1200, 630)
|
|
dc.Fill()
|
|
dc.DrawImage(catImg, 850, 150)
|
|
dc.SetHexColor("#000000")
|
|
|
|
interFontBytes, err := ui.Files.ReadFile("static/fonts/subsetted/Inter-roman.var-subset.ttf")
|
|
if err != nil {
|
|
return bytes.Buffer{}, err
|
|
}
|
|
interFont, _ := opentype.Parse(interFontBytes)
|
|
interFace, _ := opentype.NewFace(interFont, &opentype.FaceOptions{
|
|
Size: 30,
|
|
DPI: 72,
|
|
})
|
|
dc.SetFontFace(interFace)
|
|
dc.DrawStringWrapped("THOMASORUS.COM", 50, 50, 0, 0, 1100, 0.8, gg.AlignLeft)
|
|
dc.DrawStringWrapped(page.Summary, 50, 500, 0, 0, 800, 0.8, gg.AlignLeft)
|
|
|
|
editorialFontBytes, err := ui.Files.ReadFile("static/fonts/subsetted/EditorialNew-Regular-subset.ttf")
|
|
if err != nil {
|
|
return bytes.Buffer{}, err
|
|
}
|
|
editorialFont, _ := opentype.Parse(editorialFontBytes)
|
|
editorialFace, _ := opentype.NewFace(editorialFont, &opentype.FaceOptions{
|
|
Size: 80,
|
|
DPI: 72,
|
|
})
|
|
dc.SetFontFace(editorialFace)
|
|
dc.DrawStringWrapped(page.Name, 50, 150, 0, 0, 750, 0.8, gg.AlignLeft)
|
|
|
|
buf := new(bytes.Buffer)
|
|
err = jpeg.Encode(buf, dc.Image(), &jpeg.Options{Quality: 85})
|
|
if err != nil {
|
|
return bytes.Buffer{}, err
|
|
}
|
|
|
|
return *buf, err
|
|
}
|