Skip to content
This repository has been archived by the owner on Nov 23, 2018. It is now read-only.

Adding format=mp4 parameter to convert gif files into mp4 (reduces file size) #30

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 30 additions & 22 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 && \
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ADD command was downloading the sources each time docker build was called. This RUN operation does the same and it's cached by its docker layer, no need to download it each time.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very interesting catch!

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 && \
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the fact that this is actually a debian machine to download the ffmpeg package for debian.

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
Expand Down
51 changes: 51 additions & 0 deletions ffmpeg/ffmpeg.go
Original file line number Diff line number Diff line change
@@ -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
}
14 changes: 14 additions & 0 deletions ffmpeg/ffmpeg_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
51 changes: 41 additions & 10 deletions imagick/imagick.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/gographics/imagick/imagick"
"github.com/pressly/imgry"
"github.com/pressly/imgry/ffmpeg"
)

var (
Expand Down Expand Up @@ -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
convertFormat string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is convertFormat used for...? perhaps call this, the destFormat ...? or outFormat .. ? endFormat ..?

}

func (i *Image) Data() []byte {
Expand All @@ -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
}

Expand Down Expand Up @@ -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.convertFormat = strings.ToLower(sz.Format)

switch i.convertFormat {
case "jpeg", "jpg", "gif", "png":
// allow whitelisted format.
if err := i.mw.SetFormat(i.convertFormat); 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!
Expand Down Expand Up @@ -345,6 +368,17 @@ func (i *Image) sizeFrames(sz *imgry.Sizing) error {
}

func (i *Image) WriteToFile(fn string) error {
if i.convertFormat != i.format {
if i.convertFormat == "mp4" {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an special case, we may be able to support different destination files, but for now we only expect mp4. This is only for writing the file to disk.

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
}
Expand All @@ -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
}
24 changes: 24 additions & 0 deletions imagick/imagick_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
1 change: 1 addition & 0 deletions server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

var (
MimeTypes = map[string]string{
"mp4": "video/mp4",
"png": "image/png",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
Expand Down
61 changes: 58 additions & 3 deletions server/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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..?
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the format is already mp4, stop here.

// No need to convert again.
return nil
}

im.Format = im.img.Format()
im.Data = im.img.Data()

if im.conversionFormat == "mp4" && im.Format == "gif" {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handling the special case from gif to mp4. We currently don't have a way to convert our GIF data stream into a MP4 data stream, so we need to output to disk both files. We may be able to pipe the GIF bytes directly into ffmpeg, the docs say it would be something like: ffmpeg -f image2pipe - -y output.mp4, but that command does not work right away, some parameter is missing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, here is a function where we call out to exiftool and pass data to it via stdin -- https://gist.github.com/pkieltyka/5f565ee8510c4091f381

perhaps this will help you figure out how to pipe gif data to ffmpeg

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of doing something like that, but the problem is that ffmpeg does not recognize the input stream, according to the docs, this should work:

cat issue-8.500.gif | ffmpeg -f image2pipe -i - -y foo.mp4

However it throws:

...
[image2pipe @ 0x2fa33e0] Could not find codec parameters for stream 0 (Video: none, none): unknown codec
Consider increasing the value for the 'analyzeduration' and 'probesize' options
pipe:: could not find codec parameters
...

I've found other people with similar problems but without an answer, like https://ffmpeg.org/pipermail/ffmpeg-user/2012-March/005677.html

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah. well.. lets just write to disk like you’re doing

On Wednesday, December 16, 2015 at 10:28 AM, José Carlos wrote:

In server/image.go (#30 (comment)):

im.Format = im.img.Format() > im.Data = im.img.Data() > + > + if im.conversionFormat == "mp4" && im.Format == "gif" {
I was thinking of doing something like that, but the problem is that ffmpeg does not recognize the input stream, according to the docs, this should work:
cat issue-8.500.gif | ffmpeg -f image2pipe -i - -y foo.mp4
However it throws:
... [image2pipe @ 0x2fa33e0] Could not find codec parameters for stream 0 (Video: none, none): unknown codec Consider increasing the value for the 'analyzeduration' and 'probesize' options pipe:: could not find codec parameters ...
I've found other people with similar problems but without an answer, like https://ffmpeg.org/pipermail/ffmpeg-user/2012-March/005677.html


Reply to this email directly or view it on GitHub (https://github.com/pressly/imgry/pull/30/files#r47789294).

// 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")
}