2024-02-17 06:35:49 +00:00
|
|
|
// Package thumbnail automatically generates thumbnails for a variety of common
|
|
|
|
// image formats and saves them to a user-configurable path.
|
|
|
|
package thumbnail
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"image/color"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"git.foosoft.net/alex/goldsmith"
|
|
|
|
"git.foosoft.net/alex/goldsmith/filters/wildcard"
|
|
|
|
"github.com/disintegration/imaging"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Namer callback function builds thumbnail file paths based on the original file path.
|
|
|
|
// An empty path can be returned if a thumbnail should not be generated for the current file.
|
|
|
|
type Namer func(string, int) string
|
|
|
|
|
|
|
|
// Desired thumbnailing styles.
|
|
|
|
type Style int
|
|
|
|
|
|
|
|
const (
|
|
|
|
Fit Style = iota
|
|
|
|
Crop
|
|
|
|
Pad
|
|
|
|
)
|
|
|
|
|
|
|
|
// Thumbnail chainable context.
|
|
|
|
type Thumbnail struct {
|
|
|
|
size int
|
|
|
|
style Style
|
|
|
|
color color.Color
|
|
|
|
namer Namer
|
|
|
|
}
|
|
|
|
|
|
|
|
// New creates a new instance of the Thumbnail plugin.
|
|
|
|
func New() *Thumbnail {
|
|
|
|
namer := func(path string, dims int) string {
|
|
|
|
ext := filepath.Ext(path)
|
|
|
|
body := strings.TrimSuffix(path, ext)
|
|
|
|
return fmt.Sprintf("%s-thumb.png", body)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &Thumbnail{
|
|
|
|
128,
|
|
|
|
Fit,
|
|
|
|
color.Transparent,
|
|
|
|
namer,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Size sets the desired maximum pixel size of generated thumbnails (default: 128).
|
|
|
|
func (self *Thumbnail) Size(dims int) *Thumbnail {
|
|
|
|
self.size = dims
|
|
|
|
return self
|
|
|
|
}
|
|
|
|
|
|
|
|
// Style sets the desired thumbnailing style.
|
|
|
|
func (self *Thumbnail) Style(style Style) *Thumbnail {
|
|
|
|
self.style = style
|
|
|
|
return self
|
|
|
|
}
|
|
|
|
|
|
|
|
// Namer sets the callback used to build paths for thumbnail files.
|
|
|
|
// Default naming appends "-thumb" to the path and changes extension to PNG,
|
|
|
|
// for example "file.jpg" becomes "file-thumb.png".
|
|
|
|
func (self *Thumbnail) Namer(namer Namer) *Thumbnail {
|
|
|
|
self.namer = namer
|
|
|
|
return self
|
|
|
|
}
|
|
|
|
|
|
|
|
func (*Thumbnail) Name() string {
|
|
|
|
return "thumbnail"
|
|
|
|
}
|
|
|
|
|
2024-03-04 01:46:38 +00:00
|
|
|
func (*Thumbnail) Initialize(context *goldsmith.Context) error {
|
2024-02-17 06:35:49 +00:00
|
|
|
context.Filter(wildcard.New("**/*.jpg", "**/*.jpeg", "**/*.gif", "**/*.png"))
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-03-04 02:09:21 +00:00
|
|
|
func (self *Thumbnail) Process(context *goldsmith.Context, inputFile *goldsmith.File) error {
|
2024-02-17 06:35:49 +00:00
|
|
|
defer context.DispatchFile(inputFile)
|
|
|
|
|
|
|
|
thumbPath := self.namer(inputFile.Path(), self.size)
|
|
|
|
if len(thumbPath) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if outputFile := context.RetrieveCachedFile(thumbPath, inputFile); outputFile != nil {
|
|
|
|
outputFile.CopyProps(inputFile)
|
|
|
|
context.DispatchFile(outputFile)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
outputFile, err := self.thumbnail(context, inputFile, thumbPath)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
context.DispatchAndCacheFile(outputFile, inputFile)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-03-04 02:09:21 +00:00
|
|
|
func (self *Thumbnail) thumbnail(context *goldsmith.Context, inputFile *goldsmith.File, thumbPath string) (*goldsmith.File, error) {
|
2024-02-17 06:35:49 +00:00
|
|
|
var thumbFormat imaging.Format
|
|
|
|
switch strings.ToLower(filepath.Ext(thumbPath)) {
|
|
|
|
case ".jpg", ".jpeg":
|
|
|
|
thumbFormat = imaging.JPEG
|
|
|
|
case ".gif":
|
|
|
|
thumbFormat = imaging.GIF
|
|
|
|
case ".png":
|
|
|
|
thumbFormat = imaging.PNG
|
|
|
|
default:
|
|
|
|
return nil, errors.New("unsupported image format")
|
|
|
|
}
|
|
|
|
|
|
|
|
thumbImage, err := imaging.Decode(inputFile)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
switch self.style {
|
|
|
|
case Fit:
|
|
|
|
thumbImage = imaging.Fit(thumbImage, self.size, self.size, imaging.Lanczos)
|
|
|
|
case Crop:
|
|
|
|
thumbImage = imaging.Fill(thumbImage, self.size, self.size, imaging.Center, imaging.Lanczos)
|
|
|
|
case Pad:
|
|
|
|
thumbImage = imaging.Fit(thumbImage, self.size, self.size, imaging.Lanczos)
|
|
|
|
thumbImage = imaging.PasteCenter(imaging.New(self.size, self.size, self.color), thumbImage)
|
|
|
|
default:
|
|
|
|
return nil, errors.New("unsupported thumbnailing style")
|
|
|
|
}
|
|
|
|
|
|
|
|
var thumbBuff bytes.Buffer
|
|
|
|
if err := imaging.Encode(&thumbBuff, thumbImage, thumbFormat); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return context.CreateFileFromReader(thumbPath, &thumbBuff)
|
|
|
|
}
|