decentral1se 727ab07a3e
All checks were successful
continuous-integration/drone/push Build is passing
chore: vendor
2024-08-04 10:59:32 +02:00

205 lines
4.8 KiB
Go

// Package thumbnail provides a method to create thumbnails from images.
package thumbnail
import (
"bytes"
"errors"
"image"
"image/jpeg"
"image/png"
"net/http"
"os"
"golang.org/x/image/draw"
)
// An Image is an image and information about it.
type Image struct {
// Path is a path to an image.
Path string
// ContentType is the content type of the image.
ContentType string
// Data is the image data in a byte-array
Data []byte
// Size is the length of Data
Size int
// Current stores the existing image's dimensions
Current Dimensions
// Future store the new thumbnail dimensions.
Future Dimensions
}
// Dimensions stores dimensional information for an Image.
type Dimensions struct {
// Width is the width of an image in pixels.
Width int
// Height is the height on an image in pixels.
Height int
// X is the right-most X-coordinate.
X int
// Y is the top-most Y-coordinate.
Y int
}
var (
// ErrInvalidMimeType is returned when a non-image content type is
// detected.
ErrInvalidMimeType = errors.New("invalid mimetype")
// ErrInvalidScaler is returned when an unrecognized scaler is
// passed to the Generator.
ErrInvalidScaler = errors.New("invalid scaler")
)
// NewGenerator returns an instance of a thumbnail generator with a
// given configuration.
func NewGenerator(c Generator) *Generator {
return &Generator{
Width: 300,
Height: 300,
DestinationPath: c.DestinationPath,
DestinationPrefix: c.DestinationPrefix,
Scaler: c.Scaler,
}
}
// NewImageFromFile reads in an image file from the file system and
// populates an Image object. That new Image object is returned along
// with any errors that occur during the operation.
func (gen *Generator) NewImageFromFile(path string) (*Image, error) {
imageBytes, err := os.ReadFile(path)
if err != nil {
return nil, err
}
contentType := detectContentType(imageBytes)
return &Image{
Path: path,
ContentType: contentType,
Data: imageBytes,
Size: len(imageBytes),
Current: Dimensions{
Width: 0,
Height: 0,
},
Future: Dimensions{
Width: gen.Width,
Height: gen.Height,
},
}, nil
}
// NewImageFromByteArray reads in an image from a byte array and
// populates an Image object. That new Image object is returned along
// with any errors that occur during the operation.
func (gen *Generator) NewImageFromByteArray(imageBytes []byte) (*Image, error) {
contentType := detectContentType(imageBytes)
return &Image{
ContentType: contentType,
Data: imageBytes,
Size: len(imageBytes),
Current: Dimensions{
Width: 0,
Height: 0,
},
Future: Dimensions{
Width: gen.Width,
Height: gen.Height,
},
}, nil
}
// Generator registers a generator configuration to be used when
// creating thumbnails.
type Generator struct {
// Width is the destination thumbnail width.
Width int
// Height is the destination thumbnail height.
Height int
// DestinationPath is the destination thumbnail path.
DestinationPath string
// DestinationPrefix is the prefix for the destination thumbnail
// filename.
DestinationPrefix string
// Scaler is the scaler to be used when generating thumbnails.
Scaler string
}
// CreateThumbnail generates a thumbnail.
func (gen *Generator) CreateThumbnail(i *Image) ([]byte, error) {
if i.ContentType == "application/octet-stream" {
return nil, ErrInvalidMimeType
}
dst, err := gen.createRect(i)
if err != nil {
return nil, err
}
var buffer bytes.Buffer
switch i.ContentType {
case "image/jpeg":
err = jpeg.Encode(&buffer, dst, nil)
case "image/png":
err = png.Encode(&buffer, dst)
}
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func (gen *Generator) createRect(i *Image) (*image.RGBA, error) {
img, _, err := image.Decode(bytes.NewReader(i.Data))
if err != nil {
return nil, err
}
var (
width = img.Bounds().Max.X
height = img.Bounds().Max.Y
x = gen.Width * width / height
y = gen.Height
)
gen.Width = x
gen.Height = y
rect := image.Rect(0, 0, x, y)
dst := image.NewRGBA(rect)
var scaler draw.Interpolator
switch scalerChoice := gen.Scaler; scalerChoice {
case "NearestNeighbor":
scaler = draw.NearestNeighbor
case "ApproxBiLinear":
scaler = draw.ApproxBiLinear
case "BiLinear":
scaler = draw.BiLinear
case "CatmullRom":
scaler = draw.CatmullRom
}
if scaler == nil {
return nil, ErrInvalidScaler
}
scaler.Scale(dst, rect, img, img.Bounds(), draw.Over, nil)
return dst, nil
}
// detectContentType from
// https://golangcode.com/get-the-content-type-of-file/
func detectContentType(fb []byte) string {
// Only the first 512 bytes are used to sniff the content type.
// Use the net/http package's handy DetectContentType function.
// Always seems to return a valid content-type by returning
// "application/octet-stream" if no others seemed to match.
return http.DetectContentType(fb[:512])
}