diff --git a/Dockerfile b/Dockerfile index a816a1c..2ff7b87 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,32 +5,40 @@ RUN apt-get update && apt-get install --no-install-recommends -y build-essential zlib1g-dev pkg-config # Install libturbo-jpeg 1.4.2 -ADD http://sourceforge.net/projects/libjpeg-turbo/files/1.4.2/libjpeg-turbo-official_1.4.2_amd64.deb/download /tmp/libjpeg-turbo-official_1.4.2_amd64.deb -RUN cd /tmp && dpkg -i /tmp/libjpeg-turbo-official_1.4.2_amd64.deb && \ - echo /opt/libjpeg-turbo/lib64 > /etc/ld.so.conf.d/libjpeg-turbo.conf && ldconfig +RUN wget -q https://sourceforge.net/projects/libjpeg-turbo/files/1.4.2/libjpeg-turbo-official_1.4.2_amd64.deb/download -O /tmp/libjpeg-turbo-official_1.4.2_amd64.deb && \ + cd /tmp && dpkg -i /tmp/libjpeg-turbo-official_1.4.2_amd64.deb && \ + echo /opt/libjpeg-turbo/lib64 > /etc/ld.so.conf.d/libjpeg-turbo.conf && ldconfig # Install libpng 1.6.19 -ADD http://downloads.sourceforge.net/project/libpng/libpng16/1.6.19/libpng-1.6.19.tar.gz /tmp/ -RUN cd /tmp && tar -zxvf libpng-1.6.19.tar.gz && cd libpng-1.6.19 && \ +RUN wget -q https://downloads.sourceforge.net/project/libpng/libpng16/1.6.19/libpng-1.6.19.tar.gz -O /tmp/libpng-1.6.19.tar.gz && \ + cd /tmp && tar -zxvf libpng-1.6.19.tar.gz && cd libpng-1.6.19 && \ ./configure --prefix=/usr && make && make install && ldconfig -ADD http://www.imagemagick.org/download/ImageMagick-6.9.2-8.tar.xz /tmp/ -RUN cd /tmp && tar -xvf ImageMagick-6.9.2-8.tar.xz && cd ImageMagick-6.9.2-8 && \ - ./configure --prefix=/usr \ - --enable-shared \ - --disable-openmp \ - --disable-opencl \ - --without-x \ - --with-quantum-depth=8 \ - --with-magick-plus-plus=no \ - --with-jpeg=yes \ - --with-png=yes \ - --with-jp2=yes \ - LIBS="-ljpeg -lturbojpeg" \ - LDFLAGS="-L/opt/libjpeg-turbo/lib64" \ - CFLAGS="-I/opt/libjpeg-turbo/include" \ - CPPFLAGS="-I/opt/libjpeg-turbo/include" \ - && make && make install && ldconfig +RUN wget -q http://www.imagemagick.org/download/ImageMagick-6.9.2-8.tar.xz -O /tmp/ImageMagick-6.9.2-8.tar.xz && \ + cd /tmp && tar -xvf ImageMagick-6.9.2-8.tar.xz && cd ImageMagick-6.9.2-8 && \ + ./configure --prefix=/usr \ + --enable-shared \ + --disable-openmp \ + --disable-opencl \ + --without-x \ + --with-quantum-depth=8 \ + --with-magick-plus-plus=no \ + --with-jpeg=yes \ + --with-png=yes \ + --with-jp2=yes \ + LIBS="-ljpeg -lturbojpeg" \ + LDFLAGS="-L/opt/libjpeg-turbo/lib64" \ + CFLAGS="-I/opt/libjpeg-turbo/include" \ + CPPFLAGS="-I/opt/libjpeg-turbo/include" \ + && make && make install && ldconfig + +# Install ffmpeg +RUN echo "deb http://www.deb-multimedia.org jessie main non-free" >> /etc/apt/sources.list && \ + echo "deb-src http://www.deb-multimedia.org jessie main non-free" >> /etc/apt/sources.list && \ + wget -q https://www.deb-multimedia.org/pool/main/d/deb-multimedia-keyring/deb-multimedia-keyring_2015.6.1_all.deb -O /tmp/deb-multimedia-keyring_2015.6.1_all.deb && \ + dpkg -i /tmp/deb-multimedia-keyring_2015.6.1_all.deb && \ + apt-get update && \ + apt-get install -y ffmpeg # Imgry ADD . /go/src/github.com/pressly/imgry diff --git a/ffmpeg/ffmpeg.go b/ffmpeg/ffmpeg.go new file mode 100644 index 0000000..d2698dc --- /dev/null +++ b/ffmpeg/ffmpeg.go @@ -0,0 +1,51 @@ +package ffmpeg + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path" + "path/filepath" +) + +// Convert wraps the ffmpeg command and can be used to convert media files. +// ffmpeg -i src -y dst +func Convert(src string, dst string) (err error) { + if src, err = filepath.Abs(src); err != nil { + return + } + + if dst, err = filepath.Abs(dst); err != nil { + return + } + + if _, err = os.Stat(src); err != nil { + return fmt.Errorf("Failed to open file %s: %q", src, err) + } + + if _, err = os.Stat(path.Dir(dst)); err != nil { + return fmt.Errorf("No such directory %s: %q", path.Dir(src), err) + } + + cmd := exec.Command("ffmpeg", "-i", src, "-y", dst) + + var stderr io.ReadCloser + if stderr, err = cmd.StderrPipe(); err != nil { + return + } + + if err = cmd.Run(); err != nil { + return + } + + errmsg, _ := ioutil.ReadAll(stderr) + errstr := string(errmsg) + if errstr != "" { + return errors.New(errstr) + } + + return nil +} diff --git a/ffmpeg/ffmpeg_test.go b/ffmpeg/ffmpeg_test.go new file mode 100644 index 0000000..05b8c3e --- /dev/null +++ b/ffmpeg/ffmpeg_test.go @@ -0,0 +1,14 @@ +package ffmpeg + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGIFToVideo(t *testing.T) { + os.Remove("../testdata/issue-8.mp4") + err := Convert("../testdata/issue-8.gif", "../testdata/issue-8.mp4") + assert.NoError(t, err) +} diff --git a/imagick/imagick.go b/imagick/imagick.go index dbccd53..a4e976a 100644 --- a/imagick/imagick.go +++ b/imagick/imagick.go @@ -10,6 +10,7 @@ import ( "github.com/gographics/imagick/imagick" "github.com/pressly/imgry" + "github.com/pressly/imgry/ffmpeg" ) var ( @@ -153,10 +154,11 @@ func (ng Engine) GetImageInfo(b []byte, srcFormat ...string) (*imgry.ImageInfo, type Image struct { mw *imagick.MagickWand - data []byte - width int - height int - format string + data []byte + width int + height int + format string + destFormat string } func (i *Image) Data() []byte { @@ -171,7 +173,17 @@ func (i *Image) Height() int { return i.height } +func (i *Image) guessFormat() { + i.format = strings.ToLower(i.mw.GetImageFormat()) + if i.format == "jpeg" { + i.format = "jpg" + } +} + func (i *Image) Format() string { + if i.format == "" { + i.guessFormat() + } return i.format } @@ -220,16 +232,27 @@ func (i *Image) SizeIt(sz *imgry.Sizing) error { return err } - if sz.Format != "" { - if err := i.mw.SetFormat(sz.Format); err != nil { + i.destFormat = strings.ToLower(sz.Format) + + switch i.destFormat { + case "jpeg", "jpg", "gif", "png": + // allow whitelisted format. + if err := i.mw.SetFormat(i.destFormat); err != nil { return err } + // image has been converted at this point. + i.guessFormat() + case "mp4": + // won't be converted right now. + default: + // ignore unknown destination format. } // progressive jpegs if i.Format() == "jpg" { i.mw.SetInterlaceScheme(imagick.INTERLACE_PLANE) } + // exif and color profiles begone i.mw.StripImage() // compress it! @@ -345,6 +368,17 @@ func (i *Image) sizeFrames(sz *imgry.Sizing) error { } func (i *Image) WriteToFile(fn string) error { + if i.destFormat != i.format { + if i.destFormat == "mp4" { + tmpout := fn + "." + i.format + err := ioutil.WriteFile(tmpout, i.Data(), 0664) + if err != nil { + return err + } + defer os.Remove(tmpout) + return ffmpeg.Convert(tmpout, fn) + } + } err := ioutil.WriteFile(fn, i.Data(), 0664) return err } @@ -368,10 +402,7 @@ func (i *Image) sync(optFlatten ...bool) error { i.width = int(i.mw.GetImageWidth()) i.height = int(i.mw.GetImageHeight()) - i.format = strings.ToLower(i.mw.GetImageFormat()) - if i.format == "jpeg" { - i.format = "jpg" - } + i.guessFormat() return nil } diff --git a/imagick/imagick_test.go b/imagick/imagick_test.go index f635120..5eb844d 100644 --- a/imagick/imagick_test.go +++ b/imagick/imagick_test.go @@ -459,3 +459,27 @@ func TestIssue25(t *testing.T) { img.Release() } + +func TestGIFToVideo(t *testing.T) { + var sz *imgry.Sizing + var img imgry.Image + var err error + + ng := Engine{} + + // Resizing to 750, which is slightly smaller. + img, err = ng.LoadFile("../testdata/issue-8.gif") + assert.NoError(t, err) + + sz, _ = imgry.NewSizingFromQuery("format=mp4&size=750x") + err = img.SizeIt(sz) + assert.NoError(t, err) + + assert.Equal(t, 750, img.Width()) + assert.Equal(t, 422, img.Height()) + + err = img.WriteToFile("../testdata/issue-8.700.mp4") + assert.NoError(t, err) + + img.Release() +} diff --git a/server/handlers.go b/server/handlers.go index 28b344d..4e28713 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -17,6 +17,7 @@ import ( var ( MimeTypes = map[string]string{ + "mp4": "video/mp4", "png": "image/png", "jpeg": "image/jpeg", "jpg": "image/jpeg", diff --git a/server/image.go b/server/image.go index e5d13d0..6ae4657 100644 --- a/server/image.go +++ b/server/image.go @@ -5,12 +5,16 @@ import ( "encoding/hex" "errors" "fmt" + "io/ioutil" + "os" + "path" "strings" "time" "github.com/goware/go-metrics" "github.com/goware/lg" "github.com/pressly/imgry" + "github.com/pressly/imgry/ffmpeg" "github.com/pressly/imgry/imagick" ) @@ -33,7 +37,8 @@ type Image struct { Sizing *imgry.Sizing `json:"-" redis:"-"` Data []byte `json:"-" redis:"-"` - img imgry.Image + img imgry.Image + conversionFormat string } // Hrmm.. how will we generate a Uid if we just have a blob and no srcurl..? @@ -127,7 +132,11 @@ func (im *Image) SizeIt(sizing *imgry.Sizing) error { return fmt.Errorf("Error occurred when sizing an image: %s", err) } - im.sync() + im.conversionFormat = sizing.Format + + if err = im.sync(); err != nil { + return fmt.Errorf("Error occurred when syncing the image: %s", err) + } return nil } @@ -189,9 +198,55 @@ func (im *Image) Release() { } } -func (im *Image) sync() { +func (im *Image) sync() error { im.Width = im.img.Width() im.Height = im.img.Height() + + if im.Format == "mp4" && len(im.Data) != 0 { + // No need to convert again. + return nil + } + im.Format = im.img.Format() im.Data = im.img.Data() + + if im.conversionFormat == "mp4" && im.Format == "gif" { + // I still can't find how to pipe images to ffmpeg's stdin, so for now we + // need to output them to disk. + outputGIF := tmpFile(im.Key + ".gif") // Better if mounted on tmpfs. + outputMP4 := tmpFile(im.Key + ".mp4") + + if err := ioutil.WriteFile(outputGIF, im.img.Data(), 0664); err != nil { + return err + } + defer os.RemoveAll(path.Dir(outputGIF)) + + if err := ffmpeg.Convert(outputGIF, outputMP4); err != nil { + return err + } + defer os.RemoveAll(path.Dir(outputMP4)) + + buf, err := ioutil.ReadFile(outputMP4) + if err != nil { + return err + } + + // I'm going to take over the data buffer. + im.Data = buf + im.Format = "mp4" + } + + return nil +} + +func tmpFile(name string) string { + for { + dirname, _ := ioutil.TempDir("", "tmp-") + file := dirname + "/" + name + _, err := os.Stat(file) + if !os.IsExist(err) { + return file + } + } + panic("reached") }