-
Notifications
You must be signed in to change notification settings - Fork 15
Adding format=mp4 parameter to convert gif files into mp4 (reduces file size) #30
base: master
Are you sure you want to change the base?
Changes from 6 commits
e7649e7
f4a426b
ce47ab6
9c39f3b
2048d57
ee143ea
ed21075
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 && \ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
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 | ||
} |
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) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
convertFormat string | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
@@ -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.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! | ||
|
@@ -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" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handling the special case from There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. libmagic > exiftool https://github.com/rakyll/magicmime There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
However it throws:
I've found other people with similar problems but without an answer, like https://ffmpeg.org/pipermail/ffmpeg-user/2012-March/005677.html There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
|
||
// 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") | ||
} |
There was a problem hiding this comment.
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.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very interesting catch!