This repository has been archived on 2022-08-26. You can view files and clone it, but cannot push or open issues or pull requests.
gosocial/pkg/photo/upload.go
Noah Petherbridge b72973e741 Photo Upload & Profile Pictures
Basic photo upload support. Square cropped images still buggy.
2022-08-11 23:04:08 -07:00

174 lines
4.4 KiB
Go

package photo
import (
"bytes"
"errors"
"fmt"
"image"
"image/jpeg"
"image/png"
"io"
"os"
"git.kirsle.net/apps/gosocial/pkg/config"
"git.kirsle.net/apps/gosocial/pkg/log"
"git.kirsle.net/apps/gosocial/pkg/models"
"github.com/edwvee/exiffix"
"golang.org/x/image/draw"
)
type UploadConfig struct {
User *models.User
Extension string // 'jpg' or 'png' only.
Data []byte
Crop []int // x, y, w, h
}
// UploadPhoto handles an incoming photo to add to a user's account.
//
// Returns:
// - NewFilename() of the created photo file on disk.
// - NewFilename() of the cropped version, or "" if not cropping.
// - error on errors
func UploadPhoto(cfg UploadConfig) (string, string, error) {
// Validate and normalize the extension.
var extension = cfg.Extension
switch cfg.Extension {
case ".jpg":
fallthrough
case ".jpe":
fallthrough
case ".jpeg":
extension = ".jpg"
case ".png":
extension = ".png"
default:
return "", "", errors.New("unsupported image extension, must be jpg or png")
}
// Decide on a filename for this photo.
var (
filename = NewFilename(extension)
cropFilename = NewFilename(extension)
)
// Decode the image using exiffix, which will auto-rotate jpeg images
// based on their EXIF tags.
reader := bytes.NewReader(cfg.Data)
origImage, _, err := exiffix.Decode(reader)
if err != nil {
return "", "", err
}
// Read the config to get the image width.
reader.Seek(0, io.SeekStart)
var width, height int
if decoded, _, err := image.DecodeConfig(reader); err == nil {
width, height = decoded.Width, decoded.Height
} else {
return "", "", err
}
// Find the longest edge, if it's too large (over 1280px)
// cap it to the max and scale the other dimension proportionally.
log.Debug("UploadPhoto: taking a %dx%d image to name it %s", width, height, filename)
if width >= height {
if width > config.MaxPhotoWidth {
newWidth := config.MaxPhotoWidth
log.Debug("(%d / %d) * %d", width, height, newWidth)
height = int((float64(height) / float64(width)) * float64(newWidth))
width = newWidth
log.Debug("Its longest is width, scale to %sx%s", width, height)
}
} else {
if height > config.MaxPhotoWidth {
newHeight := config.MaxPhotoWidth
width = int((float64(width) / float64(height)) * float64(newHeight))
height = newHeight
log.Debug("Its longest is height, scale to %sx%s", width, height)
}
}
// Scale the image.
scaledImg := Scale(origImage, image.Rect(0, 0, width, height), draw.ApproxBiLinear)
// Write the image to disk.
if err := ToDisk(filename, extension, scaledImg); err != nil {
return "", "", err
}
// Are we producing a cropped image, too?
log.Error("Are we to crop? %+v", cfg.Crop)
if cfg.Crop != nil && len(cfg.Crop) >= 4 {
log.Debug("Also cropping this image to %+v", cfg.Crop)
var (
x = cfg.Crop[0]
y = cfg.Crop[1]
w = cfg.Crop[2]
h = cfg.Crop[3]
)
croppedImg := Crop(origImage, image.Rect(
x,
y,
w,
h,
))
// Write that to disk, too.
log.Debug("Writing cropped image to disk: %s", cropFilename)
if err := ToDisk(cropFilename, extension, croppedImg); err != nil {
return filename, "", err
}
// Return both filenames!
return filename, cropFilename, nil
}
// Not cropping, return only the first filename.
return filename, "", nil
}
// Scale down an image. Example:
//
// scaled := Scale(src, image.Rect(0, 0, 200, 200), draw.ApproxBiLinear)
func Scale(src image.Image, rect image.Rectangle, scale draw.Scaler) image.Image {
dst := image.NewRGBA(rect)
scale.Scale(dst, rect, src, src.Bounds(), draw.Over, nil)
return dst
}
// Crop an image, returning the new image. Example:
//
// cropped := Crop()
func Crop(src image.Image, rect image.Rectangle) image.Image {
dst := image.NewRGBA(rect)
draw.Copy(dst, image.Point{}, src, rect, draw.Over, nil)
return dst
}
// ToDisk commits a photo image to disk in the right file format.
//
// Filename is like NewFilename() and it goes to e.g. "./web/static/photos/"
func ToDisk(filename string, extension string, img image.Image) error {
if path, err := EnsurePath(filename); err == nil {
fh, err := os.Create(path)
if err != nil {
return err
}
defer fh.Close()
switch extension {
case ".jpg":
jpeg.Encode(fh, img, &jpeg.Options{
Quality: config.JpegQuality,
})
case ".png":
png.Encode(fh, img)
}
} else {
return fmt.Errorf("couldn't EnsurePath: %s", err)
}
return nil
}