major refactor - add png(noncrush),bmp thumbnail output
This commit is contained in:
parent
ebb5f3cc7a
commit
525dcf31ad
@ -1,6 +1,7 @@
|
||||
package thumbnail
|
||||
|
||||
import (
|
||||
"image"
|
||||
"mime"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@ -29,14 +30,29 @@ func NewThumbnailerEx(opts *Config) Thumbnailer {
|
||||
func (this *DirectThumbnailer) RenderFile(absPath string) ([]byte, error) {
|
||||
mimeType := mime.TypeByExtension(filepath.Ext(absPath))
|
||||
|
||||
// Decode source
|
||||
|
||||
var src image.Image
|
||||
var err error = nil
|
||||
if strings.HasPrefix(mimeType, `image/`) {
|
||||
return this.renderImageFile(absPath, mimeType)
|
||||
src, err = this.imageSnapshot(absPath, mimeType)
|
||||
|
||||
} else if strings.HasPrefix(mimeType, `video/`) {
|
||||
return this.renderScaledFfmpeg(absPath)
|
||||
src, err = this.videoSnapshot(absPath)
|
||||
|
||||
} else {
|
||||
return nil, ErrUnsupportedFiletype
|
||||
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Scale
|
||||
|
||||
dest := this.scaleImage(src)
|
||||
|
||||
// Rasterise result
|
||||
|
||||
return this.encode(dest)
|
||||
}
|
||||
|
25
config.go
25
config.go
@ -5,19 +5,16 @@ type AspectFormat uint8
|
||||
type ScaleFormat uint8
|
||||
|
||||
const (
|
||||
OUTPUT_PNG_CRUSH OutputFormat = 2
|
||||
OUTPUT_JPG OutputFormat = 3
|
||||
OUTPUT_WEBP OutputFormat = 4
|
||||
OUTPUT__DEFAULT = OUTPUT_PNG_CRUSH
|
||||
Png OutputFormat = 1
|
||||
PngCrush OutputFormat = 2
|
||||
Jpeg OutputFormat = 3
|
||||
Bmp OutputFormat = 4
|
||||
|
||||
ASPECT_PAD_TO_DIMENSIONS AspectFormat = 80
|
||||
ASPECT_RESPECT_MAX_DIMENSION_ONLY AspectFormat = 81
|
||||
ASPECT_CROP_TO_DIMENSIONS AspectFormat = 82
|
||||
ASPECT__DEFAULT = ASPECT_PAD_TO_DIMENSIONS
|
||||
FitOutside AspectFormat = 80 // Pad out with black bars to dimensions
|
||||
FitInside AspectFormat = 82 // Crop to dimensions
|
||||
|
||||
SCALEFMT_NN ScaleFormat = 120
|
||||
SCALEFMT_BILINEAR ScaleFormat = 121
|
||||
SCALEFMT__DEFAULT = SCALEFMT_BILINEAR
|
||||
NearestNeighbour ScaleFormat = 120
|
||||
Bilinear ScaleFormat = 121
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@ -31,7 +28,7 @@ type Config struct {
|
||||
var DefaultConfig = Config{
|
||||
Width: 128,
|
||||
Height: 128,
|
||||
Output: OUTPUT__DEFAULT,
|
||||
Aspect: ASPECT__DEFAULT,
|
||||
Scale: SCALEFMT__DEFAULT,
|
||||
Output: Jpeg,
|
||||
Aspect: FitInside,
|
||||
Scale: Bilinear,
|
||||
}
|
||||
|
43
decodeImage.go
Normal file
43
decodeImage.go
Normal file
@ -0,0 +1,43 @@
|
||||
package thumbnail
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/image/bmp"
|
||||
"golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
type imageDecoder func(io.Reader) (image.Image, error)
|
||||
|
||||
var imageDecoders map[string]imageDecoder = map[string]imageDecoder{
|
||||
`image/jpeg`: jpeg.Decode,
|
||||
`image/png`: png.Decode,
|
||||
`image/gif`: gif.Decode,
|
||||
`image/webp`: webp.Decode,
|
||||
`image/bmp`: bmp.Decode,
|
||||
}
|
||||
|
||||
func (this *DirectThumbnailer) imageSnapshot(absPath, mimeType string) (image.Image, error) {
|
||||
|
||||
// Load file from path
|
||||
|
||||
fh, err := os.OpenFile(absPath, os.O_RDONLY, 0400)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
// Decode image
|
||||
|
||||
fn, ok := imageDecoders[mimeType]
|
||||
if !ok {
|
||||
return nil, ErrUnsupportedFiletype
|
||||
}
|
||||
|
||||
return fn(fh)
|
||||
}
|
52
decodeVideo.go
Normal file
52
decodeVideo.go
Normal file
@ -0,0 +1,52 @@
|
||||
package thumbnail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/png"
|
||||
"io"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func (this *DirectThumbnailer) videoSnapshot(absPath string) (image.Image, error) {
|
||||
|
||||
cmd := exec.Command(
|
||||
"ffmpeg",
|
||||
"-loglevel", "0",
|
||||
"-timelimit", "10", // seconds
|
||||
"-an",
|
||||
"-i", absPath,
|
||||
"-vf", `thumbnail`,
|
||||
"-frames:v", "1",
|
||||
"-f", "image2pipe",
|
||||
"-c:v", "png", // always PNG output - we will resample/rescale it ourselves
|
||||
"-",
|
||||
)
|
||||
|
||||
// -ss 00:00:30
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := bytes.Buffer{}
|
||||
_, err = io.Copy(&out, stdout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Try to decode as PNG image
|
||||
|
||||
return png.Decode(bytes.NewReader(out.Bytes()))
|
||||
}
|
@ -3,48 +3,13 @@ package thumbnail
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/image/bmp"
|
||||
"golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
type imageDecoder func(io.Reader) (image.Image, error)
|
||||
|
||||
var imageDecoders map[string]imageDecoder = map[string]imageDecoder{
|
||||
`image/jpeg`: jpeg.Decode,
|
||||
`image/png`: png.Decode,
|
||||
`image/gif`: gif.Decode,
|
||||
`image/webp`: webp.Decode,
|
||||
`image/bmp`: bmp.Decode,
|
||||
}
|
||||
|
||||
func (this *DirectThumbnailer) renderImageFile(absPath, mimeType string) ([]byte, error) {
|
||||
|
||||
fh, err := os.OpenFile(absPath, os.O_RDONLY, 0400)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
fn, ok := imageDecoders[mimeType]
|
||||
if !ok {
|
||||
return nil, ErrUnsupportedFiletype
|
||||
}
|
||||
|
||||
src, err := fn(fh)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return this.RenderScaledImage(src)
|
||||
}
|
||||
|
||||
func (this *DirectThumbnailer) RenderScaledImage(src image.Image) ([]byte, error) {
|
||||
func (this *DirectThumbnailer) scaleImage(src image.Image) image.Image {
|
||||
srcW := src.Bounds().Max.X
|
||||
srcH := src.Bounds().Max.Y
|
||||
destW := 0
|
||||
@ -68,7 +33,7 @@ func (this *DirectThumbnailer) RenderScaledImage(src image.Image) ([]byte, error
|
||||
|
||||
switch this.cfg.Scale {
|
||||
|
||||
case SCALEFMT_BILINEAR:
|
||||
case Bilinear:
|
||||
|
||||
for y := 0; y < destH; y += 1 {
|
||||
for x := 0; x < destW; x += 1 {
|
||||
@ -82,7 +47,7 @@ func (this *DirectThumbnailer) RenderScaledImage(src image.Image) ([]byte, error
|
||||
}
|
||||
}
|
||||
|
||||
case SCALEFMT_NN:
|
||||
case NearestNeighbour:
|
||||
|
||||
for y := 0; y < destH; y += 1 {
|
||||
for x := 0; x < destW; x += 1 {
|
||||
@ -94,11 +59,24 @@ func (this *DirectThumbnailer) RenderScaledImage(src image.Image) ([]byte, error
|
||||
|
||||
}
|
||||
|
||||
return dest
|
||||
}
|
||||
|
||||
func (this *DirectThumbnailer) encode(dest image.Image) ([]byte, error) {
|
||||
|
||||
switch this.cfg.Output {
|
||||
case OUTPUT_PNG_CRUSH:
|
||||
case Png:
|
||||
buff := bytes.Buffer{}
|
||||
err := png.Encode(&buff, dest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buff.Bytes(), nil
|
||||
|
||||
case PngCrush:
|
||||
return crushFast(dest)
|
||||
|
||||
case OUTPUT_JPG:
|
||||
case Jpeg:
|
||||
buff := bytes.Buffer{}
|
||||
err := jpeg.Encode(&buff, dest, &jpeg.Options{Quality: jpeg.DefaultQuality})
|
||||
if err != nil {
|
||||
@ -107,6 +85,14 @@ func (this *DirectThumbnailer) RenderScaledImage(src image.Image) ([]byte, error
|
||||
|
||||
return buff.Bytes(), nil
|
||||
|
||||
case Bmp:
|
||||
buff := bytes.Buffer{}
|
||||
err := bmp.Encode(&buff, dest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buff.Bytes(), nil
|
||||
|
||||
default:
|
||||
return nil, ErrInvalidOption
|
||||
|
67
video.go
67
video.go
@ -1,67 +0,0 @@
|
||||
package thumbnail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func (this *DirectThumbnailer) renderScaledFfmpeg(absPath string) ([]byte, error) {
|
||||
|
||||
scaleCmd := fmt.Sprintf(
|
||||
`thumbnail,scale='if(gt(a,%d/%d),%d,-1)':'if(gt(a,%d/%d),-1,%d)'`,
|
||||
this.cfg.Height, this.cfg.Width, this.cfg.Height,
|
||||
this.cfg.Height, this.cfg.Width, this.cfg.Width,
|
||||
)
|
||||
|
||||
// BUG(): always produces ASPECT_SVELTE shape - need to re-pad for ASPECT_PADDED
|
||||
|
||||
var vcodec string
|
||||
switch this.cfg.Output {
|
||||
case OUTPUT_JPG:
|
||||
vcodec = "mjpeg" // yes really
|
||||
case OUTPUT_PNG_CRUSH:
|
||||
vcodec = "png"
|
||||
default:
|
||||
return nil, ErrInvalidOption
|
||||
}
|
||||
|
||||
cmd := exec.Command(
|
||||
"ffmpeg",
|
||||
"-loglevel", "0",
|
||||
"-timelimit", "10", // seconds
|
||||
"-an",
|
||||
"-i", absPath,
|
||||
"-vf", scaleCmd,
|
||||
"-frames:v", "1",
|
||||
"-f", "image2pipe",
|
||||
"-c:v", vcodec,
|
||||
"-",
|
||||
)
|
||||
|
||||
// -ss 00:00:30
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := bytes.Buffer{}
|
||||
_, err = io.Copy(&out, stdout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return out.Bytes(), nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user