Skip to content

Commit

Permalink
Add cover image upload and retrieval functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
abaldeweg authored Feb 15, 2025
1 parent ce92be8 commit 44d83ff
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 138 deletions.
7 changes: 7 additions & 0 deletions gateway/core/models/publicBook.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package models

import (
"github.com/abaldeweg/warehouse-server/gateway/cover"
"github.com/google/uuid"
"gorm.io/gorm"
)
Expand Down Expand Up @@ -36,6 +37,9 @@ type PublicBook struct {
Removed bool `json:"-" gorm:"default:false"`
Reserved bool `json:"-" gorm:"default:false"`
Recommendation bool `json:"-" gorm:"default:false"`
CoverS string `json:"cover_s" gorm:"default:null"`
CoverM string `json:"cover_m" gorm:"default:null"`
CoverL string `json:"cover_l" gorm:"default:null"`
}

// TableName overrides the default table name for PublicBook model.
Expand All @@ -60,5 +64,8 @@ func (book *PublicBook) AfterFind(tx *gorm.DB) (err error) {
book.Cond = book.Condition.Name
book.FormatName = book.Format.Name
book.BranchCart = book.Branch.Cart
book.CoverS = cover.ShowCover("s", book.ID)
book.CoverM = cover.ShowCover("m", book.ID)
book.CoverL = cover.ShowCover("l", book.ID)
return
}
148 changes: 10 additions & 138 deletions gateway/cover/cover.go
Original file line number Diff line number Diff line change
@@ -1,156 +1,28 @@
package cover

import (
"bytes"
"fmt"
"image"
"image/jpeg"
"image/png"
"mime/multipart"
"net/http"
"os"
"path/filepath"

"github.com/disintegration/imaging"
"github.com/gin-gonic/gin"
"golang.org/x/image/webp"
)

const uploadsDir = "uploads"

// SaveCover saves the uploaded cover image in different sizes.
func SaveCover(c *gin.Context, imageUUID string) {
imageData, err := c.FormFile("cover")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Image upload required"})
return
}

if err := saveResizedImages(c, imageData, imageUUID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"})
return
}

c.Status(http.StatusOK)
}

