major refactor - add png(noncrush),bmp thumbnail output

This commit is contained in:
mappu 2018-06-09 16:39:41 +12:00
parent ebb5f3cc7a
commit 525dcf31ad
6 changed files with 150 additions and 123 deletions

View File

@ -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)
} }

View File

@ -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
View 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
View 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()))
}

View File

@ -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

View File

@ -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
}