Procházet zdrojové kódy

GIF output support

DarthSim před 6 roky
rodič
revize
2c0b538eb5

+ 66 - 7
Dockerfile

@@ -5,24 +5,83 @@ ENV GOPATH /go
 ENV PATH /usr/local/go/bin:$PATH
 
 ADD . /go/src/github.com/DarthSim/imgproxy
+WORKDIR /go/src/github.com/DarthSim/imgproxy
 
+# Install dependencies
 RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories \
   && apk --no-cache upgrade \
-  && apk add --no-cache --virtual .build-deps go gcc musl-dev fftw-dev vips-dev \
-  && cd /go/src/github.com/DarthSim/imgproxy \
-  && CGO_LDFLAGS_ALLOW="-s|-w" go build -v -o /usr/local/bin/imgproxy \
-  && apk del --purge .build-deps \
-  && rm -rf /var/cache/apk*
+  && apk add --no-cache curl ca-certificates go gcc g++ make musl-dev fftw-dev glib-dev expat-dev \
+    libjpeg-turbo-dev libpng-dev libwebp-dev giflib-dev libexif-dev lcms2-dev
+
+# Build ImageMagick
+RUN cd /root \
+  && mkdir ImageMagick \
+  && curl -Ls https://imagemagick.org/download/ImageMagick.tar.gz | tar -xz -C ImageMagick --strip-components 1 \
+  && cd ImageMagick \
+  && ./configure \
+    --enable-silent-rules \
+    --disable-static \
+    --disable-openmp \
+    --disable-deprecated \
+    --disable-docs \
+    --with-threads \
+    --without-magick-plus-plus \
+    --without-utilities \
+    --without-perl \
+    --without-bzlib \
+    --without-dps \
+    --without-freetype \
+    --without-jbig \
+    --without-jpeg \
+    --without-lcms \
+    --without-lzma \
+    --without-png \
+    --without-tiff \
+    --without-wmf \
+    --without-xml \
+    --without-webp \
+  && make install-strip
+
+# Build libvips
+RUN cd /root \
+  && export VIPS_VERSION=$(curl -s "https://api.github.com/repos/libvips/libvips/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') \
+  && echo "Vips version: $VIPS_VERSION" \
+  && curl -Ls https://github.com/libvips/libvips/releases/download/v$VIPS_VERSION/vips-$VIPS_VERSION.tar.gz | tar -xz \
+  && cd vips-$VIPS_VERSION \
+  && ./configure \
+    --disable-magickload \
+    --without-python \
+    --without-tiff \
+    --without-orc \
+    --without-OpenEXR \
+    --enable-debug=no \
+    --disable-static \
+    --enable-silent-rules \
+  && make install-strip
+
+# Build imgproxy
+RUN cd /go/src/github.com/DarthSim/imgproxy \
+  && CGO_LDFLAGS_ALLOW="-s|-w" go build -v -o /usr/local/bin/imgproxy
+
+# Copy compiled libs here to copy them to the final image
+RUN cd /root \
+  && mkdir libs \
+  && ldd /usr/local/bin/imgproxy | grep /usr/local/lib/ | awk '{print $3}' | xargs -I '{}' cp '{}' libs/
+
+# ==================================================================================================
+# Final image
 
 FROM alpine:edge
 LABEL maintainer="Sergey Alexandrovich <darthsim@gmail.com>"
 
 RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories \
   && apk --no-cache upgrade \
-  && apk add --no-cache ca-certificates bash vips \
+  && apk add --no-cache bash ca-certificates fftw glib expat libjpeg-turbo libpng \
+    libwebp giflib libexif lcms2 \
   && rm -rf /var/cache/apk*
 
-COPY --from=0 /usr/local/bin/imgproxy /usr/local/bin
+COPY --from=0 /usr/local/bin/imgproxy /usr/local/bin/
+COPY --from=0 /root/libs/* /usr/local/lib/
 
 CMD ["imgproxy"]
 

+ 9 - 0
Gopkg.lock

@@ -274,6 +274,14 @@
   pruneopts = "UT"
   revision = "9dcd33a902f40452422c2367fefcb95b54f9f8f8"
 
+[[projects]]
+  branch = "master"
+  digest = "1:b521f10a2d8fa85c04a8ef4e62f2d1e14d303599a55d64dabf9f5a02f84d35eb"
+  name = "golang.org/x/sync"
+  packages = ["errgroup"]
+  pruneopts = "UT"
+  revision = "42b317875d0fa942474b76e1b46a6060d720ae6e"
+
 [[projects]]
   branch = "master"
   digest = "1:fbacfb57e3d052810813bca2d48c22fbde916ecfc4a2bac08df30ed6e9e59759"
@@ -408,6 +416,7 @@
     "github.com/stretchr/testify/suite",
     "golang.org/x/image/webp",
     "golang.org/x/net/netutil",
+    "golang.org/x/sync/errgroup",
     "google.golang.org/api/option",
   ]
   solver-name = "gps-cdcl"

+ 1 - 1
README.md

@@ -79,7 +79,7 @@ Massive processing of remote images is a potentially dangerous thing, security-w
 9. [Serving files from Google Cloud Storage](./docs/serving_files_from_google_cloud_storage.md)
 10. [New Relic](./docs/new_relic.md)
 11. [Prometheus](./docs/prometheus.md)
-12. [Source image formats support](./docs/source_image_formats_support.md)
+12. [Image formats support](./docs/image_formats_support.md)
 13. [About processing pipeline](./docs/about_processing_pipeline.md)
 14. [Health check](./docs/healthcheck.md)
 

+ 3 - 1
docs/generating_the_url_advanced.md

@@ -271,7 +271,9 @@ When using encoded source URL, you can specify the [extension](#extension) after
 
 #### Extension
 
-Extension specifies the format of the resulting image. At the moment, imgproxy supports only `jpg`, `png` and `webp`, them being the most popular and useful image formats on the Web.
+Extension specifies the format of the resulting image. At the moment, imgproxy supports only `jpg`, `png`, `webp`, and `gif`, them being the most popular and useful image formats on the Web.
+
+**Note:** Read about GIF support [here](./image_formats_support.md#gif-support).
 
 The extension part can be omitted. In this case, if the format is not defined by processing options, imgproxy will use `jpg` by default. You can also [enable WebP support detection](./configuration.md#webp-support-detection) to use it as default resulting format when possible.
 

+ 3 - 1
docs/generating_the_url_basic.md

@@ -87,7 +87,9 @@ When using encoded source URL, you can specify the [extension](#extension) after
 
 #### Extension
 
-Extension specifies the format of the resulting image. At the moment, imgproxy supports only `jpg`, `png` and `webp`, them being the most popular and useful image formats on the Web.
+Extension specifies the format of the resulting image. At the moment, imgproxy supports only `jpg`, `png`, `webp`, and `gif`, them being the most popular and useful image formats on the Web.
+
+**Note:** Read about GIF support [here](./image_formats_support.md#gif-support).
 
 The extension part can be omitted. In this case, imgproxy will use `jpg` by default. You also can [enable WebP support detection](./configuration.md#webp-support-detection) to use it as default resulting format when possible.
 

+ 12 - 0
docs/image_formats_support.md

@@ -0,0 +1,12 @@
+# Image formats support
+
+At the moment, imgproxy supports only the most popular Web image formats:
+
+* PNG;
+* JPEG;
+* WebP;
+* GIF.
+
+## GIF support
+
+imgproxy supports GIF output only when using libvips 8.7.0+ compiled with ImageMagick support. Official imgproxy Docker image supports GIF out of the box.

+ 0 - 8
docs/source_image_formats_support.md

@@ -1,8 +0,0 @@
-# Source image formats support
-
-At the moment, imgproxy supports only the most popular Web image formats:
-
-* PNG;
-* JPEG;
-* GIF;
-* WebP.

+ 18 - 8
download.go

@@ -63,23 +63,33 @@ func initDownloading() {
 	}
 }
 
+func checkDimensions(width, height int) error {
+	if width > conf.MaxSrcDimension || height > conf.MaxSrcDimension {
+		return errSourceDimensionsTooBig
+	}
+
+	if width*height > conf.MaxSrcResolution {
+		return errSourceResolutionTooBig
+	}
+
+	return nil
+}
+
 func checkTypeAndDimensions(r io.Reader) (imageType, error) {
 	imgconf, imgtypeStr, err := image.DecodeConfig(r)
-	imgtype, imgtypeOk := imageTypes[imgtypeStr]
-
 	if err != nil {
 		return imageTypeUnknown, err
 	}
-	if imgconf.Width > conf.MaxSrcDimension || imgconf.Height > conf.MaxSrcDimension {
-		return imageTypeUnknown, errSourceDimensionsTooBig
-	}
-	if imgconf.Width*imgconf.Height > conf.MaxSrcResolution {
-		return imageTypeUnknown, errSourceResolutionTooBig
-	}
+
+	imgtype, imgtypeOk := imageTypes[imgtypeStr]
 	if !imgtypeOk || !vipsTypeSupportLoad[imgtype] {
 		return imageTypeUnknown, errSourceImageTypeNotSupported
 	}
 
+	if err = checkDimensions(imgconf.Width, imgconf.Height); err != nil {
+		return imageTypeUnknown, err
+	}
+
 	return imgtype, nil
 }
 

+ 249 - 73
process.go

@@ -15,6 +15,8 @@ import (
 	"os"
 	"runtime"
 	"unsafe"
+
+	"golang.org/x/sync/errgroup"
 )
 
 var (
@@ -81,6 +83,9 @@ func initVips() {
 	if int(C.vips_type_find_save_go(C.int(imageTypeWEBP))) != 0 {
 		vipsTypeSupportSave[imageTypeWEBP] = true
 	}
+	if int(C.vips_type_find_save_go(C.int(imageTypeGIF))) != 0 {
+		vipsTypeSupportSave[imageTypeGIF] = true
+	}
 
 	if conf.JpegProgressive {
 		cConf.JpegProgressive = C.int(1)
@@ -216,35 +221,10 @@ func calcCrop(width, height int, po *processingOptions) (left, top int) {
 	return
 }
 
-func processImage(ctx context.Context) ([]byte, error) {
-	if newRelicEnabled {
-		newRelicCancel := startNewRelicSegment(ctx, "Processing image")
-		defer newRelicCancel()
-	}
-
-	if prometheusEnabled {
-		defer startPrometheusDuration(prometheusProcessingDuration)()
-	}
-
-	defer C.vips_cleanup()
-
-	data := getImageData(ctx).Bytes()
-	po := getProcessingOptions(ctx)
-	imgtype := getImageType(ctx)
-
-	if po.Gravity.Type == gravitySmart && !vipsSupportSmartcrop {
-		return nil, errSmartCropNotSupported
-	}
-
-	img, err := vipsLoadImage(data, imgtype, 1)
-	if err != nil {
-		return nil, err
-	}
-	defer C.clear_image(&img)
-
-	checkTimeout(ctx)
+func transformImage(ctx context.Context, img **C.struct__VipsImage, data []byte, po *processingOptions, imgtype imageType) error {
+	var err error
 
-	imgWidth, imgHeight, angle, flip := extractMeta(img)
+	imgWidth, imgHeight, angle, flip := extractMeta(*img)
 
 	// Ensure we won't crop out of bounds
 	if !po.Enlarge || po.Resize == resizeCrop {
@@ -257,20 +237,20 @@ func processImage(ctx context.Context) ([]byte, error) {
 		}
 	}
 
-	hasAlpha := vipsImageHasAlpha(img)
+	hasAlpha := vipsImageHasAlpha(*img)
 
 	if needToScale(imgWidth, imgHeight, po) {
 		scale := calcScale(imgWidth, imgHeight, po)
 
 		// Do some shrink-on-load
-		if scale < 1.0 {
+		if scale < 1.0 && data != nil {
 			if shrink := calcShink(scale, imgtype); shrink != 1 {
 				scale = scale * float64(shrink)
 
-				if tmp, e := vipsLoadImage(data, imgtype, shrink); e == nil {
-					C.swap_and_clear(&img, tmp)
+				if tmp, err := vipsLoadImage(data, imgtype, shrink, false); err == nil {
+					C.swap_and_clear(img, tmp)
 				} else {
-					return nil, e
+					return err
 				}
 			}
 		}
@@ -279,50 +259,46 @@ func processImage(ctx context.Context) ([]byte, error) {
 		var bandFormat C.VipsBandFormat
 
 		if hasAlpha {
-			if bandFormat, err = vipsPremultiply(&img); err != nil {
-				return nil, err
+			if bandFormat, err = vipsPremultiply(img); err != nil {
+				return err
 			}
 			premultiplied = true
 		}
 
-		if err = vipsResize(&img, scale); err != nil {
-			return nil, err
+		if err = vipsResize(img, scale); err != nil {
+			return err
 		}
 
 		// Update actual image size after resize
-		imgWidth, imgHeight, _, _ = extractMeta(img)
+		imgWidth, imgHeight, _, _ = extractMeta(*img)
 
 		if premultiplied {
-			if err = vipsUnpremultiply(&img, bandFormat); err != nil {
-				return nil, err
+			if err = vipsUnpremultiply(img, bandFormat); err != nil {
+				return err
 			}
 		}
 	}
 
-	if err = vipsImportColourProfile(&img); err != nil {
-		return nil, err
-	}
-
-	if err = vipsFixColourspace(&img); err != nil {
-		return nil, err
+	if err = vipsImportColourProfile(img); err != nil {
+		return err
 	}
 
 	checkTimeout(ctx)
 
 	if angle != C.VIPS_ANGLE_D0 || flip {
-		if err = vipsImageCopyMemory(&img); err != nil {
-			return nil, err
+		if err = vipsImageCopyMemory(img); err != nil {
+			return err
 		}
 
 		if angle != C.VIPS_ANGLE_D0 {
-			if err = vipsRotate(&img, angle); err != nil {
-				return nil, err
+			if err = vipsRotate(img, angle); err != nil {
+				return err
 			}
 		}
 
 		if flip {
-			if err = vipsFlip(&img); err != nil {
-				return nil, err
+			if err = vipsFlip(img); err != nil {
+				return err
 			}
 		}
 	}
@@ -339,21 +315,21 @@ func processImage(ctx context.Context) ([]byte, error) {
 
 	if po.Width < imgWidth || po.Height < imgHeight {
 		if po.Gravity.Type == gravitySmart {
-			if err = vipsImageCopyMemory(&img); err != nil {
-				return nil, err
+			if err = vipsImageCopyMemory(img); err != nil {
+				return err
 			}
-			if err = vipsSmartCrop(&img, po.Width, po.Height); err != nil {
-				return nil, err
+			if err = vipsSmartCrop(img, po.Width, po.Height); err != nil {
+				return err
 			}
 			// Applying additional modifications after smart crop causes SIGSEGV on Alpine
 			// so we have to copy memory after it
-			if err = vipsImageCopyMemory(&img); err != nil {
-				return nil, err
+			if err = vipsImageCopyMemory(img); err != nil {
+				return err
 			}
 		} else {
 			left, top := calcCrop(imgWidth, imgHeight, po)
-			if err = vipsCrop(&img, left, top, po.Width, po.Height); err != nil {
-				return nil, err
+			if err = vipsCrop(img, left, top, po.Width, po.Height); err != nil {
+				return err
 			}
 		}
 
@@ -361,31 +337,155 @@ func processImage(ctx context.Context) ([]byte, error) {
 	}
 
 	if hasAlpha && po.Flatten {
-		if err = vipsFlatten(&img, po.Background); err != nil {
-			return nil, err
+		if err = vipsFlatten(img, po.Background); err != nil {
+			return err
 		}
 	}
 
 	if po.Blur > 0 {
-		if err = vipsBlur(&img, po.Blur); err != nil {
-			return nil, err
+		if err = vipsBlur(img, po.Blur); err != nil {
+			return err
 		}
 	}
 
 	if po.Sharpen > 0 {
-		if err = vipsSharpen(&img, po.Sharpen); err != nil {
-			return nil, err
+		if err = vipsSharpen(img, po.Sharpen); err != nil {
+			return err
 		}
 	}
 
 	checkTimeout(ctx)
 
 	if po.Watermark.Enabled {
-		if err = vipsApplyWatermark(&img, &po.Watermark); err != nil {
+		if err = vipsApplyWatermark(img, &po.Watermark); err != nil {
+			return err
+		}
+	}
+
+	if err = vipsFixColourspace(img); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func transformGif(ctx context.Context, img **C.struct__VipsImage, po *processingOptions) error {
+	imgWidth := int((*img).Xsize)
+	imgHeight := int((*img).Ysize)
+
+	// Double check dimensions because gif may have many frames
+	if err := checkDimensions(imgWidth, imgHeight); err != nil {
+		return err
+	}
+
+	frameHeight, err := vipsGetInt(*img, "page-height")
+	if err != nil {
+		return err
+	}
+
+	delay, err := vipsGetInt(*img, "gif-delay")
+	if err != nil {
+		return err
+	}
+
+	loop, err := vipsGetInt(*img, "gif-loop")
+	if err != nil {
+		return err
+	}
+
+	framesCount := imgHeight / frameHeight
+
+	frames := make([]*C.struct__VipsImage, framesCount)
+	defer func() {
+		for _, frame := range frames {
+			C.clear_image(&frame)
+		}
+	}()
+
+	var errg errgroup.Group
+
+	for i := 0; i < framesCount; i++ {
+		ind := i
+		errg.Go(func() error {
+			var frame *C.struct__VipsImage
+
+			if err := vipsExtract(*img, &frame, 0, ind*frameHeight, imgWidth, frameHeight); err != nil {
+				return err
+			}
+
+			if err := transformImage(ctx, &frame, nil, po, imageTypeGIF); err != nil {
+				return err
+			}
+
+			frames[ind] = frame
+
+			return nil
+		})
+	}
+
+	if err := errg.Wait(); err != nil {
+		return err
+	}
+
+	checkTimeout(ctx)
+
+	if err := vipsArrayjoin(frames, img); err != nil {
+		return err
+	}
+
+	vipsSetInt(*img, "page-height", int(frames[0].Ysize))
+	vipsSetInt(*img, "gif-delay", delay)
+	vipsSetInt(*img, "gif-loop", loop)
+
+	return nil
+}
+
+func processImage(ctx context.Context) ([]byte, error) {
+	if newRelicEnabled {
+		newRelicCancel := startNewRelicSegment(ctx, "Processing image")
+		defer newRelicCancel()
+	}
+
+	if prometheusEnabled {
+		defer startPrometheusDuration(prometheusProcessingDuration)()
+	}
+
+	po := getProcessingOptions(ctx)
+
+	defer C.vips_cleanup()
+
+	data := getImageData(ctx).Bytes()
+	imgtype := getImageType(ctx)
+
+	if po.Gravity.Type == gravitySmart && !vipsSupportSmartcrop {
+		return nil, errSmartCropNotSupported
+	}
+
+	img, err := vipsLoadImage(data, imgtype, 1, po.Format == imageTypeGIF)
+	if err != nil {
+		return nil, err
+	}
+	defer C.clear_image(&img)
+
+	if imgtype == imageTypeGIF && po.Format == imageTypeGIF {
+		if err := transformGif(ctx, &img, po); err != nil {
+			return nil, err
+		}
+	} else {
+		if err := transformImage(ctx, &img, data, po, imgtype); err != nil {
 			return nil, err
 		}
 	}
 
+	checkTimeout(ctx)
+
+	if po.Format == imageTypeGIF {
+		if err := vipsCastUchar(&img); err != nil {
+			return nil, err
+		}
+		checkTimeout(ctx)
+	}
+
 	return vipsSaveImage(img, po.Format, po.Quality)
 }
 
@@ -401,8 +501,9 @@ func vipsPrepareWatermark() error {
 		return nil
 	}
 
-	if C.vips_load_buffer(unsafe.Pointer(&data[0]), C.size_t(len(data)), C.int(imgtype), 1, &watermark) != 0 {
-		return vipsError()
+	watermark, err = vipsLoadImage(data, imgtype, 1, false)
+	if err != nil {
+		return err
 	}
 
 	var tmp *C.struct__VipsImage
@@ -447,11 +548,30 @@ func vipsPrepareWatermark() error {
 	return nil
 }
 
-func vipsLoadImage(data []byte, imgtype imageType, shrink int) (*C.struct__VipsImage, error) {
+func vipsLoadImage(data []byte, imgtype imageType, shrink int, allPages bool) (*C.struct__VipsImage, error) {
 	var img *C.struct__VipsImage
-	if C.vips_load_buffer(unsafe.Pointer(&data[0]), C.size_t(len(data)), C.int(imgtype), C.int(shrink), &img) != 0 {
+
+	err := C.int(0)
+
+	pages := C.int(1)
+	if allPages {
+		pages = -1
+	}
+
+	switch imgtype {
+	case imageTypeJPEG:
+		err = C.vips_jpegload_go(unsafe.Pointer(&data[0]), C.size_t(len(data)), C.int(shrink), &img)
+	case imageTypePNG:
+		err = C.vips_pngload_go(unsafe.Pointer(&data[0]), C.size_t(len(data)), &img)
+	case imageTypeWEBP:
+		err = C.vips_webpload_go(unsafe.Pointer(&data[0]), C.size_t(len(data)), C.int(shrink), &img)
+	case imageTypeGIF:
+		err = C.vips_gifload_go(unsafe.Pointer(&data[0]), C.size_t(len(data)), pages, &img)
+	}
+	if err != 0 {
 		return nil, vipsError()
 	}
+
 	return img, nil
 }
 
@@ -470,6 +590,8 @@ func vipsSaveImage(img *C.struct__VipsImage, imgtype imageType, quality int) ([]
 		err = C.vips_pngsave_go(img, &ptr, &imgsize, cConf.PngInterlaced)
 	case imageTypeWEBP:
 		err = C.vips_webpsave_go(img, &ptr, &imgsize, 1, C.int(quality))
+	case imageTypeGIF:
+		err = C.vips_gifsave_go(img, &ptr, &imgsize)
 	}
 	if err != 0 {
 		return nil, vipsError()
@@ -478,10 +600,33 @@ func vipsSaveImage(img *C.struct__VipsImage, imgtype imageType, quality int) ([]
 	return C.GoBytes(ptr, C.int(imgsize)), nil
 }
 
+func vipsArrayjoin(in []*C.struct__VipsImage, out **C.struct__VipsImage) error {
+	var tmp *C.struct__VipsImage
+
+	if C.vips_arrayjoin_go(&in[0], &tmp, C.int(len(in))) != 0 {
+		return vipsError()
+	}
+
+	C.swap_and_clear(out, tmp)
+	return nil
+}
+
 func vipsImageHasAlpha(img *C.struct__VipsImage) bool {
 	return C.vips_image_hasalpha_go(img) > 0
 }
 
+func vipsGetInt(img *C.struct__VipsImage, name string) (int, error) {
+	var i C.int
+	if C.vips_image_get_int(img, C.CString(name), &i) != 0 {
+		return 0, vipsError()
+	}
+	return int(i), nil
+}
+
+func vipsSetInt(img *C.struct__VipsImage, name string, value int) {
+	C.vips_image_set_int(img, C.CString(name), C.int(value))
+}
+
 func vipsPremultiply(img **C.struct__VipsImage) (C.VipsBandFormat, error) {
 	var tmp *C.struct__VipsImage
 
@@ -511,6 +656,19 @@ func vipsUnpremultiply(img **C.struct__VipsImage, format C.VipsBandFormat) error
 	return nil
 }
 
+func vipsCastUchar(img **C.struct__VipsImage) error {
+	var tmp *C.struct__VipsImage
+
+	if C.vips_image_get_format(*img) != C.VIPS_FORMAT_UCHAR {
+		if C.vips_cast_go(*img, &tmp, C.VIPS_FORMAT_UCHAR) != 0 {
+			return vipsError()
+		}
+		C.swap_and_clear(img, tmp)
+	}
+
+	return nil
+}
+
 func vipsResize(img **C.struct__VipsImage, scale float64) error {
 	var tmp *C.struct__VipsImage
 
@@ -555,6 +713,13 @@ func vipsCrop(img **C.struct__VipsImage, left, top, width, height int) error {
 	return nil
 }
 
+func vipsExtract(in *C.struct__VipsImage, out **C.struct__VipsImage, left, top, width, height int) error {
+	if C.vips_extract_area_go(in, out, C.int(left), C.int(top), C.int(width), C.int(height)) != 0 {
+		return vipsError()
+	}
+	return nil
+}
+
 func vipsSmartCrop(img **C.struct__VipsImage, width, height int) error {
 	var tmp *C.struct__VipsImage
 
@@ -770,8 +935,10 @@ func vipsApplyWatermark(img **C.struct__VipsImage, opts *watermarkOptions) error
 	}
 	C.swap_and_clear(&wm, tmp)
 
-	if C.vips_image_guess_interpretation(*img) != C.vips_image_guess_interpretation(wm) {
-		if C.vips_colourspace_go(wm, &tmp, C.vips_image_guess_interpretation(*img)) != 0 {
+	imgInterpolation := C.vips_image_guess_interpretation(*img)
+
+	if imgInterpolation != C.vips_image_guess_interpretation(wm) {
+		if C.vips_colourspace_go(wm, &tmp, imgInterpolation) != 0 {
 			return vipsError()
 		}
 		C.swap_and_clear(&wm, tmp)
@@ -784,6 +951,8 @@ func vipsApplyWatermark(img **C.struct__VipsImage, opts *watermarkOptions) error
 		C.swap_and_clear(&wmAlpha, tmp)
 	}
 
+	imgFormat := C.vips_image_get_format(*img)
+
 	var imgAlpha *C.struct__VipsImage
 	defer C.clear_image(&imgAlpha)
 
@@ -812,6 +981,13 @@ func vipsApplyWatermark(img **C.struct__VipsImage, opts *watermarkOptions) error
 		C.swap_and_clear(img, tmp)
 	}
 
+	if imgFormat != C.vips_image_get_format(*img) {
+		if C.vips_cast_go(*img, &tmp, imgFormat) != 0 {
+			return vipsError()
+		}
+		C.swap_and_clear(img, tmp)
+	}
+
 	return nil
 }
 

+ 2 - 0
server.go

@@ -24,12 +24,14 @@ var (
 		imageTypeJPEG: "image/jpeg",
 		imageTypePNG:  "image/png",
 		imageTypeWEBP: "image/webp",
+		imageTypeGIF:  "image/gif",
 	}
 
 	contentDispositions = map[imageType]string{
 		imageTypeJPEG: "inline; filename=\"image.jpg\"",
 		imageTypePNG:  "inline; filename=\"image.png\"",
 		imageTypeWEBP: "inline; filename=\"image.webp\"",
+		imageTypeGIF:  "inline; filename=\"image.gif\"",
 	}
 
 	authHeaderMust []byte

+ 3 - 0
vendor/golang.org/x/sync/AUTHORS

@@ -0,0 +1,3 @@
+# This source code refers to The Go Authors for copyright purposes.
+# The master list of authors is in the main Go distribution,
+# visible at http://tip.golang.org/AUTHORS.

+ 3 - 0
vendor/golang.org/x/sync/CONTRIBUTORS

@@ -0,0 +1,3 @@
+# This source code was written by the Go contributors.
+# The master list of contributors is in the main Go distribution,
+# visible at http://tip.golang.org/CONTRIBUTORS.

+ 27 - 0
vendor/golang.org/x/sync/LICENSE

@@ -0,0 +1,27 @@
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 22 - 0
vendor/golang.org/x/sync/PATENTS

@@ -0,0 +1,22 @@
+Additional IP Rights Grant (Patents)
+
+"This implementation" means the copyrightable works distributed by
+Google as part of the Go project.
+
+Google hereby grants to You a perpetual, worldwide, non-exclusive,
+no-charge, royalty-free, irrevocable (except as stated in this section)
+patent license to make, have made, use, offer to sell, sell, import,
+transfer and otherwise run, modify and propagate the contents of this
+implementation of Go, where such license applies only to those patent
+claims, both currently owned or controlled by Google and acquired in
+the future, licensable by Google that are necessarily infringed by this
+implementation of Go.  This grant does not include claims that would be
+infringed only as a consequence of further modification of this
+implementation.  If you or your agent or exclusive licensee institute or
+order or agree to the institution of patent litigation against any
+entity (including a cross-claim or counterclaim in a lawsuit) alleging
+that this implementation of Go or any code incorporated within this
+implementation of Go constitutes direct or contributory patent
+infringement, or inducement of patent infringement, then any patent
+rights granted to you under this License for this implementation of Go
+shall terminate as of the date such litigation is filed.

+ 66 - 0
vendor/golang.org/x/sync/errgroup/errgroup.go

@@ -0,0 +1,66 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package errgroup provides synchronization, error propagation, and Context
+// cancelation for groups of goroutines working on subtasks of a common task.
+package errgroup
+
+import (
+	"context"
+	"sync"
+)
+
+// A Group is a collection of goroutines working on subtasks that are part of
+// the same overall task.
+//
+// A zero Group is valid and does not cancel on error.
+type Group struct {
+	cancel func()
+
+	wg sync.WaitGroup
+
+	errOnce sync.Once
+	err     error
+}
+
+// WithContext returns a new Group and an associated Context derived from ctx.
+//
+// The derived Context is canceled the first time a function passed to Go
+// returns a non-nil error or the first time Wait returns, whichever occurs
+// first.
+func WithContext(ctx context.Context) (*Group, context.Context) {
+	ctx, cancel := context.WithCancel(ctx)
+	return &Group{cancel: cancel}, ctx
+}
+
+// Wait blocks until all function calls from the Go method have returned, then
+// returns the first non-nil error (if any) from them.
+func (g *Group) Wait() error {
+	g.wg.Wait()
+	if g.cancel != nil {
+		g.cancel()
+	}
+	return g.err
+}
+
+// Go calls the given function in a new goroutine.
+//
+// The first call to return a non-nil error cancels the group; its error will be
+// returned by Wait.
+func (g *Group) Go(f func() error) {
+	g.wg.Add(1)
+
+	go func() {
+		defer g.wg.Done()
+
+		if err := f(); err != nil {
+			g.errOnce.Do(func() {
+				g.err = err
+				if g.cancel != nil {
+					g.cancel()
+				}
+			})
+		}
+	}()
+}

+ 51 - 24
vips.h

@@ -10,7 +10,10 @@
   (VIPS_MAJOR_VERSION > 8 || (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION >= 5))
 
 #define VIPS_SUPPORT_GIF \
-  VIPS_MAJOR_VERSION > 8 || (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION >= 3)
+  (VIPS_MAJOR_VERSION > 8 || (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION >= 3))
+
+#define VIPS_SUPPORT_MAGICK \
+  (VIPS_MAJOR_VERSION > 8 || (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION >= 7))
 
 #define EXIF_ORIENTATION "exif-ifd0-Orientation"
 
@@ -38,16 +41,16 @@ swap_and_clear(VipsImage **in, VipsImage *out) {
 int
 vips_type_find_load_go(int imgtype) {
   if (imgtype == JPEG) {
-    return vips_type_find("VipsOperation", "jpegload");
+    return vips_type_find("VipsOperation", "jpegload_buffer");
   }
   if (imgtype == PNG) {
-    return vips_type_find("VipsOperation", "pngload");
+    return vips_type_find("VipsOperation", "pngload_buffer");
   }
   if (imgtype == WEBP) {
-    return vips_type_find("VipsOperation", "webpload");
+    return vips_type_find("VipsOperation", "webpload_buffer");
   }
   if (imgtype == GIF) {
-    return vips_type_find("VipsOperation", "gifload");
+    return vips_type_find("VipsOperation", "gifload_buffer");
   }
   return 0;
 }
@@ -63,30 +66,40 @@ vips_type_find_save_go(int imgtype) {
   if (imgtype == WEBP) {
     return vips_type_find("VipsOperation", "webpsave_buffer");
   }
+  if (imgtype == GIF) {
+    return vips_type_find("VipsOperation", "magicksave_buffer");
+  }
   return 0;
 }
 
 int
-vips_load_buffer(void *buf, size_t len, int imgtype, int shrink, VipsImage **out) {
-  switch (imgtype) {
-    case JPEG:
-      if (shrink > 1) {
-        return vips_jpegload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, "shrink", shrink, NULL);
-      }
-      return vips_jpegload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, NULL);
-    case PNG:
-      return vips_pngload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, NULL);
-    case WEBP:
-      if (shrink > 1) {
-        return vips_webpload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, "shrink", shrink, NULL);
-      }
-      return vips_webpload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, NULL);
-    #if VIPS_SUPPORT_GIF
-    case GIF:
-      return vips_gifload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, NULL);
-    #endif
+vips_jpegload_go(void *buf, size_t len, int shrink, VipsImage **out) {
+  if (shrink > 1) {
+    return vips_jpegload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, "shrink", shrink, NULL);
   }
-  return 1;
+  return vips_jpegload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, NULL);
+}
+
+int
+vips_pngload_go(void *buf, size_t len, VipsImage **out) {
+  return vips_pngload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, NULL);
+}
+
+int
+vips_webpload_go(void *buf, size_t len, int shrink, VipsImage **out) {
+  if (shrink > 1) {
+    return vips_webpload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, "shrink", shrink, NULL);
+  }
+  return vips_webpload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, NULL);
+}
+
+int
+vips_gifload_go(void *buf, size_t len, int pages, VipsImage **out) {
+  #if VIPS_SUPPORT_GIF
+    return vips_gifload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, "n", pages, NULL);
+  #else
+    return 1;
+  #endif
 }
 
 int
@@ -238,6 +251,11 @@ vips_ifthenelse_go(VipsImage *cond, VipsImage *in1, VipsImage *in2, VipsImage **
   return vips_ifthenelse(cond, in1, in2, out, "blend", TRUE, NULL);
 }
 
+int
+vips_arrayjoin_go(VipsImage **in, VipsImage **out, int n) {
+  return vips_arrayjoin(in, out, n, "across", 1, NULL);
+}
+
 int
 vips_jpegsave_go(VipsImage *in, void **buf, size_t *len, int strip, int quality, int interlace) {
   return vips_jpegsave_buffer(in, buf, len, "strip", strip, "Q", quality, "optimize_coding", TRUE, "interlace", interlace, NULL);
@@ -253,6 +271,15 @@ vips_webpsave_go(VipsImage *in, void **buf, size_t *len, int strip, int quality)
   return vips_webpsave_buffer(in, buf, len, "strip", strip, "Q", quality, NULL);
 }
 
+int
+vips_gifsave_go(VipsImage *in, void **buf, size_t *len) {
+#if VIPS_SUPPORT_MAGICK
+  return vips_magicksave_buffer(in, buf, len, "format", "gif", NULL);
+#else
+  return 1;
+#endif
+}
+
 void
 vips_cleanup() {
   vips_thread_shutdown();