func saveResizedImages(c *gin.Context, imageData *multipart.FileHeader, imageUUID string) error {
imagePath, err := saveUploadedImage(c, imageData, imageUUID)
if err != nil {
return fmt.Errorf("failed to save uploaded image: %w", err)
}
defer os.Remove(imagePath)

sizes := []struct {
width int
suffix string
}{
{400, "l"},
{200, "m"},
{100, "s"},
}

for _, size := range sizes {
resizedImagePath := filepath.Join(uploadsDir, fmt.Sprintf("%s-%s%s", imageUUID, size.suffix, ".jpg"))

if err := resizeAndSaveImage(imagePath, resizedImagePath, size.width); err != nil {
return fmt.Errorf("failed to resize image: %w", err)
}
}
const (
Quality = 75
)

return nil
var Sizes = map[string]int{
"l": 400,
"m": 200,
"s": 100,
}

func saveUploadedImage(c *gin.Context, imageData *multipart.FileHeader, imageUUID string) (string, error) {
imageFilename := fmt.Sprintf("%s%s", imageUUID, filepath.Ext(imageData.Filename))
func getPath() (string, error) {
currentDir, _ := os.Getwd()
uploadsDirPath := filepath.Join(currentDir, uploadsDir)
uploadsDirPath := filepath.Join(currentDir, "uploads")

if err := os.MkdirAll(uploadsDirPath, 0755); err != nil {
return "", fmt.Errorf("failed to create uploads directory")
}

imagePath := filepath.Join(uploadsDirPath, imageFilename)
if err := c.SaveUploadedFile(imageData, imagePath); err != nil {
return "", fmt.Errorf("failed to save image")
}

return imagePath, nil
}

func resizeAndSaveImage(imagePath string, resizedImagePath string, width int) error {
file, err := os.Open(imagePath)
if err != nil {
return fmt.Errorf("failed to open image: %w", err)
}
defer file.Close()

var img image.Image

fileHeader := make([]byte, 512)
if _, err := file.Read(fileHeader); err != nil {
return fmt.Errorf("failed to read file header: %w", err)
}

mimeType := http.DetectContentType(fileHeader)
_, err = file.Seek(0, 0)
if err != nil {
return fmt.Errorf("failed to reset file pointer: %w", err)
}

switch mimeType {
case "image/jpeg":
img, err = jpeg.Decode(file)
case "image/png":
img, err = png.Decode(file)
case "image/webp":
img, err = convertWebp(file)
default:
return fmt.Errorf("unsupported image format")
}

if err != nil {
return fmt.Errorf("failed to decode image: %w", err)
}

originalBounds := img.Bounds()
aspectRatio := float64(originalBounds.Dx()) / float64(originalBounds.Dy())
height := int(float64(width) / aspectRatio)

resizedImage := imaging.Resize(img, width, height, imaging.Lanczos)

outFile, err := os.Create(resizedImagePath)
if err != nil {
return fmt.Errorf("failed to create resized image file: %w", err)
}
defer outFile.Close()

err = jpeg.Encode(outFile, resizedImage, nil)
if err != nil {
return fmt.Errorf("failed to encode resized image: %w", err)
}

return nil
}

func convertWebp(file *os.File) (image.Image, error) {
var err error
var img image.Image
img, err = webp.Decode(file)
if err != nil {
return nil, err
}

buf := new(bytes.Buffer)
err = jpeg.Encode(buf, img, nil)
if err != nil {
return nil, err
}

img, err = jpeg.Decode(buf)
if err != nil {
return nil, err
}

return img, nil
return uploadsDirPath, nil
}
156 changes: 156 additions & 0 deletions gateway/cover/cover_save.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package cover

import (
"bytes"
"fmt"
"image"
"image/jpeg"
"image/png"
"mime/multipart"
"net/http"
"os"
"path/filepath"

"github.com/disintegration/imaging"
"github.com/gin-gonic/gin"
"golang.org/x/image/webp"
)

const uploadsDir = "uploads"

// SaveCover saves the uploaded cover image in different sizes.
func SaveCover(c *gin.Context, imageUUID string) {
imageData, err := c.FormFile("cover")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Image upload required"})
return
}

if err := saveResizedImages(c, imageData, imageUUID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"})
return
}

c.Status(http.StatusOK)
}

func saveResizedImages(c *gin.Context, imageData *multipart.FileHeader, imageUUID string) error {
imagePath, err := saveUploadedImage(c, imageData, imageUUID)
if err != nil {
return fmt.Errorf("failed to save uploaded image: %w", err)
}
defer os.Remove(imagePath)

sizes := []struct {
width int
suffix string
}{
{400, "l"},
{200, "m"},
{100, "s"},
}

for _, size := range sizes {
resizedImagePath := filepath.Join(uploadsDir, fmt.Sprintf("%s-%s%s", imageUUID, size.suffix, ".jpg"))

if err := resizeAndSaveImage(imagePath, resizedImagePath, size.width); err != nil {
return fmt.Errorf("failed to resize image: %w", err)
}
}

return nil
}

func saveUploadedImage(c *gin.Context, imageData *multipart.FileHeader, imageUUID string) (string, error) {
imageFilename := fmt.Sprintf("%s%s", imageUUID, filepath.Ext(imageData.Filename))
currentDir, _ := os.Getwd()
uploadsDirPath := filepath.Join(currentDir, uploadsDir)

if err := os.MkdirAll(uploadsDirPath, 0755); err != nil {
return "", fmt.Errorf("failed to create uploads directory")
}

imagePath := filepath.Join(uploadsDirPath, imageFilename)
if err := c.SaveUploadedFile(imageData, imagePath); err != nil {
return "", fmt.Errorf("failed to save image")
}

return imagePath, nil
}

func resizeAndSaveImage(imagePath string, resizedImagePath string, width int) error {
file, err := os.Open(imagePath)
if err != nil {
return fmt.Errorf("failed to open image: %w", err)
}
defer file.Close()

var img image.Image

fileHeader := make([]byte, 512)
if _, err := file.Read(fileHeader); err != nil {
return fmt.Errorf("failed to read file header: %w", err)
}

mimeType := http.DetectContentType(fileHeader)
_, err = file.Seek(0, 0)
if err != nil {
return fmt.Errorf("failed to reset file pointer: %w", err)
}

switch mimeType {
case "image/jpeg":
img, err = jpeg.Decode(file)
case "image/png":
img, err = png.Decode(file)
case "image/webp":
img, err = convertWebp(file)
default:
return fmt.Errorf("unsupported image format")
}

if err != nil {
return fmt.Errorf("failed to decode image: %w", err)
}

originalBounds := img.Bounds()
aspectRatio := float64(originalBounds.Dx()) / float64(originalBounds.Dy())
height := int(float64(width) / aspectRatio)

resizedImage := imaging.Resize(img, width, height, imaging.Lanczos)

outFile, err := os.Create(resizedImagePath)
if err != nil {
return fmt.Errorf("failed to create resized image file: %w", err)
}
defer outFile.Close()

err = jpeg.Encode(outFile, resizedImage, nil)
if err != nil {
return fmt.Errorf("failed to encode resized image: %w", err)
}

return nil
}

func convertWebp(file *os.File) (image.Image, error) {
var err error
var img image.Image
img, err = webp.Decode(file)
if err != nil {
return nil, err
}

buf := new(bytes.Buffer)
err = jpeg.Encode(buf, img, nil)
if err != nil {
return nil, err
}

img, err = jpeg.Decode(buf)
if err != nil {
return nil, err
}

return img, nil
}
File renamed without changes.
24 changes: 24 additions & 0 deletions gateway/cover/cover_show.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package cover

import (
"encoding/base64"
"os"
"path/filepath"

"github.com/google/uuid"
)

func ShowCover(size string, bookID uuid.UUID) string {
path, _ := getPath()
filename := filepath.Join(path, bookID.String()+"-"+size+".jpg")

if _, err := os.Stat(filename); err != nil {
filename = filepath.Join(path, "none.jpg")
}

data, err := os.ReadFile(filename)
if err != nil {
return ""
}
return "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(data)
}
Binary file added gateway/uploads/none.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 44d83ff

Please sign in to comment.