From 2c0b538eb590a15ec09f6e549c468d16c49a55c9 Mon Sep 17 00:00:00 2001 From: DarthSim Date: Thu, 8 Nov 2018 16:34:21 +0600 Subject: [PATCH] GIF output support --- Dockerfile | 73 +++- Gopkg.lock | 9 + README.md | 2 +- docs/generating_the_url_advanced.md | 4 +- docs/generating_the_url_basic.md | 4 +- docs/image_formats_support.md | 12 + docs/source_image_formats_support.md | 8 - download.go | 26 +- process.go | 322 ++++++++++++++---- server.go | 2 + vendor/golang.org/x/sync/AUTHORS | 3 + vendor/golang.org/x/sync/CONTRIBUTORS | 3 + vendor/golang.org/x/sync/LICENSE | 27 ++ vendor/golang.org/x/sync/PATENTS | 22 ++ vendor/golang.org/x/sync/errgroup/errgroup.go | 66 ++++ vips.h | 75 ++-- 16 files changed, 535 insertions(+), 123 deletions(-) create mode 100644 docs/image_formats_support.md delete mode 100644 docs/source_image_formats_support.md create mode 100644 vendor/golang.org/x/sync/AUTHORS create mode 100644 vendor/golang.org/x/sync/CONTRIBUTORS create mode 100644 vendor/golang.org/x/sync/LICENSE create mode 100644 vendor/golang.org/x/sync/PATENTS create mode 100644 vendor/golang.org/x/sync/errgroup/errgroup.go diff --git a/Dockerfile b/Dockerfile index 1dcadfc1..4f3ef2b9 100644 --- a/Dockerfile +++ b/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 " 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"] diff --git a/Gopkg.lock b/Gopkg.lock index ff7cc725..a225b766 100644 --- a/Gopkg.lock +++ b/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" diff --git a/README.md b/README.md index 8abe84b5..e980b35e 100644 --- a/README.md +++ b/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) diff --git a/docs/generating_the_url_advanced.md b/docs/generating_the_url_advanced.md index 2afa070b..d1335b6c 100644 --- a/docs/generating_the_url_advanced.md +++ b/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. diff --git a/docs/generating_the_url_basic.md b/docs/generating_the_url_basic.md index 7e22d500..e8b196d8 100644 --- a/docs/generating_the_url_basic.md +++ b/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. diff --git a/docs/image_formats_support.md b/docs/image_formats_support.md new file mode 100644 index 00000000..aeb65ea9 --- /dev/null +++ b/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. diff --git a/docs/source_image_formats_support.md b/docs/source_image_formats_support.md deleted file mode 100644 index d8c55330..00000000 --- a/docs/source_image_formats_support.md +++ /dev/null @@ -1,8 +0,0 @@ -# Source image formats support - -At the moment, imgproxy supports only the most popular Web image formats: - -* PNG; -* JPEG; -* GIF; -* WebP. diff --git a/download.go b/download.go index c69c9451..75b45eda 100644 --- a/download.go +++ b/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 } diff --git a/process.go b/process.go index 53937b17..387b356d 100644 --- a/process.go +++ b/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() - } +func transformImage(ctx context.Context, img **C.struct__VipsImage, data []byte, po *processingOptions, imgtype imageType) error { + var err error - 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) - - 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,29 +337,153 @@ 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 } diff --git a/server.go b/server.go index ef66af54..ccfe506e 100644 --- a/server.go +++ b/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 diff --git a/vendor/golang.org/x/sync/AUTHORS b/vendor/golang.org/x/sync/AUTHORS new file mode 100644 index 00000000..15167cd7 --- /dev/null +++ b/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. diff --git a/vendor/golang.org/x/sync/CONTRIBUTORS b/vendor/golang.org/x/sync/CONTRIBUTORS new file mode 100644 index 00000000..1c4577e9 --- /dev/null +++ b/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. diff --git a/vendor/golang.org/x/sync/LICENSE b/vendor/golang.org/x/sync/LICENSE new file mode 100644 index 00000000..6a66aea5 --- /dev/null +++ b/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. diff --git a/vendor/golang.org/x/sync/PATENTS b/vendor/golang.org/x/sync/PATENTS new file mode 100644 index 00000000..73309904 --- /dev/null +++ b/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. diff --git a/vendor/golang.org/x/sync/errgroup/errgroup.go b/vendor/golang.org/x/sync/errgroup/errgroup.go new file mode 100644 index 00000000..9857fe53 --- /dev/null +++ b/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() + } + }) + } + }() +} diff --git a/vips.h b/vips.h index 031ba907..b8c3d2ca 100644 --- a/vips.h +++ b/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();