major refactor - add png(noncrush),bmp thumbnail output
This commit is contained in:
parent
ebb5f3cc7a
commit
525dcf31ad
@ -1,6 +1,7 @@
|
|||||||
package thumbnail
|
package thumbnail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"image"
|
||||||
"mime"
|
"mime"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@ -29,14 +30,29 @@ func NewThumbnailerEx(opts *Config) Thumbnailer {
|
|||||||
func (this *DirectThumbnailer) RenderFile(absPath string) ([]byte, error) {
|
func (this *DirectThumbnailer) RenderFile(absPath string) ([]byte, error) {
|
||||||
mimeType := mime.TypeByExtension(filepath.Ext(absPath))
|
mimeType := mime.TypeByExtension(filepath.Ext(absPath))
|
||||||
|
|
||||||
|
// Decode source
|
||||||
|
|
||||||
|
var src image.Image
|
||||||
|
var err error = nil
|
||||||
if strings.HasPrefix(mimeType, `image/`) {
|
if strings.HasPrefix(mimeType, `image/`) {
|
||||||
return this.renderImageFile(absPath, mimeType)
|
src, err = this.imageSnapshot(absPath, mimeType)
|
||||||
|
|
||||||
} else if strings.HasPrefix(mimeType, `video/`) {
|
} else if strings.HasPrefix(mimeType, `video/`) {
|
||||||
return this.renderScaledFfmpeg(absPath)
|
src, err = this.videoSnapshot(absPath)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return nil, ErrUnsupportedFiletype
|
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
|
type ScaleFormat uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
OUTPUT_PNG_CRUSH OutputFormat = 2
|
Png OutputFormat = 1
|
||||||
OUTPUT_JPG OutputFormat = 3
|
PngCrush OutputFormat = 2
|
||||||
OUTPUT_WEBP OutputFormat = 4
|
Jpeg OutputFormat = 3
|
||||||
OUTPUT__DEFAULT = OUTPUT_PNG_CRUSH
|
Bmp OutputFormat = 4
|
||||||
|
|
||||||
ASPECT_PAD_TO_DIMENSIONS AspectFormat = 80
|
FitOutside AspectFormat = 80 // Pad out with black bars to dimensions
|
||||||
ASPECT_RESPECT_MAX_DIMENSION_ONLY AspectFormat = 81
|
FitInside AspectFormat = 82 // Crop to dimensions
|
||||||
ASPECT_CROP_TO_DIMENSIONS AspectFormat = 82
|
|
||||||
ASPECT__DEFAULT = ASPECT_PAD_TO_DIMENSIONS
|
|
||||||
|
|
||||||
SCALEFMT_NN ScaleFormat = 120
|
NearestNeighbour ScaleFormat = 120
|
||||||
SCALEFMT_BILINEAR ScaleFormat = 121
|
Bilinear ScaleFormat = 121
|
||||||
SCALEFMT__DEFAULT = SCALEFMT_BILINEAR
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@ -31,7 +28,7 @@ type Config struct {
|
|||||||
var DefaultConfig = Config{
|
var DefaultConfig = Config{
|
||||||
Width: 128,
|
Width: 128,
|
||||||
Height: 128,
|
Height: 128,
|
||||||
Output: OUTPUT__DEFAULT,
|
Output: Jpeg,
|
||||||
Aspect: ASPECT__DEFAULT,
|
Aspect: FitInside,
|
||||||
Scale: SCALEFMT__DEFAULT,
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"image"
|
"image"
|
||||||
"image/gif"
|
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
"image/png"
|
"image/png"
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"golang.org/x/image/bmp"
|
"golang.org/x/image/bmp"
|
||||||
"golang.org/x/image/webp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type imageDecoder func(io.Reader) (image.Image, error)
|
func (this *DirectThumbnailer) scaleImage(src image.Image) image.Image {
|
||||||
|
|
||||||
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) {
|
|
||||||
srcW := src.Bounds().Max.X
|
srcW := src.Bounds().Max.X
|
||||||
srcH := src.Bounds().Max.Y
|
srcH := src.Bounds().Max.Y
|
||||||
destW := 0
|
destW := 0
|
||||||
@ -68,7 +33,7 @@ func (this *DirectThumbnailer) RenderScaledImage(src image.Image) ([]byte, error
|
|||||||
|
|
||||||
switch this.cfg.Scale {
|
switch this.cfg.Scale {
|
||||||
|
|
||||||
case SCALEFMT_BILINEAR:
|
case Bilinear:
|
||||||
|
|
||||||
for y := 0; y < destH; y += 1 {
|
for y := 0; y < destH; y += 1 {
|
||||||
for x := 0; x < destW; x += 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 y := 0; y < destH; y += 1 {
|
||||||
for x := 0; x < destW; x += 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 {
|
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)
|
return crushFast(dest)
|
||||||
|
|
||||||
case OUTPUT_JPG:
|
case Jpeg:
|
||||||
buff := bytes.Buffer{}
|
buff := bytes.Buffer{}
|
||||||
err := jpeg.Encode(&buff, dest, &jpeg.Options{Quality: jpeg.DefaultQuality})
|
err := jpeg.Encode(&buff, dest, &jpeg.Options{Quality: jpeg.DefaultQuality})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -107,6 +85,14 @@ func (this *DirectThumbnailer) RenderScaledImage(src image.Image) ([]byte, error
|
|||||||
|
|
||||||
return buff.Bytes(), nil
|
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:
|
default:
|
||||||
return nil, ErrInvalidOption
|
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