├── testdata ├── test.gif ├── test.jp2 ├── test.jpg ├── test.pdf ├── test.png ├── corrupt.jpg ├── test.webp ├── test_gif.jpg ├── test_pdf.jpg ├── test_svg.jpg ├── vertical.jpg ├── vertical.webp ├── test_issue.jpg ├── test_square.jpg ├── transparent.png ├── exif │ ├── Landscape_1.jpg │ ├── Landscape_2.jpg │ ├── Landscape_3.jpg │ ├── Landscape_4.jpg │ ├── Landscape_5.jpg │ ├── Landscape_6.jpg │ ├── Landscape_7.jpg │ ├── Landscape_8.jpg │ ├── Portrait_1.jpg │ ├── Portrait_2.jpg │ ├── Portrait_3.jpg │ ├── Portrait_4.jpg │ ├── Portrait_5.jpg │ ├── Portrait_6.jpg │ ├── Portrait_7.jpg │ ├── Portrait_8.jpg │ ├── Portrait_1_out.jpg │ ├── Portrait_2_out.jpg │ ├── Portrait_3_out.jpg │ ├── Portrait_4_out.jpg │ ├── Portrait_5_out.jpg │ ├── Portrait_6_out.jpg │ ├── Portrait_7_out.jpg │ ├── Portrait_8_out.jpg │ ├── Landscape_1_out.jpg │ ├── Landscape_2_out.jpg │ ├── Landscape_3_out.jpg │ ├── Landscape_4_out.jpg │ ├── Landscape_5_out.jpg │ ├── Landscape_6_out.jpg │ ├── Landscape_7_out.jpg │ └── Landscape_8_out.jpg ├── test_icc_prophoto.jpg ├── test_smart_crop.jpg ├── transparent_trim.png └── northern_cardinal_bird.jpg ├── version.go ├── .gitignore ├── .editorconfig ├── resize_legacy.go ├── Gopkg.lock ├── file.go ├── Gopkg.toml ├── file_test.go ├── resize.go ├── LICENSE ├── metadata.go ├── .travis.yml ├── type_test.go ├── metadata_test.go ├── vips_test.go ├── type.go ├── History.md ├── image.go ├── options.go ├── README.md ├── image_test.go ├── preinstall.sh ├── vips.h ├── resizer.go ├── vips.go └── resizer_test.go /testdata/test.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/test.gif -------------------------------------------------------------------------------- /testdata/test.jp2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/test.jp2 -------------------------------------------------------------------------------- /testdata/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/test.jpg -------------------------------------------------------------------------------- /testdata/test.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/test.pdf -------------------------------------------------------------------------------- /testdata/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/test.png -------------------------------------------------------------------------------- /testdata/corrupt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/corrupt.jpg -------------------------------------------------------------------------------- /testdata/test.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/test.webp -------------------------------------------------------------------------------- /testdata/test_gif.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/test_gif.jpg -------------------------------------------------------------------------------- /testdata/test_pdf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/test_pdf.jpg -------------------------------------------------------------------------------- /testdata/test_svg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/test_svg.jpg -------------------------------------------------------------------------------- /testdata/vertical.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/vertical.jpg -------------------------------------------------------------------------------- /testdata/vertical.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/vertical.webp -------------------------------------------------------------------------------- /testdata/test_issue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/test_issue.jpg -------------------------------------------------------------------------------- /testdata/test_square.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/test_square.jpg -------------------------------------------------------------------------------- /testdata/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/transparent.png -------------------------------------------------------------------------------- /testdata/exif/Landscape_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Landscape_1.jpg -------------------------------------------------------------------------------- /testdata/exif/Landscape_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Landscape_2.jpg -------------------------------------------------------------------------------- /testdata/exif/Landscape_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Landscape_3.jpg -------------------------------------------------------------------------------- /testdata/exif/Landscape_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Landscape_4.jpg -------------------------------------------------------------------------------- /testdata/exif/Landscape_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Landscape_5.jpg -------------------------------------------------------------------------------- /testdata/exif/Landscape_6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Landscape_6.jpg -------------------------------------------------------------------------------- /testdata/exif/Landscape_7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Landscape_7.jpg -------------------------------------------------------------------------------- /testdata/exif/Landscape_8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Landscape_8.jpg -------------------------------------------------------------------------------- /testdata/exif/Portrait_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Portrait_1.jpg -------------------------------------------------------------------------------- /testdata/exif/Portrait_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Portrait_2.jpg -------------------------------------------------------------------------------- /testdata/exif/Portrait_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Portrait_3.jpg -------------------------------------------------------------------------------- /testdata/exif/Portrait_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Portrait_4.jpg -------------------------------------------------------------------------------- /testdata/exif/Portrait_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Portrait_5.jpg -------------------------------------------------------------------------------- /testdata/exif/Portrait_6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Portrait_6.jpg -------------------------------------------------------------------------------- /testdata/exif/Portrait_7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Portrait_7.jpg -------------------------------------------------------------------------------- /testdata/exif/Portrait_8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Portrait_8.jpg -------------------------------------------------------------------------------- /testdata/test_icc_prophoto.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/test_icc_prophoto.jpg -------------------------------------------------------------------------------- /testdata/test_smart_crop.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/test_smart_crop.jpg -------------------------------------------------------------------------------- /testdata/transparent_trim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/transparent_trim.png -------------------------------------------------------------------------------- /testdata/exif/Portrait_1_out.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Portrait_1_out.jpg -------------------------------------------------------------------------------- /testdata/exif/Portrait_2_out.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Portrait_2_out.jpg -------------------------------------------------------------------------------- /testdata/exif/Portrait_3_out.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Portrait_3_out.jpg -------------------------------------------------------------------------------- /testdata/exif/Portrait_4_out.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Portrait_4_out.jpg -------------------------------------------------------------------------------- /testdata/exif/Portrait_5_out.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Portrait_5_out.jpg -------------------------------------------------------------------------------- /testdata/exif/Portrait_6_out.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Portrait_6_out.jpg -------------------------------------------------------------------------------- /testdata/exif/Portrait_7_out.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Portrait_7_out.jpg -------------------------------------------------------------------------------- /testdata/exif/Portrait_8_out.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Portrait_8_out.jpg -------------------------------------------------------------------------------- /testdata/exif/Landscape_1_out.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Landscape_1_out.jpg -------------------------------------------------------------------------------- /testdata/exif/Landscape_2_out.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Landscape_2_out.jpg -------------------------------------------------------------------------------- /testdata/exif/Landscape_3_out.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Landscape_3_out.jpg -------------------------------------------------------------------------------- /testdata/exif/Landscape_4_out.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Landscape_4_out.jpg -------------------------------------------------------------------------------- /testdata/exif/Landscape_5_out.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Landscape_5_out.jpg -------------------------------------------------------------------------------- /testdata/exif/Landscape_6_out.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Landscape_6_out.jpg -------------------------------------------------------------------------------- /testdata/exif/Landscape_7_out.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Landscape_7_out.jpg -------------------------------------------------------------------------------- /testdata/exif/Landscape_8_out.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/exif/Landscape_8_out.jpg -------------------------------------------------------------------------------- /testdata/northern_cardinal_bird.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikia/bimg/master/testdata/northern_cardinal_bird.jpg -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package bimg 2 | 3 | // Version represents the current package semantic version. 4 | const Version = "1.0.18" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bimg 2 | /bundle 3 | bin 4 | /*.jpg 5 | /*.png 6 | /*.webp 7 | /testdata/*_out.* 8 | /.idea/ 9 | testdata/test_vertical_*.jpg 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /resize_legacy.go: -------------------------------------------------------------------------------- 1 | // +build !go1.7 2 | 3 | package bimg 4 | 5 | // Resize is used to transform a given image as byte buffer 6 | // with the passed options. 7 | // Used as proxy to resizer() only in Go <= 1.6 versions 8 | func Resize(buf []byte, o Options) ([]byte, error) { 9 | return resizer(buf, o) 10 | } 11 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [solve-meta] 5 | analyzer-name = "dep" 6 | analyzer-version = 1 7 | inputs-digest = "ab4fef131ee828e96ba67d31a7d690bd5f2f42040c6766b1b12fe856f87e0ff7" 8 | solver-name = "gps-cdcl" 9 | solver-version = 1 10 | -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | package bimg 2 | 3 | import "io/ioutil" 4 | 5 | // Read reads all the content of the given file path 6 | // and returns it as byte buffer. 7 | func Read(path string) ([]byte, error) { 8 | return ioutil.ReadFile(path) 9 | } 10 | 11 | // Write writes the given byte buffer into disk 12 | // to the given file path. 13 | func Write(path string, buf []byte) error { 14 | return ioutil.WriteFile(path, buf, 0644) 15 | } 16 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | -------------------------------------------------------------------------------- /file_test.go: -------------------------------------------------------------------------------- 1 | package bimg 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestRead(t *testing.T) { 8 | buf, err := Read("testdata/test.jpg") 9 | 10 | if err != nil { 11 | t.Errorf("Cannot read the image: %#v", err) 12 | } 13 | 14 | if len(buf) == 0 { 15 | t.Fatal("Empty buffer") 16 | } 17 | 18 | if DetermineImageType(buf) != JPEG { 19 | t.Fatal("Image is not jpeg") 20 | } 21 | } 22 | 23 | func TestWrite(t *testing.T) { 24 | buf, err := Read("testdata/test.jpg") 25 | 26 | if err != nil { 27 | t.Errorf("Cannot read the image: %#v", err) 28 | } 29 | 30 | if len(buf) == 0 { 31 | t.Fatal("Empty buffer") 32 | } 33 | 34 | err = Write("testdata/test_write_out.jpg", buf) 35 | if err != nil { 36 | t.Fatalf("Cannot write the file: %#v", err) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /resize.go: -------------------------------------------------------------------------------- 1 | // +build go1.7 2 | 3 | package bimg 4 | 5 | import ( 6 | "runtime" 7 | ) 8 | 9 | // Resize is used to transform a given image as byte buffer 10 | // with the passed options. 11 | func Resize(buf []byte, o Options) ([]byte, error) { 12 | // Required in order to prevent premature garbage collection. See: 13 | // https://github.com/h2non/bimg/pull/162 14 | defer runtime.KeepAlive(buf) 15 | return resizer(buf, o) 16 | } 17 | 18 | // Simulate the window-crop-fixed operation of Vignette 19 | // see https://github.com/Wikia/vignette/#window-crop-fixed 20 | func WindowCropFixed(buf []byte, o Options) ([]byte, error) { 21 | // Required in order to prevent premature garbage collection. See: 22 | // https://github.com/h2non/bimg/pull/162 23 | defer runtime.KeepAlive(buf) 24 | return windowcropfixed(buf, o) 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) Tomas Aparicio and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /metadata.go: -------------------------------------------------------------------------------- 1 | package bimg 2 | 3 | /* 4 | #cgo pkg-config: vips 5 | #include "vips/vips.h" 6 | */ 7 | import "C" 8 | 9 | // ImageSize represents the image width and height values 10 | type ImageSize struct { 11 | Width int 12 | Height int 13 | } 14 | 15 | // ImageMetadata represents the basic metadata fields 16 | type ImageMetadata struct { 17 | Orientation int 18 | Channels int 19 | Alpha bool 20 | Profile bool 21 | Type string 22 | Space string 23 | Colourspace string 24 | Size ImageSize 25 | } 26 | 27 | // Size returns the image size by width and height pixels. 28 | func Size(buf []byte) (ImageSize, error) { 29 | metadata, err := Metadata(buf) 30 | if err != nil { 31 | return ImageSize{}, err 32 | } 33 | 34 | return ImageSize{ 35 | Width: int(metadata.Size.Width), 36 | Height: int(metadata.Size.Height), 37 | }, nil 38 | } 39 | 40 | // ColourspaceIsSupported checks if the image colourspace is supported by libvips. 41 | func ColourspaceIsSupported(buf []byte) (bool, error) { 42 | return vipsColourspaceIsSupportedBuffer(buf) 43 | } 44 | 45 | // ImageInterpretation returns the image interpretation type. 46 | // See: https://jcupitt.github.io/libvips/API/current/VipsImage.html#VipsInterpretation 47 | func ImageInterpretation(buf []byte) (Interpretation, error) { 48 | return vipsInterpretationBuffer(buf) 49 | } 50 | 51 | // Metadata returns the image metadata (size, type, alpha channel, profile, EXIF orientation...). 52 | func Metadata(buf []byte) (ImageMetadata, error) { 53 | defer C.vips_thread_shutdown() 54 | 55 | image, imageType, err := vipsRead(buf) 56 | if err != nil { 57 | return ImageMetadata{}, err 58 | } 59 | defer C.g_object_unref(C.gpointer(image)) 60 | 61 | size := ImageSize{ 62 | Width: int(image.Xsize), 63 | Height: int(image.Ysize), 64 | } 65 | 66 | metadata := ImageMetadata{ 67 | Size: size, 68 | Channels: int(image.Bands), 69 | Orientation: vipsExifOrientation(image), 70 | Alpha: vipsHasAlpha(image), 71 | Profile: vipsHasProfile(image), 72 | Space: vipsSpace(image), 73 | Type: ImageTypeName(imageType), 74 | } 75 | 76 | return metadata, nil 77 | } 78 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | dist: trusty 4 | sudo: false 5 | 6 | go: 7 | - "1.8" 8 | - "1.9" 9 | - "1.10" 10 | - "1.11" 11 | - "tip" 12 | 13 | env: 14 | - LIBVIPS=7.42.3 15 | - LIBVIPS=8.2.3 16 | - LIBVIPS=8.3.3 17 | - LIBVIPS=8.4.6 18 | - LIBVIPS=8.5.8 19 | - LIBVIPS=8.6.2 20 | - LIBVIPS=master 21 | 22 | matrix: 23 | allow_failures: 24 | - env: LIBVIPS=7.42.3 25 | - env: LIBVIPS=8.2.3 26 | - env: LIBVIPS=8.3.3 27 | 28 | cache: 29 | apt: 30 | directories: 31 | - $HOME/libvips 32 | 33 | addons: 34 | apt: 35 | packages: 36 | - gobject-introspection 37 | - gtk-doc-tools 38 | - libcfitsio3-dev 39 | - libfftw3-dev 40 | - libgif-dev 41 | - libgs-dev 42 | - libgsf-1-dev 43 | - libmatio-dev 44 | - libopenslide-dev 45 | - liborc-0.4-dev 46 | - libpango1.0-dev 47 | - libpoppler-glib-dev 48 | - libwebp-dev 49 | 50 | # VIPS 8.3.3 requires Poppler 0.30 which is not released on Trusty. 51 | before_install: 52 | - > 53 | test "$LIBVIPS" != "master" -a "$LIBVIPS" \< "8.4" \ 54 | && wget http://www.vips.ecs.soton.ac.uk/supported/${LIBVIPS%.*}/vips-${LIBVIPS}.tar.gz -O vips.tgz \ 55 | || echo ":-)" 56 | - > 57 | test "$LIBVIPS" != "master" -a "$LIBVIPS" \> "8.4" \ 58 | && wget https://github.com/jcupitt/libvips/archive/v${LIBVIPS}.tar.gz -O vips.tgz \ 59 | || echo ":-)" 60 | - > 61 | test $LIBVIPS == "master" \ 62 | && wget https://github.com/jcupitt/libvips/archive/${LIBVIPS}.tar.gz -O vips.tgz \ 63 | || echo ":-)" 64 | - mkdir libvips 65 | - tar xf vips.tgz -C libvips --strip-components 1 66 | - cd libvips 67 | - test -f autogen.sh && ./autogen.sh || ./bootstrap.sh 68 | - > 69 | CXXFLAGS=-D_GLIBCXX_USE_CXX11_ABI=0 70 | ./configure 71 | --disable-debug 72 | --disable-dependency-tracking 73 | --disable-introspection 74 | --disable-static 75 | --enable-gtk-doc-html=no 76 | --enable-gtk-doc=no 77 | --enable-pyvips8=no 78 | --without-orc 79 | --without-python 80 | --prefix=$HOME/libvips 81 | $1 82 | - make 83 | - make install 84 | - cd .. 85 | - export PATH=$PATH:$HOME/libvips/bin 86 | - export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:$HOME/libvips/lib/pkgconfig 87 | - export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$HOME/libvips/lib 88 | - vips --vips-version 89 | 90 | install: 91 | - go get -u golang.org/x/lint/golint 92 | 93 | script: 94 | - diff -u <(echo -n) <(gofmt -s -d ./) 95 | - diff -u <(echo -n) <(go vet ./) 96 | - diff -u <(echo -n) <(golint ./) 97 | - go test -v -race -covermode=atomic -coverprofile=coverage.out 98 | 99 | after_success: 100 | - goveralls -coverprofile=coverage.out -service=travis-ci 101 | -------------------------------------------------------------------------------- /type_test.go: -------------------------------------------------------------------------------- 1 | package bimg 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path" 7 | "testing" 8 | ) 9 | 10 | func TestDeterminateImageType(t *testing.T) { 11 | files := []struct { 12 | name string 13 | expected ImageType 14 | }{ 15 | {"test.jpg", JPEG}, 16 | {"test.png", PNG}, 17 | {"test.webp", WEBP}, 18 | {"test.gif", GIF}, 19 | {"test.pdf", PDF}, 20 | {"test.svg", SVG}, 21 | {"test.jp2", MAGICK}, 22 | } 23 | 24 | for _, file := range files { 25 | img, _ := os.Open(path.Join("testdata", file.name)) 26 | buf, _ := ioutil.ReadAll(img) 27 | defer img.Close() 28 | 29 | if VipsIsTypeSupported(file.expected) { 30 | if DetermineImageType(buf) != file.expected { 31 | t.Fatalf("Image type is not valid: %s != %s", file.name, ImageTypes[file.expected]) 32 | } 33 | } 34 | } 35 | } 36 | 37 | func TestDeterminateImageTypeName(t *testing.T) { 38 | files := []struct { 39 | name string 40 | expected string 41 | }{ 42 | {"test.jpg", "jpeg"}, 43 | {"test.png", "png"}, 44 | {"test.webp", "webp"}, 45 | {"test.gif", "gif"}, 46 | {"test.pdf", "pdf"}, 47 | {"test.svg", "svg"}, 48 | {"test.jp2", "magick"}, 49 | } 50 | 51 | for _, file := range files { 52 | img, _ := os.Open(path.Join("testdata", file.name)) 53 | buf, _ := ioutil.ReadAll(img) 54 | defer img.Close() 55 | 56 | if DetermineImageTypeName(buf) != file.expected { 57 | t.Fatalf("Image type is not valid: %s != %s", file.name, file.expected) 58 | } 59 | } 60 | } 61 | 62 | func TestIsTypeSupported(t *testing.T) { 63 | types := []struct { 64 | name ImageType 65 | }{ 66 | {JPEG}, {PNG}, {WEBP}, {GIF}, {PDF}, 67 | } 68 | 69 | for _, n := range types { 70 | if IsTypeSupported(n.name) == false { 71 | t.Fatalf("Image type %s is not valid", ImageTypes[n.name]) 72 | } 73 | } 74 | } 75 | 76 | func TestIsTypeNameSupported(t *testing.T) { 77 | types := []struct { 78 | name string 79 | expected bool 80 | }{ 81 | {"jpeg", true}, 82 | {"png", true}, 83 | {"webp", true}, 84 | {"gif", true}, 85 | {"pdf", true}, 86 | } 87 | 88 | for _, n := range types { 89 | if IsTypeNameSupported(n.name) != n.expected { 90 | t.Fatalf("Image type %s is not valid", n.name) 91 | } 92 | } 93 | } 94 | 95 | func TestIsTypeSupportedSave(t *testing.T) { 96 | types := []struct { 97 | name ImageType 98 | }{ 99 | {JPEG}, {PNG}, {WEBP}, 100 | } 101 | if VipsVersion >= "8.5.0" { 102 | types = append(types, struct{ name ImageType }{TIFF}) 103 | } 104 | 105 | for _, n := range types { 106 | if IsTypeSupportedSave(n.name) == false { 107 | t.Fatalf("Image type %s is not valid", ImageTypes[n.name]) 108 | } 109 | } 110 | } 111 | 112 | func TestIsTypeNameSupportedSave(t *testing.T) { 113 | types := []struct { 114 | name string 115 | expected bool 116 | }{ 117 | {"jpeg", true}, 118 | {"png", true}, 119 | {"webp", true}, 120 | {"gif", false}, 121 | {"pdf", false}, 122 | {"tiff", VipsVersion >= "8.5.0"}, 123 | } 124 | 125 | for _, n := range types { 126 | if IsTypeNameSupportedSave(n.name) != n.expected { 127 | t.Fatalf("Image type %s is not valid", n.name) 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /metadata_test.go: -------------------------------------------------------------------------------- 1 | package bimg 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path" 7 | "testing" 8 | ) 9 | 10 | func TestSize(t *testing.T) { 11 | files := []struct { 12 | name string 13 | width int 14 | height int 15 | }{ 16 | {"test.jpg", 1680, 1050}, 17 | {"test.png", 400, 300}, 18 | {"test.webp", 550, 368}, 19 | } 20 | for _, file := range files { 21 | size, err := Size(readFile(file.name)) 22 | if err != nil { 23 | t.Fatalf("Cannot read the image: %#v", err) 24 | } 25 | 26 | if size.Width != file.width || size.Height != file.height { 27 | t.Fatalf("Unexpected image size: %dx%d", size.Width, size.Height) 28 | } 29 | } 30 | } 31 | 32 | func TestMetadata(t *testing.T) { 33 | files := []struct { 34 | name string 35 | format string 36 | orientation int 37 | alpha bool 38 | profile bool 39 | space string 40 | }{ 41 | {"test.jpg", "jpeg", 0, false, false, "srgb"}, 42 | {"test_icc_prophoto.jpg", "jpeg", 0, false, true, "srgb"}, 43 | {"test.png", "png", 0, true, false, "srgb"}, 44 | {"test.webp", "webp", 0, false, false, "srgb"}, 45 | } 46 | 47 | for _, file := range files { 48 | metadata, err := Metadata(readFile(file.name)) 49 | if err != nil { 50 | t.Fatalf("Cannot read the image: %s -> %s", file.name, err) 51 | } 52 | 53 | if metadata.Type != file.format { 54 | t.Fatalf("Unexpected image format: %s", file.format) 55 | } 56 | if metadata.Orientation != file.orientation { 57 | t.Fatalf("Unexpected image orientation: %d != %d", metadata.Orientation, file.orientation) 58 | } 59 | if metadata.Alpha != file.alpha { 60 | t.Fatalf("Unexpected image alpha: %t != %t", metadata.Alpha, file.alpha) 61 | } 62 | if metadata.Profile != file.profile { 63 | t.Fatalf("Unexpected image profile: %t != %t", metadata.Profile, file.profile) 64 | } 65 | if metadata.Space != file.space { 66 | t.Fatalf("Unexpected image profile: %t != %t", metadata.Profile, file.profile) 67 | } 68 | } 69 | } 70 | 71 | func TestImageInterpretation(t *testing.T) { 72 | files := []struct { 73 | name string 74 | interpretation Interpretation 75 | }{ 76 | {"test.jpg", InterpretationSRGB}, 77 | {"test.png", InterpretationSRGB}, 78 | {"test.webp", InterpretationSRGB}, 79 | } 80 | 81 | for _, file := range files { 82 | interpretation, err := ImageInterpretation(readFile(file.name)) 83 | if err != nil { 84 | t.Fatalf("Cannot read the image: %s -> %s", file.name, err) 85 | } 86 | if interpretation != file.interpretation { 87 | t.Fatalf("Unexpected image interpretation") 88 | } 89 | } 90 | } 91 | 92 | func TestColourspaceIsSupported(t *testing.T) { 93 | files := []struct { 94 | name string 95 | }{ 96 | {"test.jpg"}, 97 | {"test.png"}, 98 | {"test.webp"}, 99 | } 100 | 101 | for _, file := range files { 102 | supported, err := ColourspaceIsSupported(readFile(file.name)) 103 | if err != nil { 104 | t.Fatalf("Cannot read the image: %s -> %s", file.name, err) 105 | } 106 | if supported != true { 107 | t.Fatalf("Unsupported image colourspace") 108 | } 109 | } 110 | 111 | supported, err := initImage("test.jpg").ColourspaceIsSupported() 112 | if err != nil { 113 | t.Errorf("Cannot process the image: %#v", err) 114 | } 115 | if supported != true { 116 | t.Errorf("Non-supported colourspace") 117 | } 118 | } 119 | 120 | func readFile(file string) []byte { 121 | data, _ := os.Open(path.Join("testdata", file)) 122 | buf, _ := ioutil.ReadAll(data) 123 | return buf 124 | } 125 | -------------------------------------------------------------------------------- /vips_test.go: -------------------------------------------------------------------------------- 1 | package bimg 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path" 7 | "testing" 8 | ) 9 | 10 | func TestVipsRead(t *testing.T) { 11 | files := []struct { 12 | name string 13 | expected ImageType 14 | }{ 15 | {"test.jpg", JPEG}, 16 | {"test.png", PNG}, 17 | {"test.webp", WEBP}, 18 | } 19 | 20 | for _, file := range files { 21 | image, imageType, _ := vipsRead(readImage(file.name)) 22 | if image == nil { 23 | t.Fatal("Empty image") 24 | } 25 | if imageType != file.expected { 26 | t.Fatal("Invalid image type") 27 | } 28 | } 29 | } 30 | 31 | func TestVipsSave(t *testing.T) { 32 | types := [...]ImageType{JPEG, PNG, WEBP} 33 | 34 | for _, typ := range types { 35 | image, _, _ := vipsRead(readImage("test.jpg")) 36 | options := vipsSaveOptions{Quality: 95, Type: typ, StripMetadata: true} 37 | 38 | buf, err := vipsSave(image, options) 39 | if err != nil { 40 | t.Fatalf("Cannot save the image as '%v'", ImageTypes[typ]) 41 | } 42 | if len(buf) == 0 { 43 | t.Fatalf("Empty saved '%v' image", ImageTypes[typ]) 44 | } 45 | } 46 | } 47 | 48 | func TestVipsSaveTiff(t *testing.T) { 49 | if !IsTypeSupportedSave(TIFF) { 50 | t.Skipf("Format %#v is not supported", ImageTypes[TIFF]) 51 | } 52 | image, _, _ := vipsRead(readImage("test.jpg")) 53 | options := vipsSaveOptions{Quality: 95, Type: TIFF} 54 | buf, _ := vipsSave(image, options) 55 | 56 | if len(buf) == 0 { 57 | t.Fatalf("Empty saved '%v' image", ImageTypes[TIFF]) 58 | } 59 | } 60 | 61 | func TestVipsRotate(t *testing.T) { 62 | files := []struct { 63 | name string 64 | rotate Angle 65 | }{ 66 | {"test.jpg", D90}, 67 | {"test_square.jpg", D45}, 68 | } 69 | 70 | for _, file := range files { 71 | image, _, _ := vipsRead(readImage(file.name)) 72 | 73 | newImg, err := vipsRotate(image, file.rotate) 74 | if err != nil { 75 | t.Fatal("Cannot rotate the image") 76 | } 77 | 78 | buf, _ := vipsSave(newImg, vipsSaveOptions{Quality: 95}) 79 | if len(buf) == 0 { 80 | t.Fatal("Empty image") 81 | } 82 | } 83 | } 84 | 85 | func TestVipsZoom(t *testing.T) { 86 | image, _, _ := vipsRead(readImage("test.jpg")) 87 | 88 | newImg, err := vipsZoom(image, 1) 89 | if err != nil { 90 | t.Fatal("Cannot save the image") 91 | } 92 | 93 | buf, _ := vipsSave(newImg, vipsSaveOptions{Quality: 95}) 94 | if len(buf) == 0 { 95 | t.Fatal("Empty image") 96 | } 97 | } 98 | 99 | func TestVipsWatermark(t *testing.T) { 100 | image, _, _ := vipsRead(readImage("test.jpg")) 101 | 102 | watermark := Watermark{ 103 | Text: "Copy me if you can", 104 | Font: "sans bold 12", 105 | Opacity: 0.5, 106 | Width: 200, 107 | DPI: 100, 108 | Margin: 100, 109 | Background: Color{255, 255, 255}, 110 | } 111 | 112 | newImg, err := vipsWatermark(image, watermark) 113 | if err != nil { 114 | t.Errorf("Cannot add watermark: %s", err) 115 | } 116 | 117 | buf, _ := vipsSave(newImg, vipsSaveOptions{Quality: 95}) 118 | if len(buf) == 0 { 119 | t.Fatal("Empty image") 120 | } 121 | } 122 | 123 | func TestVipsWatermarkWithImage(t *testing.T) { 124 | image, _, _ := vipsRead(readImage("test.jpg")) 125 | 126 | watermark := readImage("transparent.png") 127 | 128 | options := WatermarkImage{Left: 100, Top: 100, Opacity: 1.0, Buf: watermark} 129 | newImg, err := vipsDrawWatermark(image, options) 130 | if err != nil { 131 | t.Errorf("Cannot add watermark: %s", err) 132 | } 133 | 134 | buf, _ := vipsSave(newImg, vipsSaveOptions{Quality: 95}) 135 | if len(buf) == 0 { 136 | t.Fatal("Empty image") 137 | } 138 | } 139 | 140 | func TestVipsImageType(t *testing.T) { 141 | imgType := vipsImageType(readImage("test.jpg")) 142 | if imgType != JPEG { 143 | t.Fatal("Invalid image type") 144 | } 145 | } 146 | 147 | func TestVipsImageTypeInvalid(t *testing.T) { 148 | imgType := vipsImageType([]byte("vip")) 149 | if imgType != UNKNOWN { 150 | t.Fatal("Invalid image type") 151 | } 152 | } 153 | 154 | func TestVipsMemory(t *testing.T) { 155 | mem := VipsMemory() 156 | 157 | if mem.Memory < 1024 { 158 | t.Fatal("Invalid memory") 159 | } 160 | if mem.Allocations == 0 { 161 | t.Fatal("Invalid memory allocations") 162 | } 163 | } 164 | 165 | func readImage(file string) []byte { 166 | img, _ := os.Open(path.Join("testdata", file)) 167 | buf, _ := ioutil.ReadAll(img) 168 | defer img.Close() 169 | return buf 170 | } 171 | -------------------------------------------------------------------------------- /type.go: -------------------------------------------------------------------------------- 1 | package bimg 2 | 3 | import ( 4 | "regexp" 5 | "sync" 6 | "unicode/utf8" 7 | ) 8 | 9 | const ( 10 | // UNKNOWN represents an unknow image type value. 11 | UNKNOWN ImageType = iota 12 | // JPEG represents the JPEG image type. 13 | JPEG 14 | // WEBP represents the WEBP image type. 15 | WEBP 16 | // PNG represents the PNG image type. 17 | PNG 18 | // TIFF represents the TIFF image type. 19 | TIFF 20 | // GIF represents the GIF image type. 21 | GIF 22 | // PDF represents the PDF type. 23 | PDF 24 | // SVG represents the SVG image type. 25 | SVG 26 | // MAGICK represents the libmagick compatible genetic image type. 27 | MAGICK 28 | ) 29 | 30 | // ImageType represents an image type value. 31 | type ImageType int 32 | 33 | var ( 34 | htmlCommentRegex = regexp.MustCompile("(?i)") 35 | svgRegex = regexp.MustCompile(`(?i)^\s*(?:<\?xml[^>]*>\s*)?(?:]*>\s*)?]*>[^*]*<\/svg>\s*$`) 36 | ) 37 | 38 | // ImageTypes stores as pairs of image types supported and its alias names. 39 | var ImageTypes = map[ImageType]string{ 40 | JPEG: "jpeg", 41 | PNG: "png", 42 | WEBP: "webp", 43 | TIFF: "tiff", 44 | GIF: "gif", 45 | PDF: "pdf", 46 | SVG: "svg", 47 | MAGICK: "magick", 48 | } 49 | 50 | // imageMutex is used to provide thread-safe synchronization 51 | // for SupportedImageTypes map. 52 | var imageMutex = &sync.RWMutex{} 53 | 54 | // SupportedImageType represents whether a type can be loaded and/or saved by 55 | // the current libvips compilation. 56 | type SupportedImageType struct { 57 | Load bool 58 | Save bool 59 | } 60 | 61 | // SupportedImageTypes stores the optional image type supported 62 | // by the current libvips compilation. 63 | // Note: lazy evaluation as demand is required due 64 | // to bootstrap runtime limitation with C/libvips world. 65 | var SupportedImageTypes = map[ImageType]SupportedImageType{} 66 | 67 | // discoverSupportedImageTypes is used to fill SupportedImageTypes map. 68 | func discoverSupportedImageTypes() { 69 | imageMutex.Lock() 70 | for imageType := range ImageTypes { 71 | SupportedImageTypes[imageType] = SupportedImageType{ 72 | Load: VipsIsTypeSupported(imageType), 73 | Save: VipsIsTypeSupportedSave(imageType), 74 | } 75 | } 76 | imageMutex.Unlock() 77 | } 78 | 79 | // isBinary checks if the given buffer is a binary file. 80 | func isBinary(buf []byte) bool { 81 | if len(buf) < 24 { 82 | return false 83 | } 84 | for i := 0; i < 24; i++ { 85 | charCode, _ := utf8.DecodeRuneInString(string(buf[i])) 86 | if charCode == 65533 || charCode <= 8 { 87 | return true 88 | } 89 | } 90 | return false 91 | } 92 | 93 | // IsSVGImage returns true if the given buffer is a valid SVG image. 94 | func IsSVGImage(buf []byte) bool { 95 | return !isBinary(buf) && svgRegex.Match(htmlCommentRegex.ReplaceAll(buf, []byte{})) 96 | } 97 | 98 | // DetermineImageType determines the image type format (jpeg, png, webp or tiff) 99 | func DetermineImageType(buf []byte) ImageType { 100 | return vipsImageType(buf) 101 | } 102 | 103 | // DetermineImageTypeName determines the image type format by name (jpeg, png, webp or tiff) 104 | func DetermineImageTypeName(buf []byte) string { 105 | return ImageTypeName(vipsImageType(buf)) 106 | } 107 | 108 | // IsImageTypeSupportedByVips returns true if the given image type 109 | // is supported by current libvips compilation. 110 | func IsImageTypeSupportedByVips(t ImageType) SupportedImageType { 111 | imageMutex.RLock() 112 | 113 | // Discover supported image types and cache the result 114 | itShouldDiscover := len(SupportedImageTypes) == 0 115 | if itShouldDiscover { 116 | imageMutex.RUnlock() 117 | discoverSupportedImageTypes() 118 | } 119 | 120 | // Check if image type is actually supported 121 | supported, ok := SupportedImageTypes[t] 122 | if !itShouldDiscover { 123 | imageMutex.RUnlock() 124 | } 125 | 126 | if ok { 127 | return supported 128 | } 129 | return SupportedImageType{Load: false, Save: false} 130 | } 131 | 132 | // IsTypeSupported checks if a given image type is supported 133 | func IsTypeSupported(t ImageType) bool { 134 | _, ok := ImageTypes[t] 135 | return ok && IsImageTypeSupportedByVips(t).Load 136 | } 137 | 138 | // IsTypeNameSupported checks if a given image type name is supported 139 | func IsTypeNameSupported(t string) bool { 140 | for imageType, name := range ImageTypes { 141 | if name == t { 142 | return IsImageTypeSupportedByVips(imageType).Load 143 | } 144 | } 145 | return false 146 | } 147 | 148 | // IsTypeSupportedSave checks if a given image type is support for saving 149 | func IsTypeSupportedSave(t ImageType) bool { 150 | _, ok := ImageTypes[t] 151 | return ok && IsImageTypeSupportedByVips(t).Save 152 | } 153 | 154 | // IsTypeNameSupportedSave checks if a given image type name is supported for 155 | // saving 156 | func IsTypeNameSupportedSave(t string) bool { 157 | for imageType, name := range ImageTypes { 158 | if name == t { 159 | return IsImageTypeSupportedByVips(imageType).Save 160 | } 161 | } 162 | return false 163 | } 164 | 165 | // ImageTypeName is used to get the human friendly name of an image format. 166 | func ImageTypeName(t ImageType) string { 167 | imageType := ImageTypes[t] 168 | if imageType == "" { 169 | return "unknown" 170 | } 171 | return imageType 172 | } 173 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | ## v1.0.18 / 2017-12-22 3 | 4 | * Merge pull request #216 from Bynder/master 5 | * Merge pull request #208 from mikestead/feature/webp-lossless 6 | * Remove go-debug usage 7 | * refactor(docs): remove codesponsor :( 8 | * fix(options): use float64 type in Options.Threshold 9 | * Merge pull request #206 from tstm/add-trim-options 10 | * Add lossless option for saving webp 11 | * Set the test file to write its own file 12 | * Add the option to use background and threshold options on trim 13 | 14 | ## v1.0.17 / 2017-11-14 15 | 16 | * refactor(resizer): remove fmt statement 17 | * fix(type_test): use string formatting 18 | * Merge pull request #207 from traum-ferienwohnungen/nearest-neighbour 19 | * Add nearest-neighbour interpolation 20 | * Merge pull request #203 from traum-ferienwohnungen/fix_icc_memory_leak 21 | * Fix memory leak on icc_transform 22 | 23 | ## v1.0.16 / 2017-10-30 24 | 25 | * fix(travis): use install directive 26 | * Merge branch 'master' of https://github.com/h2non/bimg 27 | * feat: add Gopkg manifests, move fixtures to testdata, add vendor dependencies 28 | * Merge pull request #202 from openskydoor/openskydoor/fix-build-tag 29 | * fix build tag 30 | * fix(#199): presinstall.sh tarball download URL 31 | 32 | ## v1.0.15 / 2017-10-05 33 | 34 | * Merge pull request #198 from greut/webpload 35 | * Add shrink-on-load for webp. 36 | * Merge pull request #197 from greut/typos 37 | * Small typo. 38 | * feat(docs): add codesponsor 39 | 40 | ## v1.0.14 / 2017-09-12 41 | 42 | * Merge pull request #192 from greut/trim 43 | * Adding trim operation. 44 | * Merge pull request #191 from greut/alpha4 45 | * Update 8.6 to alpha4. 46 | 47 | ## v1.0.13 / 2017-09-11 48 | 49 | * Merge pull request #190 from greut/typos 50 | * Fix typo and small cleanup. 51 | 52 | ## v1.0.12 / 2017-09-10 53 | 54 | * Merge branch '99designs-vips-reduce' 55 | * fix(reduce): resolve conflicts with master 56 | * Use vips reduce when downscaling 57 | 58 | ## v1.0.11 / 2017-09-10 59 | 60 | * feat(#189): allow strip image metadata via bimg.Options.StripMetadata = bool 61 | * fix(resize): code format issue 62 | * refactor(resize): add Go version comment 63 | * refactor(tests): fix minor code formatting issues 64 | * fix(#162): garbage collection fix. split Resize() implementation for Go runtime specific 65 | * feat(travis): add go 1.9 66 | * Merge pull request #183 from greut/autorotate 67 | * Proper handling of the EXIF cases. 68 | * Merge pull request #184 from greut/libvips858 69 | * Merge branch 'master' into libvips858 70 | * Merge pull request #185 from greut/libvips860 71 | * Add libvips 8.6 pre-release 72 | * Update to libvips 8.5.8 73 | * fix(resize): runtime.KeepAlive is only Go 74 | * fix(#159): prevent buf to be freed by the GC before resize function exits 75 | * Merge pull request #171 from greut/fix-170 76 | * Check the length before jumping into buffer. 77 | * Merge pull request #168 from Traum-Ferienwohnungen/icc_transform 78 | * Add option to convert embedded ICC profiles 79 | * Merge pull request #166 from danjou-a/patch-1 80 | * Fix Resize verification value 81 | * Merge pull request #165 from greut/libvips846 82 | * Testing using libvips8.4.6 from Github. 83 | 84 | ## v1.0.10 / 2017-06-25 85 | 86 | * Merge pull request #164 from greut/length 87 | * Add Image.Length() 88 | * Merge pull request #163 from greut/libvips856 89 | * Run libvips 8.5.6 on Travis. 90 | * Merge pull request #161 from henry-blip/master 91 | * Expose vips cache memory management functions. 92 | * feat(docs): add watermark image note in features 93 | 94 | ## v1.0.9 / 2017-05-25 95 | 96 | * Merge pull request #156 from Dynom/SmartCropToGravity 97 | * Adding a test, verifying both ways of enabling SmartCrop work 98 | * Merge pull request #149 from waldophotos/master 99 | * Replacing SmartCrop with a Gravity option 100 | * refactor(docs): v8.4 101 | * Change for older LIBVIPS versions. `vips_bandjoin_const1` is added in libvips 8.2. 102 | * Second try, watermarking memory issue fix 103 | 104 | ## v1.0.8 / 2017-05-18 105 | 106 | * Merge pull request #145 from greut/smartcrop 107 | * Merge pull request #155 from greut/libvips8.5.5 108 | * Update libvips to 8.5.5. 109 | * Adding basic smartcrop support. 110 | * Merge pull request #153 from abracadaber/master 111 | * Added Linux Mint 17.3+ distro names 112 | * feat(docs): add new maintainer notice (thanks to @kirillDanshin) 113 | * Merge pull request #152 from greut/libvips85 114 | * Download latest version of libvips from github. 115 | * Merge pull request #147 from h2non/revert-143-master 116 | * Revert "Fix for memory issue when watermarking images" 117 | * Merge pull request #146 from greut/minor-major 118 | * Merge pull request #143 from waldophotos/master 119 | * Merge pull request #144 from greut/go18 120 | * Fix tests where minor/major were mixed up 121 | * Enabled go 1.8 builds. 122 | * Fix the unref of images, when image isn't transparent 123 | * Fix for memory issue when watermarking images 124 | * feat(docs): add maintainers sections 125 | * Merge pull request #132 from jaume-pinyol/WATERMARK_SUPPORT 126 | * Add support for image watermarks 127 | * Merge pull request #131 from greut/versions 128 | * Running tests on more specific versions. 129 | * refactor(preinstall.sh): remove deprecation notice 130 | * Update preinstall.sh 131 | * fix(requirements): required libvips 7.42 132 | * fix(History): typo 133 | * chore(History): add breaking change note 134 | 135 | ## v1.0.7 / 13-01-2017 136 | 137 | - fix(#128): crop image calculation for missing width or height axis. 138 | - feat: add TIFF save output format (**note**: this introduces a minor interface breaking change in `bimg.IsImageTypeSupportedByVips` auxiliary function). 139 | 140 | ## v1.0.6 / 12-11-2016 141 | 142 | - feat(#118): handle 16-bit PNGs. 143 | - feat(#119): adds JPEG2000 file for the type tests. 144 | - feat(#121): test bimg against multiple libvips versions. 145 | 146 | ## v1.0.5 / 01-10-2016 147 | 148 | - feat(#92): support Extend param with optional background. 149 | - fix(#106): allow image area extraction without explicit x/y axis. 150 | - feat(api): add Extend type with `libvips` enum alias. 151 | 152 | ## v1.0.4 / 29-09-2016 153 | 154 | - fix(#111): safe check of magick image type support. 155 | 156 | ## v1.0.3 / 28-09-2016 157 | 158 | - fix(#95): better image type inference and support check. 159 | - fix(background): pass proper background RGB color for PNG image conversion. 160 | - feat(types): validate supported image types by current `libvips` compilation. 161 | - feat(types): consistent SVG image checking. 162 | - feat(api): add public functions `VipsIsTypeSupported()`, `IsImageTypeSupportedByVips()` and `IsSVGImage()`. 163 | 164 | ## v1.0.2 / 27-09-2016 165 | 166 | - feat(#95): support GIF, SVG and PDF formats. 167 | - fix(#108): auto-width and height calculations now round instead of floor. 168 | 169 | ## v1.0.1 / 22-06-2016 170 | 171 | - fix(#90): Do not not dereference the original image a second time. 172 | 173 | ## v1.0.0 / 21-04-2016 174 | 175 | - refactor(api): breaking changes: normalize public members to follow Go naming idioms. 176 | - feat(version): bump to major version. API contract won't be compromised in `v1`. 177 | - feat(docs): add missing inline godoc documentation. 178 | -------------------------------------------------------------------------------- /image.go: -------------------------------------------------------------------------------- 1 | package bimg 2 | 3 | // Image provides a simple method DSL to transform a given image as byte buffer. 4 | type Image struct { 5 | buffer []byte 6 | } 7 | 8 | // NewImage creates a new Image struct with method DSL. 9 | func NewImage(buf []byte) *Image { 10 | return &Image{buf} 11 | } 12 | 13 | // Resize resizes the image to fixed width and height. 14 | func (i *Image) Resize(width, height int) ([]byte, error) { 15 | options := Options{ 16 | Width: width, 17 | Height: height, 18 | Embed: true, 19 | } 20 | return i.Process(options) 21 | } 22 | 23 | // ForceResize resizes with custom size (aspect ratio won't be maintained). 24 | func (i *Image) ForceResize(width, height int) ([]byte, error) { 25 | options := Options{ 26 | Width: width, 27 | Height: height, 28 | Force: true, 29 | } 30 | return i.Process(options) 31 | } 32 | 33 | // ResizeAndCrop resizes the image to fixed width and height with additional crop transformation. 34 | func (i *Image) ResizeAndCrop(width, height int) ([]byte, error) { 35 | options := Options{ 36 | Width: width, 37 | Height: height, 38 | Embed: true, 39 | Crop: true, 40 | } 41 | return i.Process(options) 42 | } 43 | 44 | // SmartCrop produces a thumbnail aiming at focus on the interesting part. 45 | func (i *Image) SmartCrop(width, height int) ([]byte, error) { 46 | options := Options{ 47 | Width: width, 48 | Height: height, 49 | Crop: true, 50 | Gravity: GravitySmart, 51 | } 52 | return i.Process(options) 53 | } 54 | 55 | // Extract area from the by X/Y axis in the current image. 56 | func (i *Image) Extract(top, left, width, height int) ([]byte, error) { 57 | options := Options{ 58 | Top: top, 59 | Left: left, 60 | AreaWidth: width, 61 | AreaHeight: height, 62 | } 63 | 64 | if top == 0 && left == 0 { 65 | options.Top = -1 66 | } 67 | 68 | return i.Process(options) 69 | } 70 | 71 | func (i *Image) WindowCropFixed(o Options) ([]byte, error) { 72 | image, err := WindowCropFixed(i.buffer, o) 73 | if err != nil { 74 | return nil, err 75 | } 76 | i.buffer = image 77 | return image, nil 78 | } 79 | 80 | // Enlarge enlarges the image by width and height. Aspect ratio is maintained. 81 | func (i *Image) Enlarge(width, height int) ([]byte, error) { 82 | options := Options{ 83 | Width: width, 84 | Height: height, 85 | Enlarge: true, 86 | } 87 | return i.Process(options) 88 | } 89 | 90 | // EnlargeAndCrop enlarges the image by width and height with additional crop transformation. 91 | func (i *Image) EnlargeAndCrop(width, height int) ([]byte, error) { 92 | options := Options{ 93 | Width: width, 94 | Height: height, 95 | Enlarge: true, 96 | Crop: true, 97 | } 98 | return i.Process(options) 99 | } 100 | 101 | // Crop crops the image to the exact size specified. 102 | func (i *Image) Crop(width, height int, gravity Gravity) ([]byte, error) { 103 | options := Options{ 104 | Width: width, 105 | Height: height, 106 | Gravity: gravity, 107 | Crop: true, 108 | } 109 | return i.Process(options) 110 | } 111 | 112 | // CropByWidth crops an image by width only param (auto height). 113 | func (i *Image) CropByWidth(width int) ([]byte, error) { 114 | options := Options{ 115 | Width: width, 116 | Crop: true, 117 | } 118 | return i.Process(options) 119 | } 120 | 121 | // CropByHeight crops an image by height (auto width). 122 | func (i *Image) CropByHeight(height int) ([]byte, error) { 123 | options := Options{ 124 | Height: height, 125 | Crop: true, 126 | } 127 | return i.Process(options) 128 | } 129 | 130 | // Thumbnail creates a thumbnail of the image by the a given width by aspect ratio 4:4. 131 | func (i *Image) Thumbnail(pixels int) ([]byte, error) { 132 | options := Options{ 133 | Width: pixels, 134 | Height: pixels, 135 | Crop: true, 136 | Quality: 95, 137 | } 138 | return i.Process(options) 139 | } 140 | 141 | // Watermark adds text as watermark on the given image. 142 | func (i *Image) Watermark(w Watermark) ([]byte, error) { 143 | options := Options{Watermark: w} 144 | return i.Process(options) 145 | } 146 | 147 | // WatermarkImage adds image as watermark on the given image. 148 | func (i *Image) WatermarkImage(w WatermarkImage) ([]byte, error) { 149 | options := Options{WatermarkImage: w} 150 | return i.Process(options) 151 | } 152 | 153 | // Zoom zooms the image by the given factor. 154 | // You should probably call Extract() before. 155 | func (i *Image) Zoom(factor int) ([]byte, error) { 156 | options := Options{Zoom: factor} 157 | return i.Process(options) 158 | } 159 | 160 | // Rotate rotates the image by given angle degrees (0, 90, 180 or 270). 161 | func (i *Image) Rotate(a Angle) ([]byte, error) { 162 | options := Options{Rotate: a} 163 | return i.Process(options) 164 | } 165 | 166 | // Flip flips the image about the vertical Y axis. 167 | func (i *Image) Flip() ([]byte, error) { 168 | options := Options{Flip: true} 169 | return i.Process(options) 170 | } 171 | 172 | // Flop flops the image about the horizontal X axis. 173 | func (i *Image) Flop() ([]byte, error) { 174 | options := Options{Flop: true} 175 | return i.Process(options) 176 | } 177 | 178 | // Convert converts image to another format. 179 | func (i *Image) Convert(t ImageType) ([]byte, error) { 180 | options := Options{Type: t} 181 | return i.Process(options) 182 | } 183 | 184 | // Colourspace performs a color space conversion bsaed on the given interpretation. 185 | func (i *Image) Colourspace(c Interpretation) ([]byte, error) { 186 | options := Options{Interpretation: c} 187 | return i.Process(options) 188 | } 189 | 190 | // Trim removes the background from the picture. It can result in a 0x0 output 191 | // if the image is all background. 192 | func (i *Image) Trim() ([]byte, error) { 193 | options := Options{Trim: true} 194 | return i.Process(options) 195 | } 196 | 197 | // Process processes the image based on the given transformation options, 198 | // talking with libvips bindings accordingly and returning the resultant 199 | // image buffer. 200 | func (i *Image) Process(o Options) ([]byte, error) { 201 | image, err := Resize(i.buffer, o) 202 | if err != nil { 203 | return nil, err 204 | } 205 | i.buffer = image 206 | return image, nil 207 | } 208 | 209 | // Metadata returns the image metadata (size, alpha channel, profile, EXIF rotation). 210 | func (i *Image) Metadata() (ImageMetadata, error) { 211 | return Metadata(i.buffer) 212 | } 213 | 214 | // Interpretation gets the image interpretation type. 215 | // See: https://jcupitt.github.io/libvips/API/current/VipsImage.html#VipsInterpretation 216 | func (i *Image) Interpretation() (Interpretation, error) { 217 | return ImageInterpretation(i.buffer) 218 | } 219 | 220 | // ColourspaceIsSupported checks if the current image 221 | // color space is supported. 222 | func (i *Image) ColourspaceIsSupported() (bool, error) { 223 | return ColourspaceIsSupported(i.buffer) 224 | } 225 | 226 | // Type returns the image type format (jpeg, png, webp, tiff). 227 | func (i *Image) Type() string { 228 | return DetermineImageTypeName(i.buffer) 229 | } 230 | 231 | // Size returns the image size as form of width and height pixels. 232 | func (i *Image) Size() (ImageSize, error) { 233 | return Size(i.buffer) 234 | } 235 | 236 | // Image returns the current resultant image buffer. 237 | func (i *Image) Image() []byte { 238 | return i.buffer 239 | } 240 | 241 | // Length returns the size in bytes of the image buffer. 242 | func (i *Image) Length() int { 243 | return len(i.buffer) 244 | } 245 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package bimg 2 | 3 | /* 4 | #cgo pkg-config: vips 5 | #include "vips/vips.h" 6 | */ 7 | import "C" 8 | 9 | const ( 10 | // Quality defines the default JPEG quality to be used. 11 | Quality = 80 12 | // MaxSize defines the maximum pixels width or height supported. 13 | MaxSize = 16383 14 | ) 15 | 16 | // Gravity represents the image gravity value. 17 | type Gravity int 18 | 19 | const ( 20 | // GravityCentre represents the centre value used for image gravity orientation. 21 | GravityCentre Gravity = iota 22 | // GravityNorth represents the north value used for image gravity orientation. 23 | GravityNorth 24 | // GravityEast represents the east value used for image gravity orientation. 25 | GravityEast 26 | // GravitySouth represents the south value used for image gravity orientation. 27 | GravitySouth 28 | // GravityWest represents the west value used for image gravity orientation. 29 | GravityWest 30 | // GravitySmart enables libvips Smart Crop algorithm for image gravity orientation. 31 | GravitySmart 32 | ) 33 | 34 | // Interpolator represents the image interpolation value. 35 | type Interpolator int 36 | 37 | const ( 38 | // Bicubic interpolation value. 39 | Bicubic Interpolator = iota 40 | // Bilinear interpolation value. 41 | Bilinear 42 | // Nohalo interpolation value. 43 | Nohalo 44 | // Nearest neighbour interpolation value. 45 | Nearest 46 | ) 47 | 48 | var interpolations = map[Interpolator]string{ 49 | Bicubic: "bicubic", 50 | Bilinear: "bilinear", 51 | Nohalo: "nohalo", 52 | Nearest: "nearest", 53 | } 54 | 55 | func (i Interpolator) String() string { 56 | return interpolations[i] 57 | } 58 | 59 | // Angle represents the image rotation angle value. 60 | type Angle int 61 | 62 | const ( 63 | // D0 represents the rotation angle 0 degrees. 64 | D0 Angle = 0 65 | // D45 represents the rotation angle 90 degrees. 66 | D45 Angle = 45 67 | // D90 represents the rotation angle 90 degrees. 68 | D90 Angle = 90 69 | // D135 represents the rotation angle 90 degrees. 70 | D135 Angle = 135 71 | // D180 represents the rotation angle 180 degrees. 72 | D180 Angle = 180 73 | // D235 represents the rotation angle 235 degrees. 74 | D235 Angle = 235 75 | // D270 represents the rotation angle 270 degrees. 76 | D270 Angle = 270 77 | // D315 represents the rotation angle 180 degrees. 78 | D315 Angle = 315 79 | ) 80 | 81 | // Direction represents the image direction value. 82 | type Direction int 83 | 84 | const ( 85 | // Horizontal represents the orizontal image direction value. 86 | Horizontal Direction = C.VIPS_DIRECTION_HORIZONTAL 87 | // Vertical represents the vertical image direction value. 88 | Vertical Direction = C.VIPS_DIRECTION_VERTICAL 89 | ) 90 | 91 | // Interpretation represents the image interpretation type. 92 | // See: https://jcupitt.github.io/libvips/API/current/VipsImage.html#VipsInterpretation 93 | type Interpretation int 94 | 95 | const ( 96 | // InterpretationError points to the libvips interpretation error type. 97 | InterpretationError Interpretation = C.VIPS_INTERPRETATION_ERROR 98 | // InterpretationMultiband points to its libvips interpretation equivalent type. 99 | InterpretationMultiband Interpretation = C.VIPS_INTERPRETATION_MULTIBAND 100 | // InterpretationBW points to its libvips interpretation equivalent type. 101 | InterpretationBW Interpretation = C.VIPS_INTERPRETATION_B_W 102 | // InterpretationCMYK points to its libvips interpretation equivalent type. 103 | InterpretationCMYK Interpretation = C.VIPS_INTERPRETATION_CMYK 104 | // InterpretationRGB points to its libvips interpretation equivalent type. 105 | InterpretationRGB Interpretation = C.VIPS_INTERPRETATION_RGB 106 | // InterpretationSRGB points to its libvips interpretation equivalent type. 107 | InterpretationSRGB Interpretation = C.VIPS_INTERPRETATION_sRGB 108 | // InterpretationRGB16 points to its libvips interpretation equivalent type. 109 | InterpretationRGB16 Interpretation = C.VIPS_INTERPRETATION_RGB16 110 | // InterpretationGREY16 points to its libvips interpretation equivalent type. 111 | InterpretationGREY16 Interpretation = C.VIPS_INTERPRETATION_GREY16 112 | // InterpretationScRGB points to its libvips interpretation equivalent type. 113 | InterpretationScRGB Interpretation = C.VIPS_INTERPRETATION_scRGB 114 | // InterpretationLAB points to its libvips interpretation equivalent type. 115 | InterpretationLAB Interpretation = C.VIPS_INTERPRETATION_LAB 116 | // InterpretationXYZ points to its libvips interpretation equivalent type. 117 | InterpretationXYZ Interpretation = C.VIPS_INTERPRETATION_XYZ 118 | ) 119 | 120 | // Extend represents the image extend mode, used when the edges 121 | // of an image are extended, you can specify how you want the extension done. 122 | // See: https://jcupitt.github.io/libvips/API/current/libvips-conversion.html#VIPS-EXTEND-BACKGROUND:CAPS 123 | type Extend int 124 | 125 | const ( 126 | // ExtendBlack extend with black (all 0) pixels mode. 127 | ExtendBlack Extend = C.VIPS_EXTEND_BLACK 128 | // ExtendCopy copy the image edges. 129 | ExtendCopy Extend = C.VIPS_EXTEND_COPY 130 | // ExtendRepeat repeat the whole image. 131 | ExtendRepeat Extend = C.VIPS_EXTEND_REPEAT 132 | // ExtendMirror mirror the whole image. 133 | ExtendMirror Extend = C.VIPS_EXTEND_MIRROR 134 | // ExtendWhite extend with white (all bits set) pixels. 135 | ExtendWhite Extend = C.VIPS_EXTEND_WHITE 136 | // ExtendBackground with colour from the background property. 137 | ExtendBackground Extend = C.VIPS_EXTEND_BACKGROUND 138 | // ExtendLast extend with last pixel. 139 | ExtendLast Extend = C.VIPS_EXTEND_LAST 140 | ) 141 | 142 | // WatermarkFont defines the default watermark font to be used. 143 | var WatermarkFont = "sans 10" 144 | 145 | // Color represents a traditional RGB color scheme. 146 | type Color struct { 147 | R, G, B uint8 148 | } 149 | 150 | // ColorBlack is a shortcut to black RGB color representation. 151 | var ColorBlack = Color{0, 0, 0} 152 | 153 | // Watermark represents the text-based watermark supported options. 154 | type Watermark struct { 155 | Width int 156 | DPI int 157 | Margin int 158 | Opacity float32 159 | NoReplicate bool 160 | Text string 161 | Font string 162 | Background Color 163 | } 164 | 165 | // WatermarkImage represents the image-based watermark supported options. 166 | type WatermarkImage struct { 167 | Left int 168 | Top int 169 | Buf []byte 170 | Opacity float32 171 | } 172 | 173 | // GaussianBlur represents the gaussian image transformation values. 174 | type GaussianBlur struct { 175 | Sigma float64 176 | MinAmpl float64 177 | } 178 | 179 | // Sharpen represents the image sharp transformation options. 180 | type Sharpen struct { 181 | Radius int 182 | X1 float64 183 | Y2 float64 184 | Y3 float64 185 | M1 float64 186 | M2 float64 187 | } 188 | 189 | // Options represents the supported image transformation options. 190 | type Options struct { 191 | Height int 192 | Width int 193 | AreaHeight int 194 | AreaWidth int 195 | Top int 196 | Left int 197 | Quality int 198 | Compression int 199 | Zoom int 200 | Crop bool 201 | SmartCrop bool // Deprecated, use: bimg.Options.Gravity = bimg.GravitySmart 202 | Enlarge bool 203 | Embed bool 204 | Flip bool 205 | Flop bool 206 | Force bool 207 | NoAutoRotate bool 208 | NoProfile bool 209 | Interlace bool 210 | StripMetadata bool 211 | Trim bool 212 | Lossless bool 213 | Extend Extend 214 | Rotate Angle 215 | Background Color 216 | Gravity Gravity 217 | Watermark Watermark 218 | WatermarkImage WatermarkImage 219 | Type ImageType 220 | Interpolator Interpolator 221 | Interpretation Interpretation 222 | GaussianBlur GaussianBlur 223 | Sharpen Sharpen 224 | Threshold float64 225 | OutputICC string 226 | } 227 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bimg [![Build Status](https://travis-ci.org/h2non/bimg.svg)](https://travis-ci.org/h2non/bimg) [![GoDoc](https://godoc.org/github.com/h2non/bimg?status.svg)](https://godoc.org/github.com/h2non/bimg) [![Go Report Card](http://goreportcard.com/badge/h2non/bimg)](http://goreportcard.com/report/h2non/bimg) [![Coverage Status](https://coveralls.io/repos/github/h2non/bimg/badge.svg?branch=master)](https://coveralls.io/github/h2non/bimg?branch=master) ![License](https://img.shields.io/badge/license-MIT-blue.svg) 2 | 3 | Small [Go](http://golang.org) package for fast high-level image processing using [libvips](https://github.com/jcupitt/libvips) via C bindings, providing a simple, elegant and fluent [programmatic API](#examples). 4 | 5 | bimg was designed to be a small and efficient library supporting a common set of [image operations](#supported-image-operations) such as crop, resize, rotate, zoom or watermark. It can read JPEG, PNG, WEBP natively, and optionally TIFF, PDF, GIF and SVG formats if `libvips@8.3+` is compiled with proper library bindings. 6 | 7 | bimg is able to output images as JPEG, PNG and WEBP formats, including transparent conversion across them. 8 | 9 | bimg uses internally libvips, a powerful library written in C for image processing which requires a [low memory footprint](https://github.com/jcupitt/libvips/wiki/Speed_and_Memory_Use) 10 | and it's typically 4x faster than using the quickest ImageMagick and GraphicsMagick settings or Go native `image` package, and in some cases it's even 8x faster processing JPEG images. 11 | 12 | If you're looking for an HTTP based image processing solution, see [imaginary](https://github.com/h2non/imaginary). 13 | 14 | bimg was heavily inspired in [sharp](https://github.com/lovell/sharp), its homologous package built for [node.js](http://nodejs.org). bimg is used in production environments processing thousands of images per day. 15 | 16 | **v1 notice**: `bimg` introduces some minor breaking changes in `v1` release. 17 | If you're using `gopkg.in`, you can still rely in the `v0` without worrying about API breaking changes. 18 | 19 | ## Contents 20 | 21 | - [Supported image operations](#supported-image-operations) 22 | - [Prerequisites](#prerequisites) 23 | - [Installation](#installation) 24 | - [Performance](#performance) 25 | - [Benchmark](#benchmark) 26 | - [Examples](#examples) 27 | - [Debugging](#debugging) 28 | - [API](#api) 29 | - [Authors](#authors) 30 | - [Credits](#credits) 31 | 32 | ## Supported image operations 33 | 34 | - Resize 35 | - Enlarge 36 | - Crop (including smart crop support, libvips 8.5+) 37 | - Rotate (with auto-rotate based on EXIF orientation) 38 | - Flip (with auto-flip based on EXIF metadata) 39 | - Flop 40 | - Zoom 41 | - Thumbnail 42 | - Extract area 43 | - Watermark (using text or image) 44 | - Gaussian blur effect 45 | - Custom output color space (RGB, grayscale...) 46 | - Format conversion (with additional quality/compression settings) 47 | - EXIF metadata (size, alpha channel, profile, orientation...) 48 | - Trim (libvips 8.6+) 49 | 50 | ## Prerequisites 51 | 52 | - [libvips](https://github.com/jcupitt/libvips) 7.42+ or 8+ (8.4+ recommended) 53 | - C compatible compiler such as gcc 4.6+ or clang 3.0+ 54 | - Go 1.3+ 55 | 56 | **Note**: `libvips` v8.3+ is required for GIF, PDF and SVG support. 57 | 58 | ## Installation 59 | 60 | ```bash 61 | go get -u gopkg.in/h2non/bimg.v1 62 | ``` 63 | 64 | ### libvips 65 | 66 | Run the following script as `sudo` (supports OSX, Debian/Ubuntu, Redhat, Fedora, Amazon Linux): 67 | ```bash 68 | curl -s https://raw.githubusercontent.com/h2non/bimg/master/preinstall.sh | sudo bash - 69 | ``` 70 | 71 | If you wanna take the advantage of [OpenSlide](http://openslide.org/), simply add `--with-openslide` to enable it: 72 | ```bash 73 | curl -s https://raw.githubusercontent.com/h2non/bimg/master/preinstall.sh | sudo bash -s --with-openslide 74 | ``` 75 | 76 | The [install script](https://github.com/h2non/bimg/blob/master/preinstall.sh) requires `curl` and `pkg-config`. 77 | 78 | ## Performance 79 | 80 | libvips is probably the fastest open source solution for image processing. 81 | Here you can see some performance test comparisons for multiple scenarios: 82 | 83 | - [libvips speed and memory usage](https://github.com/jcupitt/libvips/wiki/Speed-and-memory-use) 84 | 85 | ## Benchmark 86 | 87 | Tested using Go 1.5.1 and libvips-7.42.3 in OSX i7 2.7Ghz 88 | ``` 89 | BenchmarkRotateJpeg-8 20 64686945 ns/op 90 | BenchmarkResizeLargeJpeg-8 20 63390416 ns/op 91 | BenchmarkResizePng-8 100 18147294 ns/op 92 | BenchmarkResizeWebP-8 100 20836741 ns/op 93 | BenchmarkConvertToJpeg-8 100 12831812 ns/op 94 | BenchmarkConvertToPng-8 10 128901422 ns/op 95 | BenchmarkConvertToWebp-8 10 204027990 ns/op 96 | BenchmarkCropJpeg-8 30 59068572 ns/op 97 | BenchmarkCropPng-8 10 117303259 ns/op 98 | BenchmarkCropWebP-8 10 107060659 ns/op 99 | BenchmarkExtractJpeg-8 50 30708919 ns/op 100 | BenchmarkExtractPng-8 3000 595546 ns/op 101 | BenchmarkExtractWebp-8 3000 386379 ns/op 102 | BenchmarkZoomJpeg-8 10 160005424 ns/op 103 | BenchmarkZoomPng-8 30 44561047 ns/op 104 | BenchmarkZoomWebp-8 10 126732678 ns/op 105 | BenchmarkWatermarkJpeg-8 20 79006133 ns/op 106 | BenchmarkWatermarPng-8 200 8197291 ns/op 107 | BenchmarkWatermarWebp-8 30 49360369 ns/op 108 | ``` 109 | 110 | ## Examples 111 | 112 | ```go 113 | import ( 114 | "fmt" 115 | "os" 116 | "gopkg.in/h2non/bimg.v1" 117 | ) 118 | ``` 119 | 120 | #### Resize 121 | 122 | ```go 123 | buffer, err := bimg.Read("image.jpg") 124 | if err != nil { 125 | fmt.Fprintln(os.Stderr, err) 126 | } 127 | 128 | newImage, err := bimg.NewImage(buffer).Resize(800, 600) 129 | if err != nil { 130 | fmt.Fprintln(os.Stderr, err) 131 | } 132 | 133 | size, err := bimg.NewImage(newImage).Size() 134 | if size.Width == 800 && size.Height == 600 { 135 | fmt.Println("The image size is valid") 136 | } 137 | 138 | bimg.Write("new.jpg", newImage) 139 | ``` 140 | 141 | #### Rotate 142 | 143 | ```go 144 | buffer, err := bimg.Read("image.jpg") 145 | if err != nil { 146 | fmt.Fprintln(os.Stderr, err) 147 | } 148 | 149 | newImage, err := bimg.NewImage(buffer).Rotate(90) 150 | if err != nil { 151 | fmt.Fprintln(os.Stderr, err) 152 | } 153 | 154 | bimg.Write("new.jpg", newImage) 155 | ``` 156 | 157 | #### Convert 158 | 159 | ```go 160 | buffer, err := bimg.Read("image.jpg") 161 | if err != nil { 162 | fmt.Fprintln(os.Stderr, err) 163 | } 164 | 165 | newImage, err := bimg.NewImage(buffer).Convert(bimg.PNG) 166 | if err != nil { 167 | fmt.Fprintln(os.Stderr, err) 168 | } 169 | 170 | if bimg.NewImage(newImage).Type() == "png" { 171 | fmt.Fprintln(os.Stderr, "The image was converted into png") 172 | } 173 | ``` 174 | 175 | #### Force resize 176 | 177 | Force resize operation without perserving the aspect ratio: 178 | 179 | ```go 180 | buffer, err := bimg.Read("image.jpg") 181 | if err != nil { 182 | fmt.Fprintln(os.Stderr, err) 183 | } 184 | 185 | newImage, err := bimg.NewImage(buffer).ForceResize(1000, 500) 186 | if err != nil { 187 | fmt.Fprintln(os.Stderr, err) 188 | } 189 | 190 | size := bimg.Size(newImage) 191 | if size.Width != 1000 || size.Height != 500 { 192 | fmt.Fprintln(os.Stderr, "Incorrect image size") 193 | } 194 | ``` 195 | 196 | #### Custom colour space (black & white) 197 | 198 | ```go 199 | buffer, err := bimg.Read("image.jpg") 200 | if err != nil { 201 | fmt.Fprintln(os.Stderr, err) 202 | } 203 | 204 | newImage, err := bimg.NewImage(buffer).Colourspace(bimg.INTERPRETATION_B_W) 205 | if err != nil { 206 | fmt.Fprintln(os.Stderr, err) 207 | } 208 | 209 | colourSpace, _ := bimg.ImageInterpretation(newImage) 210 | if colourSpace != bimg.INTERPRETATION_B_W { 211 | fmt.Fprintln(os.Stderr, "Invalid colour space") 212 | } 213 | ``` 214 | 215 | #### Custom options 216 | 217 | See [Options](https://godoc.org/github.com/h2non/bimg#Options) struct to discover all the available fields 218 | 219 | ```go 220 | options := bimg.Options{ 221 | Width: 800, 222 | Height: 600, 223 | Crop: true, 224 | Quality: 95, 225 | Rotate: 180, 226 | Interlace: true, 227 | } 228 | 229 | buffer, err := bimg.Read("image.jpg") 230 | if err != nil { 231 | fmt.Fprintln(os.Stderr, err) 232 | } 233 | 234 | newImage, err := bimg.NewImage(buffer).Process(options) 235 | if err != nil { 236 | fmt.Fprintln(os.Stderr, err) 237 | } 238 | 239 | bimg.Write("new.jpg", newImage) 240 | ``` 241 | 242 | #### Watermark 243 | 244 | ```go 245 | buffer, err := bimg.Read("image.jpg") 246 | if err != nil { 247 | fmt.Fprintln(os.Stderr, err) 248 | } 249 | 250 | watermark := bimg.Watermark{ 251 | Text: "Chuck Norris (c) 2315", 252 | Opacity: 0.25, 253 | Width: 200, 254 | DPI: 100, 255 | Margin: 150, 256 | Font: "sans bold 12", 257 | Background: bimg.Color{255, 255, 255}, 258 | } 259 | 260 | newImage, err := bimg.NewImage(buffer).Watermark(watermark) 261 | if err != nil { 262 | fmt.Fprintln(os.Stderr, err) 263 | } 264 | 265 | bimg.Write("new.jpg", newImage) 266 | ``` 267 | 268 | #### Fluent interface 269 | 270 | ```go 271 | buffer, err := bimg.Read("image.jpg") 272 | if err != nil { 273 | fmt.Fprintln(os.Stderr, err) 274 | } 275 | 276 | image := bimg.NewImage(buffer) 277 | 278 | // first crop image 279 | _, err := image.CropByWidth(300) 280 | if err != nil { 281 | fmt.Fprintln(os.Stderr, err) 282 | } 283 | 284 | // then flip it 285 | newImage, err := image.Flip() 286 | if err != nil { 287 | fmt.Fprintln(os.Stderr, err) 288 | } 289 | 290 | // save the cropped and flipped image 291 | bimg.Write("new.jpg", newImage) 292 | ``` 293 | 294 | ## Debugging 295 | 296 | Run the process passing the `DEBUG` environment variable 297 | ``` 298 | DEBUG=bimg ./app 299 | ``` 300 | 301 | Enable libvips traces (note that a lot of data will be written in stdout): 302 | ``` 303 | VIPS_TRACE=1 ./app 304 | ``` 305 | 306 | You can also dump a core on failure, as [John Cuppit](https://github.com/jcupitt) said: 307 | ```c 308 | g_log_set_always_fatal( 309 | G_LOG_FLAG_RECURSION | 310 | G_LOG_FLAG_FATAL | 311 | G_LOG_LEVEL_ERROR | 312 | G_LOG_LEVEL_CRITICAL | 313 | G_LOG_LEVEL_WARNING ); 314 | ``` 315 | 316 | Or set the G_DEBUG environment variable: 317 | ``` 318 | export G_DEBUG=fatal-warnings,fatal-criticals 319 | ``` 320 | 321 | ## API 322 | 323 | See [godoc reference](https://godoc.org/github.com/h2non/bimg) for detailed API documentation. 324 | 325 | ## Authors 326 | 327 | - [Tomás Aparicio](https://github.com/h2non) - Original author and architect. 328 | - [Kirill Danshin](https://github.com/kirillDanshin) - Maintainer since April 2017. 329 | 330 | ## Credits 331 | 332 | People who recurrently contributed to improve `bimg` in some way. 333 | 334 | - [John Cupitt](https://github.com/jcupitt) 335 | - [Yoan Blanc](https://github.com/greut) 336 | - [Christophe Eblé](https://github.com/chreble) 337 | - [Brant Fitzsimmons](https://github.com/bfitzsimmons) 338 | - [Thomas Meson](https://github.com/zllak) 339 | 340 | Thank you! 341 | 342 | ## License 343 | 344 | MIT - Tomas Aparicio 345 | 346 | [![views](https://sourcegraph.com/api/repos/github.com/h2non/bimg/.counters/views.svg)](https://sourcegraph.com/github.com/h2non/bimg) 347 | -------------------------------------------------------------------------------- /image_test.go: -------------------------------------------------------------------------------- 1 | package bimg 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "testing" 7 | ) 8 | 9 | func TestImageResize(t *testing.T) { 10 | buf, err := initImage("test.jpg").Resize(300, 240) 11 | if err != nil { 12 | t.Errorf("Cannot process the image: %#v", err) 13 | } 14 | 15 | err = assertSize(buf, 300, 240) 16 | if err != nil { 17 | t.Error(err) 18 | } 19 | 20 | Write("testdata/test_resize_out.jpg", buf) 21 | } 22 | 23 | func TestImageGifResize(t *testing.T) { 24 | _, err := initImage("test.gif").Resize(300, 240) 25 | if err == nil { 26 | t.Errorf("GIF shouldn't be saved within VIPS") 27 | } 28 | } 29 | 30 | func TestImagePdfResize(t *testing.T) { 31 | _, err := initImage("test.pdf").Resize(300, 240) 32 | if err == nil { 33 | t.Errorf("PDF cannot be saved within VIPS") 34 | } 35 | } 36 | 37 | func TestImageSvgResize(t *testing.T) { 38 | _, err := initImage("test.svg").Resize(300, 240) 39 | if err == nil { 40 | t.Errorf("SVG cannot be saved within VIPS") 41 | } 42 | } 43 | 44 | func TestImageGifToJpeg(t *testing.T) { 45 | if VipsMajorVersion >= 8 && VipsMinorVersion > 2 { 46 | i := initImage("test.gif") 47 | options := Options{ 48 | Type: JPEG, 49 | } 50 | buf, err := i.Process(options) 51 | if err != nil { 52 | t.Errorf("Cannot process the image: %#v", err) 53 | } 54 | 55 | Write("testdata/test_gif.jpg", buf) 56 | } 57 | } 58 | 59 | func TestImagePdfToJpeg(t *testing.T) { 60 | if VipsMajorVersion >= 8 && VipsMinorVersion > 2 { 61 | i := initImage("test.pdf") 62 | options := Options{ 63 | Type: JPEG, 64 | } 65 | buf, err := i.Process(options) 66 | if err != nil { 67 | t.Errorf("Cannot process the image: %#v", err) 68 | } 69 | 70 | Write("testdata/test_pdf.jpg", buf) 71 | } 72 | } 73 | 74 | func TestImageSvgToJpeg(t *testing.T) { 75 | if VipsMajorVersion >= 8 && VipsMinorVersion > 2 { 76 | i := initImage("test.svg") 77 | options := Options{ 78 | Type: JPEG, 79 | } 80 | buf, err := i.Process(options) 81 | if err != nil { 82 | t.Errorf("Cannot process the image: %#v", err) 83 | } 84 | 85 | Write("testdata/test_svg.jpg", buf) 86 | } 87 | } 88 | 89 | func TestImageResizeAndCrop(t *testing.T) { 90 | buf, err := initImage("test.jpg").ResizeAndCrop(300, 200) 91 | if err != nil { 92 | t.Errorf("Cannot process the image: %#v", err) 93 | } 94 | 95 | err = assertSize(buf, 300, 200) 96 | if err != nil { 97 | t.Error(err) 98 | } 99 | 100 | Write("testdata/test_resize_crop_out.jpg", buf) 101 | } 102 | 103 | func TestImageExtract(t *testing.T) { 104 | buf, err := initImage("test.jpg").Extract(100, 100, 300, 200) 105 | if err != nil { 106 | t.Errorf("Cannot process the image: %s", err) 107 | } 108 | 109 | err = assertSize(buf, 300, 200) 110 | if err != nil { 111 | t.Error(err) 112 | } 113 | 114 | Write("testdata/test_extract_out.jpg", buf) 115 | } 116 | 117 | func TestImageExtractZero(t *testing.T) { 118 | buf, err := initImage("test.jpg").Extract(0, 0, 300, 200) 119 | if err != nil { 120 | t.Errorf("Cannot process the image: %s", err) 121 | } 122 | 123 | err = assertSize(buf, 300, 200) 124 | if err != nil { 125 | t.Error(err) 126 | } 127 | 128 | Write("testdata/test_extract_zero_out.jpg", buf) 129 | } 130 | 131 | func TestImageEnlarge(t *testing.T) { 132 | buf, err := initImage("test.png").Enlarge(500, 375) 133 | if err != nil { 134 | t.Errorf("Cannot process the image: %#v", err) 135 | } 136 | 137 | err = assertSize(buf, 500, 375) 138 | if err != nil { 139 | t.Error(err) 140 | } 141 | 142 | Write("testdata/test_enlarge_out.jpg", buf) 143 | } 144 | 145 | func TestImageEnlargeAndCrop(t *testing.T) { 146 | buf, err := initImage("test.png").EnlargeAndCrop(800, 480) 147 | if err != nil { 148 | t.Errorf("Cannot process the image: %#v", err) 149 | } 150 | 151 | err = assertSize(buf, 800, 480) 152 | if err != nil { 153 | t.Error(err) 154 | } 155 | 156 | Write("testdata/test_enlarge_crop_out.jpg", buf) 157 | } 158 | 159 | func TestImageCrop(t *testing.T) { 160 | buf, err := initImage("test.jpg").Crop(800, 600, GravityNorth) 161 | if err != nil { 162 | t.Errorf("Cannot process the image: %s", err) 163 | } 164 | 165 | err = assertSize(buf, 800, 600) 166 | if err != nil { 167 | t.Error(err) 168 | } 169 | 170 | Write("testdata/test_crop_out.jpg", buf) 171 | } 172 | 173 | func TestImageCropByWidth(t *testing.T) { 174 | buf, err := initImage("test.jpg").CropByWidth(600) 175 | if err != nil { 176 | t.Errorf("Cannot process the image: %s", err) 177 | } 178 | 179 | err = assertSize(buf, 600, 1050) 180 | if err != nil { 181 | t.Error(err) 182 | } 183 | 184 | Write("testdata/test_crop_width_out.jpg", buf) 185 | } 186 | 187 | func TestImageCropByHeight(t *testing.T) { 188 | buf, err := initImage("test.jpg").CropByHeight(300) 189 | if err != nil { 190 | t.Errorf("Cannot process the image: %s", err) 191 | } 192 | 193 | err = assertSize(buf, 1680, 300) 194 | if err != nil { 195 | t.Error(err) 196 | } 197 | 198 | Write("testdata/test_crop_height_out.jpg", buf) 199 | } 200 | 201 | func TestImageThumbnail(t *testing.T) { 202 | buf, err := initImage("test.jpg").Thumbnail(100) 203 | if err != nil { 204 | t.Errorf("Cannot process the image: %s", err) 205 | } 206 | 207 | err = assertSize(buf, 100, 100) 208 | if err != nil { 209 | t.Error(err) 210 | } 211 | 212 | Write("testdata/test_thumbnail_out.jpg", buf) 213 | } 214 | 215 | func TestImageWatermark(t *testing.T) { 216 | image := initImage("test.jpg") 217 | _, err := image.Crop(800, 600, GravityNorth) 218 | if err != nil { 219 | t.Errorf("Cannot process the image: %#v", err) 220 | } 221 | 222 | buf, err := image.Watermark(Watermark{ 223 | Text: "Copy me if you can", 224 | Opacity: 0.5, 225 | Width: 200, 226 | DPI: 100, 227 | Background: Color{255, 255, 255}, 228 | }) 229 | if err != nil { 230 | t.Error(err) 231 | } 232 | 233 | err = assertSize(buf, 800, 600) 234 | if err != nil { 235 | t.Error(err) 236 | } 237 | 238 | if DetermineImageType(buf) != JPEG { 239 | t.Fatal("Image is not jpeg") 240 | } 241 | 242 | Write("testdata/test_watermark_text_out.jpg", buf) 243 | } 244 | 245 | func TestImageWatermarkWithImage(t *testing.T) { 246 | image := initImage("test.jpg") 247 | watermark, _ := imageBuf("transparent.png") 248 | 249 | _, err := image.Crop(800, 600, GravityNorth) 250 | if err != nil { 251 | t.Errorf("Cannot process the image: %#v", err) 252 | } 253 | 254 | buf, err := image.WatermarkImage(WatermarkImage{Left: 100, Top: 100, Buf: watermark}) 255 | 256 | if err != nil { 257 | t.Error(err) 258 | } 259 | 260 | err = assertSize(buf, 800, 600) 261 | if err != nil { 262 | t.Error(err) 263 | } 264 | 265 | if DetermineImageType(buf) != JPEG { 266 | t.Fatal("Image is not jpeg") 267 | } 268 | 269 | Write("testdata/test_watermark_image_out.jpg", buf) 270 | } 271 | 272 | func TestImageWatermarkNoReplicate(t *testing.T) { 273 | image := initImage("test.jpg") 274 | _, err := image.Crop(800, 600, GravityNorth) 275 | if err != nil { 276 | t.Errorf("Cannot process the image: %s", err) 277 | } 278 | 279 | buf, err := image.Watermark(Watermark{ 280 | Text: "Copy me if you can", 281 | Opacity: 0.5, 282 | Width: 200, 283 | DPI: 100, 284 | NoReplicate: true, 285 | Background: Color{255, 255, 255}, 286 | }) 287 | if err != nil { 288 | t.Error(err) 289 | } 290 | 291 | err = assertSize(buf, 800, 600) 292 | if err != nil { 293 | t.Error(err) 294 | } 295 | 296 | if DetermineImageType(buf) != JPEG { 297 | t.Fatal("Image is not jpeg") 298 | } 299 | 300 | Write("testdata/test_watermark_replicate_out.jpg", buf) 301 | } 302 | 303 | func TestImageZoom(t *testing.T) { 304 | image := initImage("test.jpg") 305 | 306 | _, err := image.Extract(100, 100, 400, 300) 307 | if err != nil { 308 | t.Errorf("Cannot extract the image: %s", err) 309 | } 310 | 311 | buf, err := image.Zoom(1) 312 | if err != nil { 313 | t.Errorf("Cannot process the image: %s", err) 314 | } 315 | 316 | err = assertSize(buf, 800, 600) 317 | if err != nil { 318 | t.Error(err) 319 | } 320 | 321 | Write("testdata/test_zoom_out.jpg", buf) 322 | } 323 | 324 | func TestImageFlip(t *testing.T) { 325 | buf, err := initImage("test.jpg").Flip() 326 | if err != nil { 327 | t.Errorf("Cannot process the image: %#v", err) 328 | } 329 | Write("testdata/test_flip_out.jpg", buf) 330 | } 331 | 332 | func TestImageFlop(t *testing.T) { 333 | buf, err := initImage("test.jpg").Flop() 334 | if err != nil { 335 | t.Errorf("Cannot process the image: %#v", err) 336 | } 337 | Write("testdata/test_flop_out.jpg", buf) 338 | } 339 | 340 | func TestImageRotate(t *testing.T) { 341 | buf, err := initImage("test_flip_out.jpg").Rotate(90) 342 | if err != nil { 343 | t.Errorf("Cannot process the image: %#v", err) 344 | } 345 | Write("testdata/test_image_rotate_out.jpg", buf) 346 | } 347 | 348 | func TestImageConvert(t *testing.T) { 349 | buf, err := initImage("test.jpg").Convert(PNG) 350 | if err != nil { 351 | t.Errorf("Cannot process the image: %#v", err) 352 | } 353 | Write("testdata/test_image_convert_out.png", buf) 354 | } 355 | 356 | func TestTransparentImageConvert(t *testing.T) { 357 | image := initImage("transparent.png") 358 | options := Options{ 359 | Type: JPEG, 360 | Background: Color{255, 255, 255}, 361 | } 362 | buf, err := image.Process(options) 363 | if err != nil { 364 | t.Errorf("Cannot process the image: %#v", err) 365 | } 366 | Write("testdata/test_transparent_image_convert_out.jpg", buf) 367 | } 368 | 369 | func TestImageMetadata(t *testing.T) { 370 | data, err := initImage("test.png").Metadata() 371 | if err != nil { 372 | t.Errorf("Cannot process the image: %#v", err) 373 | } 374 | if data.Alpha != true { 375 | t.Fatal("Invalid alpha channel") 376 | } 377 | if data.Size.Width != 400 { 378 | t.Fatal("Invalid width size") 379 | } 380 | if data.Type != "png" { 381 | t.Fatal("Invalid image type") 382 | } 383 | } 384 | 385 | func TestInterpretation(t *testing.T) { 386 | interpretation, err := initImage("test.jpg").Interpretation() 387 | if err != nil { 388 | t.Errorf("Cannot process the image: %#v", err) 389 | } 390 | if interpretation != InterpretationSRGB { 391 | t.Errorf("Invalid interpretation: %d", interpretation) 392 | } 393 | } 394 | 395 | func TestImageColourspace(t *testing.T) { 396 | tests := []struct { 397 | file string 398 | interpretation Interpretation 399 | }{ 400 | {"test.jpg", InterpretationSRGB}, 401 | {"test.jpg", InterpretationBW}, 402 | } 403 | 404 | for _, test := range tests { 405 | buf, err := initImage(test.file).Colourspace(test.interpretation) 406 | if err != nil { 407 | t.Errorf("Cannot process the image: %#v", err) 408 | } 409 | 410 | interpretation, err := ImageInterpretation(buf) 411 | if interpretation != test.interpretation { 412 | t.Errorf("Invalid colourspace") 413 | } 414 | } 415 | } 416 | 417 | func TestImageColourspaceIsSupported(t *testing.T) { 418 | supported, err := initImage("test.jpg").ColourspaceIsSupported() 419 | if err != nil { 420 | t.Errorf("Cannot process the image: %#v", err) 421 | } 422 | if supported != true { 423 | t.Errorf("Non-supported colourspace") 424 | } 425 | } 426 | 427 | func TestFluentInterface(t *testing.T) { 428 | image := initImage("test.jpg") 429 | _, err := image.CropByWidth(300) 430 | if err != nil { 431 | t.Errorf("Cannot process the image: %#v", err) 432 | } 433 | 434 | _, err = image.Flip() 435 | if err != nil { 436 | t.Errorf("Cannot process the image: %#v", err) 437 | } 438 | 439 | _, err = image.Convert(PNG) 440 | if err != nil { 441 | t.Errorf("Cannot process the image: %#v", err) 442 | } 443 | 444 | data, _ := image.Metadata() 445 | if data.Alpha != false { 446 | t.Fatal("Invalid alpha channel") 447 | } 448 | if data.Size.Width != 300 { 449 | t.Fatal("Invalid width size") 450 | } 451 | if data.Type != "png" { 452 | t.Fatal("Invalid image type") 453 | } 454 | 455 | Write("testdata/test_image_fluent_out.png", image.Image()) 456 | } 457 | 458 | func TestImageSmartCrop(t *testing.T) { 459 | 460 | if !(VipsMajorVersion >= 8 && VipsMinorVersion >= 5) { 461 | t.Skipf("Skipping this test, libvips doesn't meet version requirement %s >= 8.5", VipsVersion) 462 | } 463 | 464 | i := initImage("northern_cardinal_bird.jpg") 465 | buf, err := i.SmartCrop(300, 300) 466 | if err != nil { 467 | t.Errorf("Cannot process the image: %#v", err) 468 | } 469 | 470 | err = assertSize(buf, 300, 300) 471 | if err != nil { 472 | t.Error(err) 473 | } 474 | 475 | Write("testdata/test_smart_crop.jpg", buf) 476 | } 477 | 478 | func TestImageTrim(t *testing.T) { 479 | 480 | if !(VipsMajorVersion >= 8 && VipsMinorVersion >= 6) { 481 | t.Skipf("Skipping this test, libvips doesn't meet version requirement %s >= 8.6", VipsVersion) 482 | } 483 | 484 | i := initImage("transparent.png") 485 | buf, err := i.Trim() 486 | if err != nil { 487 | t.Errorf("Cannot process the image: %#v", err) 488 | } 489 | 490 | err = assertSize(buf, 250, 208) 491 | if err != nil { 492 | t.Errorf("The image wasn't trimmed.") 493 | } 494 | 495 | Write("testdata/transparent_trim.png", buf) 496 | } 497 | 498 | func TestImageTrimParameters(t *testing.T) { 499 | 500 | if !(VipsMajorVersion >= 8 && VipsMinorVersion >= 6) { 501 | t.Skipf("Skipping this test, libvips doesn't meet version requirement %s >= 8.6", VipsVersion) 502 | } 503 | 504 | i := initImage("test.png") 505 | options := Options{ 506 | Trim: true, 507 | Background: Color{0.0, 0.0, 0.0}, 508 | Threshold: 10.0, 509 | } 510 | buf, err := i.Process(options) 511 | if err != nil { 512 | t.Errorf("Cannot process the image: %#v", err) 513 | } 514 | 515 | err = assertSize(buf, 400, 257) 516 | if err != nil { 517 | t.Errorf("The image wasn't trimmed.") 518 | } 519 | 520 | Write("testdata/parameter_trim.png", buf) 521 | } 522 | 523 | func TestImageLength(t *testing.T) { 524 | i := initImage("test.jpg") 525 | 526 | actual := i.Length() 527 | expected := 53653 528 | 529 | if expected != actual { 530 | t.Errorf("Size in Bytes of the image doesn't correspond. %d != %d", expected, actual) 531 | } 532 | } 533 | 534 | func initImage(file string) *Image { 535 | buf, _ := imageBuf(file) 536 | return NewImage(buf) 537 | } 538 | 539 | func imageBuf(file string) ([]byte, error) { 540 | return Read(path.Join("testdata", file)) 541 | } 542 | 543 | func assertSize(buf []byte, width, height int) error { 544 | size, err := NewImage(buf).Size() 545 | if err != nil { 546 | return err 547 | } 548 | if size.Width != width || size.Height != height { 549 | return fmt.Errorf("Invalid image size: %dx%d", size.Width, size.Height) 550 | } 551 | return nil 552 | } 553 | -------------------------------------------------------------------------------- /preinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | vips_version_minimum=8.7.2 4 | vips_version_latest_major_minor=8.7 5 | vips_version_latest_patch=2 6 | vips_version_full="$vips_version_latest_major_minor.$vips_version_latest_patch" 7 | 8 | openslide_version_minimum=3.4.0 9 | openslide_version_latest_major_minor=3.4 10 | openslide_version_latest_patch=1 11 | 12 | tarbal_url="https://github.com/libvips/libvips/releases/download/v$vips_version_full/vips-$vips_version_full.tar.gz" 13 | 14 | install_libvips_from_source() { 15 | # Download tarball 16 | echo "Compiling libvips v$vips_version_full from source" 17 | curl -L -o vips-$vips_version_full.tar.gz $tarbal_url 18 | tar zvxf vips-$vips_version_full.tar.gz 19 | cd vips-$vips_version_full 20 | 21 | # Compile 22 | CXXFLAGS="-D_GLIBCXX_USE_CXX11_ABI=0" ./configure --disable-debug --disable-docs --disable-static --disable-introspection --disable-dependency-tracking --enable-cxx=yes --without-python --without-orc --without-fftw $1 23 | make 24 | make install 25 | cd .. 26 | rm -rf vips-$vips_version_latest_major_minor.$vips_version_latest_patch 27 | rm vips-$vips_version_latest_major_minor.$vips_version_latest_patch.tar.gz 28 | ldconfig 29 | echo "Installed libvips $(PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig pkg-config --modversion vips)" 30 | } 31 | 32 | install_libopenslide_from_source() { 33 | echo "Compiling openslide $openslide_version_latest_major_minor.$openslide_version_latest_patch from source" 34 | curl -O -L https://github.com/openslide/openslide/releases/download/v$openslide_version_latest_major_minor.$openslide_version_latest_patch/openslide-$openslide_version_latest_major_minor.$openslide_version_latest_patch.tar.gz 35 | tar xzvf openslide-$openslide_version_latest_major_minor.$openslide_version_latest_patch.tar.gz 36 | cd openslide-$openslide_version_latest_major_minor.$openslide_version_latest_patch 37 | PKG_CONFIG_PATH=$pkg_config_path ./configure $1 38 | make 39 | make install 40 | cd .. 41 | rm -rf openslide-$openslide_version_latest_major_minor.$openslide_version_latest_patch 42 | rm openslide-$openslide_version_latest_major_minor.$openslide_version_latest_patch.tar.gz 43 | ldconfig 44 | echo "Installed libopenslide $openslide_version_latest_major_minor.$openslide_version_latest_patch" 45 | } 46 | 47 | sorry() { 48 | echo "Sorry, I don't yet know how to install lib$1 on $2" 49 | exit 1 50 | } 51 | 52 | pkg_config_path="$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig" 53 | 54 | check_if_library_exists() { 55 | PKG_CONFIG_PATH=$pkg_config_path pkg-config --exists $1 56 | if [ $? -eq 0 ]; then 57 | version_found=$(PKG_CONFIG_PATH=$pkg_config_path pkg-config --modversion $1) 58 | PKG_CONFIG_PATH=$pkg_config_path pkg-config --atleast-version=$2 $1 59 | if [ $? -eq 0 ]; then 60 | # Found suitable version of libvips 61 | echo "Found lib$1 $version_found" 62 | return 1 63 | fi 64 | echo "Found lib$1 $version_found but require $2" 65 | else 66 | echo "Could not find lib$1 using a PKG_CONFIG_PATH of '$pkg_config_path'" 67 | fi 68 | return 0 69 | } 70 | 71 | enable_openslide=0 72 | # Is libvips already installed, and is it at least the minimum required version? 73 | if [ $# -eq 1 ]; then 74 | if [ "$1" = "--with-openslide" ]; then 75 | echo "Installing vips with openslide support" 76 | enable_openslide=1 77 | else 78 | echo "Sorry, $1 is not supported. Did you mean --with-openslide?" 79 | exit 1 80 | fi 81 | fi 82 | 83 | if ! type pkg-config >/dev/null; then 84 | sorry "vips" "a system without pkg-config" 85 | fi 86 | 87 | openslide_exists=0 88 | if [ $enable_openslide -eq 1 ]; then 89 | check_if_library_exists "openslide" "$openslide_version_minimum" 90 | openslide_exists=$? 91 | fi 92 | 93 | check_if_library_exists "vips" "$vips_version_minimum" 94 | vips_exists=$? 95 | if [ $vips_exists -eq 1 ] && [ $enable_openslide -eq 1 ]; then 96 | if [ $openslide_exists -eq 1 ]; then 97 | # Check if vips compiled with openslide support 98 | vips_with_openslide=`vips list classes | grep -i opensli` 99 | if [ -z $vips_with_openslide ]; then 100 | echo "Vips compiled without openslide support." 101 | else 102 | exit 0 103 | fi 104 | fi 105 | elif [ $vips_exists -eq 1 ] && [ $enable_openslide -eq 0 ]; then 106 | exit 0 107 | fi 108 | 109 | # Verify root/sudo access 110 | if [ "$(id -u)" -ne "0" ]; then 111 | echo "Sorry, I need root/sudo access to continue" 112 | exit 1 113 | fi 114 | 115 | # Deprecation warning 116 | if [ "$(arch)" == "x86_64" ]; then 117 | echo "This script is no longer required on most 64-bit Linux systems when using sharp v0.12.0+" 118 | fi 119 | 120 | # OS-specific installations of libopenslide follows 121 | # Either openslide does not exist, or vips is installed without openslide support 122 | if [ $enable_openslide -eq 1 ] && [ -z $vips_with_openslide ] && [ $openslide_exists -eq 0 ]; then 123 | if [ -f /etc/debian_version ]; then 124 | # Debian Linux 125 | DISTRO=$(lsb_release -c -s) 126 | echo "Detected Debian Linux '$DISTRO'" 127 | case "$DISTRO" in 128 | jessie|vivid|wily|xenial|stretch) 129 | # Debian 9, Debian 8, Ubuntu 15 130 | echo "Installing libopenslide via apt-get" 131 | apt-get install -y libopenslide-dev 132 | ;; 133 | trusty|utopic|qiana|rebecca|rafaela|freya|rosa|sarah|serena) 134 | # Ubuntu 14, Mint 17+ 135 | echo "Installing libopenslide dependencies via apt-get" 136 | apt-get install -y automake build-essential curl zlib1g-dev libopenjpeg-dev libpng12-dev libjpeg-dev libtiff5-dev libgdk-pixbuf2.0-dev libxml2-dev libsqlite3-dev libcairo2-dev libglib2.0-dev sqlite3 libsqlite3-dev 137 | install_libopenslide_from_source 138 | ;; 139 | precise|wheezy|maya) 140 | # Debian 7, Ubuntu 12.04, Mint 13 141 | echo "Installing libopenslide dependencies via apt-get" 142 | apt-get install -y automake build-essential curl zlib1g-dev libopenjpeg-dev libpng12-dev libjpeg-dev libtiff5-dev libgdk-pixbuf2.0-dev libxml2-dev libsqlite3-dev libcairo2-dev libglib2.0-dev sqlite3 libsqlite3-dev 143 | install_libopenslide_from_source 144 | ;; 145 | *) 146 | # Unsupported Debian-based OS 147 | sorry "openslide" "Debian-based $DISTRO" 148 | ;; 149 | esac 150 | elif [ -f /etc/redhat-release ]; then 151 | # Red Hat Linux 152 | RELEASE=$(cat /etc/redhat-release) 153 | echo "Detected Red Hat Linux '$RELEASE'" 154 | case $RELEASE in 155 | "Red Hat Enterprise Linux release 7."*|"CentOS Linux release 7."*|"Scientific Linux release 7."*) 156 | # RHEL/CentOS 7 157 | echo "Installing libopenslide dependencies via yum" 158 | yum groupinstall -y "Development Tools" 159 | yum install -y tar curl libpng-devel libjpeg-devel libxml2-devel zlib-devel openjpeg-devel libtiff-devel gdk-pixbuf2-devel sqlite-devel cairo-devel glib2-devel expat-devel 160 | install_libopenslide_from_source "--prefix=/usr" 161 | ;; 162 | "Red Hat Enterprise Linux release 6."*|"CentOS release 6."*|"Scientific Linux release 6."*) 163 | # RHEL/CentOS 6 164 | echo "Installing libopenslide dependencies via yum" 165 | yum groupinstall -y "Development Tools" 166 | yum install -y tar curl libpng-devel libjpeg-devel libxml2-devel zlib-devel openjpeg-devel libtiff-devel gdk-pixbuf2-devel sqlite-devel cairo-devel glib2-devel 167 | install_libopenslide_from_source "--prefix=/usr" 168 | ;; 169 | "Fedora release 21 "*|"Fedora release 22 "*) 170 | # Fedora 21, 22 171 | echo "Installing libopenslide via yum" 172 | yum install -y openslide-devel 173 | ;; 174 | *) 175 | # Unsupported RHEL-based OS 176 | sorry "openslide" "$RELEASE" 177 | ;; 178 | esac 179 | elif [ -f /etc/os-release ]; then 180 | RELEASE=$(cat /etc/os-release | grep VERSION) 181 | echo "Detected OpenSuse Linux '$RELEASE'" 182 | case $RELEASE in 183 | *"13.2"*) 184 | echo "Installing libopenslide via zypper" 185 | zypper --gpg-auto-import-keys install -y libopenslide-devel 186 | ;; 187 | esac 188 | elif [ -f /etc/SuSE-brand ]; then 189 | RELEASE=$(cat /etc/SuSE-brand | grep VERSION) 190 | echo "Detected OpenSuse Linux '$RELEASE'" 191 | case $RELEASE in 192 | *"13.1") 193 | echo "Installing libopenslide dependencies via zypper" 194 | zypper --gpg-auto-import-keys install -y --type pattern devel_basis 195 | zypper --gpg-auto-import-keys install -y tar curl libpng16-devel libjpeg-turbo libjpeg8-devel libxml2-devel zlib-devel openjpeg-devel libtiff-devel libgdk_pixbuf-2_0-0 sqlite3-devel cairo-devel glib2-devel 196 | install_libopenslide_from_source 197 | ;; 198 | esac 199 | else 200 | # Unsupported OS 201 | sorry "openslide" "$(uname -a)" 202 | fi 203 | fi 204 | 205 | # OS-specific installations of libvips follows 206 | 207 | if [ -f /etc/debian_version ]; then 208 | # Debian Linux 209 | DISTRO=$(lsb_release -c -s) 210 | echo "Detected Debian Linux '$DISTRO'" 211 | case "$DISTRO" in 212 | jessie|trusty|utopic|vivid|wily|xenial|qiana|rebecca|rafaela|freya|rosa|sarah|serena) 213 | # Debian 8, Ubuntu 14.04+, Mint 17+ 214 | echo "Installing libvips dependencies via apt-get" 215 | apt-get install -y automake build-essential gobject-introspection gtk-doc-tools libglib2.0-dev libjpeg-dev libpng12-dev libwebp-dev libtiff5-dev libexif-dev libgsf-1-dev liblcms2-dev libxml2-dev swig libmagickcore-dev curl 216 | install_libvips_from_source 217 | ;; 218 | stretch) 219 | # Debian 9 220 | echo "Installing libvips dependencies via apt-get" 221 | apt-get install -y automake build-essential gobject-introspection gtk-doc-tools libglib2.0-dev libjpeg-dev libpng-dev libwebp-dev libtiff5-dev libexif-dev libgsf-1-dev liblcms2-dev libxml2-dev swig libmagickcore-dev curl 222 | install_libvips_from_source 223 | ;; 224 | precise|wheezy|maya) 225 | # Debian 7, Ubuntu 12.04, Mint 13 226 | echo "Installing libvips dependencies via apt-get" 227 | add-apt-repository -y ppa:lyrasis/precise-backports 228 | apt-get update 229 | apt-get install -y automake build-essential gobject-introspection gtk-doc-tools libglib2.0-dev libjpeg-dev libpng12-dev libwebp-dev libtiff4-dev libexif-dev libgsf-1-dev liblcms2-dev libxml2-dev swig libmagickcore-dev curl 230 | install_libvips_from_source 231 | ;; 232 | *) 233 | # Unsupported Debian-based OS 234 | sorry "vips" "Debian-based $DISTRO" 235 | ;; 236 | esac 237 | elif [ -f /etc/redhat-release ]; then 238 | # Red Hat Linux 239 | RELEASE=$(cat /etc/redhat-release) 240 | echo "Detected Red Hat Linux '$RELEASE'" 241 | case $RELEASE in 242 | "Red Hat Enterprise Linux release 7."*|"CentOS Linux release 7."*|"Scientific Linux release 7."*) 243 | # RHEL/CentOS 7 244 | echo "Installing libvips dependencies via yum" 245 | yum groupinstall -y "Development Tools" 246 | yum install -y tar curl gtk-doc libxml2-devel libjpeg-turbo-devel libpng-devel libtiff-devel libexif-devel libgsf-devel lcms2-devel ImageMagick-devel gobject-introspection-devel libwebp-devel 247 | install_libvips_from_source "--prefix=/usr" 248 | ;; 249 | "Red Hat Enterprise Linux release 6."*|"CentOS release 6."*|"Scientific Linux release 6."*) 250 | # RHEL/CentOS 6 251 | echo "Installing libvips dependencies via yum" 252 | yum groupinstall -y "Development Tools" 253 | yum install -y tar curl gtk-doc libxml2-devel libjpeg-turbo-devel libpng-devel libtiff-devel libexif-devel libgsf-devel lcms-devel ImageMagick-devel 254 | yum install -y http://li.nux.ro/download/nux/dextop/el6/x86_64/nux-dextop-release-0-2.el6.nux.noarch.rpm 255 | yum install -y --enablerepo=nux-dextop gobject-introspection-devel 256 | yum install -y http://rpms.famillecollet.com/enterprise/remi-release-6.rpm 257 | yum install -y --enablerepo=remi libwebp-devel 258 | install_libvips_from_source "--prefix=/usr" 259 | ;; 260 | "Fedora"*) 261 | # Fedora 21, 22, 23 262 | echo "Installing libvips dependencies via yum" 263 | yum groupinstall -y "Development Tools" 264 | yum install -y gcc-c++ gtk-doc libxml2-devel libjpeg-turbo-devel libpng-devel libtiff-devel libexif-devel lcms-devel ImageMagick-devel gobject-introspection-devel libwebp-devel curl 265 | install_libvips_from_source "--prefix=/usr" 266 | ;; 267 | *) 268 | # Unsupported RHEL-based OS 269 | sorry "vips" "$RELEASE" 270 | ;; 271 | esac 272 | elif [ -f /etc/system-release ]; then 273 | # Probably Amazon Linux 274 | RELEASE=$(cat /etc/system-release) 275 | case $RELEASE in 276 | "Amazon Linux AMI release 2015.03"|"Amazon Linux AMI release 2015.09") 277 | # Amazon Linux 278 | echo "Detected '$RELEASE'" 279 | echo "Installing libvips dependencies via yum" 280 | yum groupinstall -y "Development Tools" 281 | yum install -y gtk-doc libxml2-devel libjpeg-turbo-devel libpng-devel libtiff-devel libexif-devel libgsf-devel lcms2-devel ImageMagick-devel gobject-introspection-devel libwebp-devel curl 282 | install_libvips_from_source "--prefix=/usr" 283 | ;; 284 | *) 285 | # Unsupported Amazon Linux version 286 | sorry "vips" "$RELEASE" 287 | ;; 288 | esac 289 | elif [ -f /etc/os-release ]; then 290 | RELEASE=$(cat /etc/os-release | grep VERSION) 291 | echo "Detected OpenSuse Linux '$RELEASE'" 292 | case $RELEASE in 293 | *"13.2"*) 294 | echo "Installing libvips dependencies via zypper" 295 | zypper --gpg-auto-import-keys install -y --type pattern devel_basis 296 | zypper --gpg-auto-import-keys install -y tar curl gtk-doc libxml2-devel libjpeg-turbo libjpeg8-devel libpng16-devel libtiff-devel libexif-devel liblcms2-devel ImageMagick-devel gobject-introspection-devel libwebp-devel 297 | install_libvips_from_source 298 | ;; 299 | esac 300 | elif [ -f /etc/SuSE-brand ]; then 301 | RELEASE=$(cat /etc/SuSE-brand | grep VERSION) 302 | echo "Detected OpenSuse Linux '$RELEASE'" 303 | case $RELEASE in 304 | *"13.1") 305 | echo "Installing libvips dependencies via zypper" 306 | zypper --gpg-auto-import-keys install -y --type pattern devel_basis 307 | zypper --gpg-auto-import-keys install -y tar curl gtk-doc libxml2-devel libjpeg-turbo libjpeg8-devel libpng16-devel libtiff-devel libexif-devel liblcms2-devel ImageMagick-devel gobject-introspection-devel libwebp-devel 308 | install_libvips_from_source 309 | ;; 310 | esac 311 | else 312 | # Unsupported OS 313 | sorry "vips" "$(uname -a)" 314 | fi 315 | -------------------------------------------------------------------------------- /vips.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | /** 8 | * Starting libvips 7.41, VIPS_ANGLE_x has been renamed to VIPS_ANGLE_Dx 9 | * "to help python". So we provide the macro to correctly build for versions 10 | * before 7.41.x. 11 | * https://github.com/jcupitt/libvips/blob/master/ChangeLog#L128 12 | */ 13 | 14 | #if (VIPS_MAJOR_VERSION == 7 && VIPS_MINOR_VERSION < 41) 15 | #define VIPS_ANGLE_D0 VIPS_ANGLE_0 16 | #define VIPS_ANGLE_D90 VIPS_ANGLE_90 17 | #define VIPS_ANGLE_D180 VIPS_ANGLE_180 18 | #define VIPS_ANGLE_D270 VIPS_ANGLE_270 19 | #endif 20 | 21 | #define EXIF_IFD0_ORIENTATION "exif-ifd0-Orientation" 22 | 23 | #define INT_TO_GBOOLEAN(bool) (bool > 0 ? TRUE : FALSE) 24 | 25 | 26 | enum types { 27 | UNKNOWN = 0, 28 | JPEG, 29 | WEBP, 30 | PNG, 31 | TIFF, 32 | GIF, 33 | PDF, 34 | SVG, 35 | MAGICK 36 | }; 37 | 38 | typedef struct { 39 | const char *Text; 40 | const char *Font; 41 | } WatermarkTextOptions; 42 | 43 | typedef struct { 44 | int Width; 45 | int DPI; 46 | int Margin; 47 | int NoReplicate; 48 | float Opacity; 49 | double Background[3]; 50 | } WatermarkOptions; 51 | 52 | typedef struct { 53 | int Left; 54 | int Top; 55 | float Opacity; 56 | } WatermarkImageOptions; 57 | 58 | static unsigned long 59 | has_profile_embed(VipsImage *image) { 60 | return vips_image_get_typeof(image, VIPS_META_ICC_NAME); 61 | } 62 | 63 | static void 64 | remove_profile(VipsImage *image) { 65 | vips_image_remove(image, VIPS_META_ICC_NAME); 66 | } 67 | 68 | static int 69 | has_alpha_channel(VipsImage *image) { 70 | return ( 71 | (image->Bands == 2 && image->Type == VIPS_INTERPRETATION_B_W) || 72 | (image->Bands == 4 && image->Type != VIPS_INTERPRETATION_CMYK) || 73 | (image->Bands == 5 && image->Type == VIPS_INTERPRETATION_CMYK) 74 | ) ? 1 : 0; 75 | } 76 | 77 | /** 78 | * This method is here to handle the weird initialization of the vips lib. 79 | * libvips use a macro VIPS_INIT() that call vips__init() in version < 7.41, 80 | * or calls vips_init() in version >= 7.41. 81 | * 82 | * Anyway, it's not possible to build bimg on Debian Jessie with libvips 7.40.x, 83 | * as vips_init() is a macro to VIPS_INIT(), which is also a macro, hence, cgo 84 | * is unable to determine the return type of vips_init(), making the build impossible. 85 | * In order to correctly build bimg, for version < 7.41, we should undef vips_init and 86 | * creates a vips_init() method that calls VIPS_INIT(). 87 | */ 88 | 89 | #if (VIPS_MAJOR_VERSION == 7 && VIPS_MINOR_VERSION < 41) 90 | #undef vips_init 91 | int 92 | vips_init(const char *argv0) 93 | { 94 | return VIPS_INIT(argv0); 95 | } 96 | #endif 97 | 98 | void 99 | vips_enable_cache_set_trace() { 100 | vips_cache_set_trace(TRUE); 101 | } 102 | 103 | int 104 | vips_affine_interpolator(VipsImage *in, VipsImage **out, double a, double b, double c, double d, VipsInterpolate *interpolator) { 105 | return vips_affine(in, out, a, b, c, d, "interpolate", interpolator, NULL); 106 | } 107 | 108 | int 109 | vips_jpegload_buffer_shrink(void *buf, size_t len, VipsImage **out, int shrink) { 110 | return vips_jpegload_buffer(buf, len, out, "shrink", shrink, NULL); 111 | } 112 | 113 | int 114 | vips_webpload_buffer_shrink(void *buf, size_t len, VipsImage **out, int shrink) { 115 | return vips_webpload_buffer(buf, len, out, "shrink", shrink, NULL); 116 | } 117 | 118 | int 119 | vips_flip_bridge(VipsImage *in, VipsImage **out, int direction) { 120 | return vips_flip(in, out, direction, NULL); 121 | } 122 | 123 | int 124 | vips_shrink_bridge(VipsImage *in, VipsImage **out, double xshrink, double yshrink) { 125 | return vips_shrink(in, out, xshrink, yshrink, NULL); 126 | } 127 | 128 | int 129 | vips_reduce_bridge(VipsImage *in, VipsImage **out, double xshrink, double yshrink) { 130 | return vips_reduce(in, out, xshrink, yshrink, NULL); 131 | } 132 | 133 | int 134 | vips_type_find_bridge(int t) { 135 | if (t == GIF) { 136 | return vips_type_find("VipsOperation", "gifload"); 137 | } 138 | if (t == PDF) { 139 | return vips_type_find("VipsOperation", "pdfload"); 140 | } 141 | if (t == TIFF) { 142 | return vips_type_find("VipsOperation", "tiffload"); 143 | } 144 | if (t == SVG) { 145 | return vips_type_find("VipsOperation", "svgload"); 146 | } 147 | if (t == WEBP) { 148 | return vips_type_find("VipsOperation", "webpload"); 149 | } 150 | if (t == PNG) { 151 | return vips_type_find("VipsOperation", "pngload"); 152 | } 153 | if (t == JPEG) { 154 | return vips_type_find("VipsOperation", "jpegload"); 155 | } 156 | if (t == MAGICK) { 157 | return vips_type_find("VipsOperation", "magickload"); 158 | } 159 | return 0; 160 | } 161 | 162 | int 163 | vips_type_find_save_bridge(int t) { 164 | if (t == TIFF) { 165 | return vips_type_find("VipsOperation", "tiffsave_buffer"); 166 | } 167 | if (t == WEBP) { 168 | return vips_type_find("VipsOperation", "webpsave_buffer"); 169 | } 170 | if (t == PNG) { 171 | return vips_type_find("VipsOperation", "pngsave_buffer"); 172 | } 173 | if (t == JPEG) { 174 | return vips_type_find("VipsOperation", "jpegsave_buffer"); 175 | } 176 | return 0; 177 | } 178 | 179 | int 180 | vips_rotate_bimg(VipsImage *in, VipsImage **out, int angle) { 181 | int rotate = VIPS_ANGLE_D0; 182 | 183 | angle %= 360; 184 | 185 | if (angle == 45) { 186 | rotate = VIPS_ANGLE45_D45; 187 | } else if (angle == 90) { 188 | rotate = VIPS_ANGLE_D90; 189 | } else if (angle == 135) { 190 | rotate = VIPS_ANGLE45_D135; 191 | } else if (angle == 180) { 192 | rotate = VIPS_ANGLE_D180; 193 | } else if (angle == 225) { 194 | rotate = VIPS_ANGLE45_D225; 195 | } else if (angle == 270) { 196 | rotate = VIPS_ANGLE_D270; 197 | } else if (angle == 315) { 198 | rotate = VIPS_ANGLE45_D315; 199 | } else { 200 | angle = 0; 201 | } 202 | 203 | if (angle > 0 && angle % 90 != 0) { 204 | return vips_rot45(in, out, "angle", rotate, NULL); 205 | } else { 206 | return vips_rot(in, out, rotate, NULL); 207 | } 208 | } 209 | 210 | int 211 | vips_exif_orientation(VipsImage *image) { 212 | int orientation = 0; 213 | const char *exif; 214 | if ( 215 | vips_image_get_typeof(image, EXIF_IFD0_ORIENTATION) != 0 && 216 | !vips_image_get_string(image, EXIF_IFD0_ORIENTATION, &exif) 217 | ) { 218 | orientation = atoi(&exif[0]); 219 | } 220 | return orientation; 221 | } 222 | 223 | int 224 | interpolator_window_size(char const *name) { 225 | VipsInterpolate *interpolator = vips_interpolate_new(name); 226 | int window_size = vips_interpolate_get_window_size(interpolator); 227 | g_object_unref(interpolator); 228 | return window_size; 229 | } 230 | 231 | const char * 232 | vips_enum_nick_bridge(VipsImage *image) { 233 | return vips_enum_nick(VIPS_TYPE_INTERPRETATION, image->Type); 234 | } 235 | 236 | int 237 | vips_zoom_bridge(VipsImage *in, VipsImage **out, int xfac, int yfac) { 238 | return vips_zoom(in, out, xfac, yfac, NULL); 239 | } 240 | 241 | int 242 | vips_embed_bridge(VipsImage *in, VipsImage **out, int left, int top, int width, int height, int extend, double r, double g, double b) { 243 | if (extend == VIPS_EXTEND_BACKGROUND) { 244 | double background[3] = {r, g, b}; 245 | VipsArrayDouble *vipsBackground = vips_array_double_new(background, 3); 246 | return vips_embed(in, out, left, top, width, height, "extend", extend, "background", vipsBackground, NULL); 247 | } 248 | return vips_embed(in, out, left, top, width, height, "extend", extend, NULL); 249 | } 250 | 251 | int 252 | vips_extract_area_bridge(VipsImage *in, VipsImage **out, int left, int top, int width, int height) { 253 | return vips_extract_area(in, out, left, top, width, height, NULL); 254 | } 255 | 256 | int 257 | vips_resize_bridge(VipsImage *in, VipsImage **out, double scale) { 258 | return vips_resize(in, out, scale, NULL); 259 | } 260 | 261 | int 262 | vips_colourspace_issupported_bridge(VipsImage *in) { 263 | return vips_colourspace_issupported(in) ? 1 : 0; 264 | } 265 | 266 | VipsInterpretation 267 | vips_image_guess_interpretation_bridge(VipsImage *in) { 268 | return vips_image_guess_interpretation(in); 269 | } 270 | 271 | int 272 | vips_colourspace_bridge(VipsImage *in, VipsImage **out, VipsInterpretation space) { 273 | return vips_colourspace(in, out, space, NULL); 274 | } 275 | 276 | int 277 | vips_icc_transform_bridge (VipsImage *in, VipsImage **out, const char *output_icc_profile) { 278 | // `output_icc_profile` represents the absolute path to the output ICC profile file 279 | return vips_icc_transform(in, out, output_icc_profile, "embedded", TRUE, NULL); 280 | } 281 | 282 | int 283 | vips_jpegsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality, int interlace) { 284 | return vips_jpegsave_buffer(in, buf, len, 285 | "strip", INT_TO_GBOOLEAN(strip), 286 | "Q", quality, 287 | "optimize_coding", TRUE, 288 | "interlace", INT_TO_GBOOLEAN(interlace), 289 | NULL 290 | ); 291 | } 292 | 293 | int 294 | vips_pngsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int compression, int quality, int interlace) { 295 | #if (VIPS_MAJOR_VERSION >= 8 || (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 42)) 296 | return vips_pngsave_buffer(in, buf, len, 297 | "strip", INT_TO_GBOOLEAN(strip), 298 | "compression", compression, 299 | "interlace", INT_TO_GBOOLEAN(interlace), 300 | "filter", VIPS_FOREIGN_PNG_FILTER_NONE, 301 | NULL 302 | ); 303 | #else 304 | return vips_pngsave_buffer(in, buf, len, 305 | "strip", INT_TO_GBOOLEAN(strip), 306 | "compression", compression, 307 | "interlace", INT_TO_GBOOLEAN(interlace), 308 | NULL 309 | ); 310 | #endif 311 | } 312 | 313 | int 314 | vips_webpsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality, int lossless) { 315 | return vips_webpsave_buffer(in, buf, len, 316 | "strip", INT_TO_GBOOLEAN(strip), 317 | "Q", quality, 318 | "lossless", INT_TO_GBOOLEAN(lossless), 319 | NULL 320 | ); 321 | } 322 | 323 | int 324 | vips_tiffsave_bridge(VipsImage *in, void **buf, size_t *len) { 325 | #if (VIPS_MAJOR_VERSION >= 8 && VIPS_MINOR_VERSION >= 5) 326 | return vips_tiffsave_buffer(in, buf, len, NULL); 327 | #else 328 | return 0; 329 | #endif 330 | } 331 | 332 | int 333 | vips_is_16bit (VipsInterpretation interpretation) { 334 | return interpretation == VIPS_INTERPRETATION_RGB16 || interpretation == VIPS_INTERPRETATION_GREY16; 335 | } 336 | 337 | int 338 | vips_flatten_background_brigde(VipsImage *in, VipsImage **out, double r, double g, double b) { 339 | if (vips_is_16bit(in->Type)) { 340 | r = 65535 * r / 255; 341 | g = 65535 * g / 255; 342 | b = 65535 * b / 255; 343 | } 344 | 345 | double background[3] = {r, g, b}; 346 | VipsArrayDouble *vipsBackground = vips_array_double_new(background, 3); 347 | 348 | return vips_flatten(in, out, 349 | "background", vipsBackground, 350 | "max_alpha", vips_is_16bit(in->Type) ? 65535.0 : 255.0, 351 | NULL 352 | ); 353 | } 354 | 355 | int 356 | vips_init_image (void *buf, size_t len, int imageType, VipsImage **out) { 357 | int code = 1; 358 | 359 | if (imageType == JPEG) { 360 | code = vips_jpegload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); 361 | } else if (imageType == PNG) { 362 | code = vips_pngload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); 363 | } else if (imageType == WEBP) { 364 | code = vips_webpload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); 365 | } else if (imageType == TIFF) { 366 | code = vips_tiffload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); 367 | #if (VIPS_MAJOR_VERSION >= 8) 368 | #if (VIPS_MINOR_VERSION >= 3) 369 | } else if (imageType == GIF) { 370 | code = vips_gifload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); 371 | } else if (imageType == PDF) { 372 | code = vips_pdfload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); 373 | } else if (imageType == SVG) { 374 | code = vips_svgload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); 375 | #endif 376 | } else if (imageType == MAGICK) { 377 | code = vips_magickload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); 378 | #endif 379 | } 380 | 381 | return code; 382 | } 383 | 384 | int 385 | vips_watermark_replicate (VipsImage *orig, VipsImage *in, VipsImage **out) { 386 | VipsImage *cache = vips_image_new(); 387 | 388 | if ( 389 | vips_replicate(in, &cache, 390 | 1 + orig->Xsize / in->Xsize, 391 | 1 + orig->Ysize / in->Ysize, NULL) || 392 | vips_crop(cache, out, 0, 0, orig->Xsize, orig->Ysize, NULL) 393 | ) { 394 | g_object_unref(cache); 395 | return 1; 396 | } 397 | 398 | g_object_unref(cache); 399 | return 0; 400 | } 401 | 402 | int 403 | vips_watermark(VipsImage *in, VipsImage **out, WatermarkTextOptions *to, WatermarkOptions *o) { 404 | double ones[3] = { 1, 1, 1 }; 405 | 406 | VipsImage *base = vips_image_new(); 407 | VipsImage **t = (VipsImage **) vips_object_local_array(VIPS_OBJECT(base), 10); 408 | t[0] = in; 409 | 410 | // Make the mask. 411 | if ( 412 | vips_text(&t[1], to->Text, 413 | "width", o->Width, 414 | "dpi", o->DPI, 415 | "font", to->Font, 416 | NULL) || 417 | vips_linear1(t[1], &t[2], o->Opacity, 0.0, NULL) || 418 | vips_cast(t[2], &t[3], VIPS_FORMAT_UCHAR, NULL) || 419 | vips_embed(t[3], &t[4], 100, 100, t[3]->Xsize + o->Margin, t[3]->Ysize + o->Margin, NULL) 420 | ) { 421 | g_object_unref(base); 422 | return 1; 423 | } 424 | 425 | // Replicate if necessary 426 | if (o->NoReplicate != 1) { 427 | VipsImage *cache = vips_image_new(); 428 | if (vips_watermark_replicate(t[0], t[4], &cache)) { 429 | g_object_unref(cache); 430 | g_object_unref(base); 431 | return 1; 432 | } 433 | g_object_unref(t[4]); 434 | t[4] = cache; 435 | } 436 | 437 | // Make the constant image to paint the text with. 438 | if ( 439 | vips_black(&t[5], 1, 1, NULL) || 440 | vips_linear(t[5], &t[6], ones, o->Background, 3, NULL) || 441 | vips_cast(t[6], &t[7], VIPS_FORMAT_UCHAR, NULL) || 442 | vips_copy(t[7], &t[8], "interpretation", t[0]->Type, NULL) || 443 | vips_embed(t[8], &t[9], 0, 0, t[0]->Xsize, t[0]->Ysize, "extend", VIPS_EXTEND_COPY, NULL) 444 | ) { 445 | g_object_unref(base); 446 | return 1; 447 | } 448 | 449 | // Blend the mask and text and write to output. 450 | if (vips_ifthenelse(t[4], t[9], t[0], out, "blend", TRUE, NULL)) { 451 | g_object_unref(base); 452 | return 1; 453 | } 454 | 455 | g_object_unref(base); 456 | return 0; 457 | } 458 | 459 | int 460 | vips_gaussblur_bridge(VipsImage *in, VipsImage **out, double sigma, double min_ampl) { 461 | #if (VIPS_MAJOR_VERSION == 7 && VIPS_MINOR_VERSION < 41) 462 | return vips_gaussblur(in, out, (int) sigma, NULL); 463 | #else 464 | return vips_gaussblur(in, out, sigma, NULL, "min_ampl", min_ampl, NULL); 465 | #endif 466 | } 467 | 468 | int 469 | vips_sharpen_bridge(VipsImage *in, VipsImage **out, int radius, double x1, double y2, double y3, double m1, double m2) { 470 | #if (VIPS_MAJOR_VERSION == 7 && VIPS_MINOR_VERSION < 41) 471 | return vips_sharpen(in, out, radius, x1, y2, y3, m1, m2, NULL); 472 | #else 473 | return vips_sharpen(in, out, "radius", radius, "x1", x1, "y2", y2, "y3", y3, "m1", m1, "m2", m2, NULL); 474 | #endif 475 | } 476 | 477 | int 478 | vips_add_band(VipsImage *in, VipsImage **out, double c) { 479 | #if (VIPS_MAJOR_VERSION > 8 || (VIPS_MAJOR_VERSION >= 8 && VIPS_MINOR_VERSION >= 2)) 480 | return vips_bandjoin_const1(in, out, c, NULL); 481 | #else 482 | VipsImage *base = vips_image_new(); 483 | if ( 484 | vips_black(&base, in->Xsize, in->Ysize, NULL) || 485 | vips_linear1(base, &base, 1, c, NULL)) { 486 | g_object_unref(base); 487 | return 1; 488 | } 489 | g_object_unref(base); 490 | return vips_bandjoin2(in, base, out, c, NULL); 491 | #endif 492 | } 493 | 494 | int 495 | vips_watermark_image(VipsImage *in, VipsImage *sub, VipsImage **out, WatermarkImageOptions *o) { 496 | VipsImage *base = vips_image_new(); 497 | VipsImage **t = (VipsImage **) vips_object_local_array(VIPS_OBJECT(base), 10); 498 | 499 | // add in and sub for unreffing and later use 500 | t[0] = in; 501 | t[1] = sub; 502 | 503 | if (has_alpha_channel(in) == 0) { 504 | vips_add_band(in, &t[0], 255.0); 505 | // in is no longer in the array and won't be unreffed, so add it at the end 506 | t[8] = in; 507 | } 508 | 509 | if (has_alpha_channel(sub) == 0) { 510 | vips_add_band(sub, &t[1], 255.0); 511 | // sub is no longer in the array and won't be unreffed, so add it at the end 512 | t[9] = sub; 513 | } 514 | 515 | // Place watermark image in the right place and size it to the size of the 516 | // image that should be watermarked 517 | if ( 518 | vips_embed(t[1], &t[2], o->Left, o->Top, t[0]->Xsize, t[0]->Ysize, NULL)) { 519 | g_object_unref(base); 520 | return 1; 521 | } 522 | 523 | // Create a mask image based on the alpha band from the watermark image 524 | // and place it in the right position 525 | if ( 526 | vips_extract_band(t[1], &t[3], t[1]->Bands - 1, "n", 1, NULL) || 527 | vips_linear1(t[3], &t[4], o->Opacity, 0.0, NULL) || 528 | vips_cast(t[4], &t[5], VIPS_FORMAT_UCHAR, NULL) || 529 | vips_copy(t[5], &t[6], "interpretation", t[0]->Type, NULL) || 530 | vips_embed(t[6], &t[7], o->Left, o->Top, t[0]->Xsize, t[0]->Ysize, NULL)) { 531 | g_object_unref(base); 532 | return 1; 533 | } 534 | 535 | // Blend the mask and watermark image and write to output. 536 | if (vips_ifthenelse(t[7], t[2], t[0], out, "blend", TRUE, NULL)) { 537 | g_object_unref(base); 538 | return 1; 539 | } 540 | 541 | g_object_unref(base); 542 | return 0; 543 | } 544 | 545 | int 546 | vips_smartcrop_bridge(VipsImage *in, VipsImage **out, int width, int height) { 547 | #if (VIPS_MAJOR_VERSION >= 8 && VIPS_MINOR_VERSION >= 5) 548 | return vips_smartcrop(in, out, width, height, NULL); 549 | #else 550 | return 0; 551 | #endif 552 | } 553 | 554 | int vips_find_trim_bridge(VipsImage *in, int *top, int *left, int *width, int *height, double r, double g, double b, double threshold) { 555 | #if (VIPS_MAJOR_VERSION >= 8 && VIPS_MINOR_VERSION >= 6) 556 | if (vips_is_16bit(in->Type)) { 557 | r = 65535 * r / 255; 558 | g = 65535 * g / 255; 559 | b = 65535 * b / 255; 560 | } 561 | 562 | double background[3] = {r, g, b}; 563 | VipsArrayDouble *vipsBackground = vips_array_double_new(background, 3); 564 | return vips_find_trim(in, top, left, width, height, "background", vipsBackground, "threshold", threshold, NULL); 565 | #else 566 | return 0; 567 | #endif 568 | } 569 | -------------------------------------------------------------------------------- /resizer.go: -------------------------------------------------------------------------------- 1 | package bimg 2 | 3 | /* 4 | #cgo pkg-config: vips 5 | #include "vips/vips.h" 6 | */ 7 | import "C" 8 | 9 | import ( 10 | "errors" 11 | "fmt" 12 | "math" 13 | ) 14 | 15 | var ( 16 | ErrExtractAreaParamsRequired = errors.New("extract area width/height params are required") 17 | ) 18 | 19 | // resizer is used to transform a given image as byte buffer 20 | // with the passed options. 21 | func resizer(buf []byte, o Options) ([]byte, error) { 22 | defer C.vips_thread_shutdown() 23 | 24 | image, imageType, err := loadImage(buf) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | // Clone and define default options 30 | o = applyDefaults(o, imageType) 31 | 32 | if !IsTypeSupported(o.Type) { 33 | return nil, errors.New("Unsupported image output type") 34 | } 35 | 36 | // Auto rotate image based on EXIF orientation header 37 | image, rotated, err := rotateAndFlipImage(image, o) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | // If JPEG image, retrieve the buffer 43 | if rotated && imageType == JPEG && !o.NoAutoRotate { 44 | buf, err = getImageBuffer(image) 45 | if err != nil { 46 | return nil, err 47 | } 48 | } 49 | 50 | inWidth := int(image.Xsize) 51 | inHeight := int(image.Ysize) 52 | 53 | // Infer the required operation based on the in/out image sizes for a coherent transformation 54 | normalizeOperation(&o, inWidth, inHeight) 55 | 56 | // image calculations 57 | factor := imageCalculations(&o, inWidth, inHeight) 58 | shrink := calculateShrink(factor, o.Interpolator) 59 | residual := calculateResidual(factor, shrink) 60 | 61 | // Do not enlarge the output if the input width or height 62 | // are already less than the required dimensions 63 | if !o.Enlarge && !o.Force { 64 | if inWidth < o.Width && inHeight < o.Height { 65 | factor = 1.0 66 | shrink = 1 67 | residual = 0 68 | o.Width = inWidth 69 | o.Height = inHeight 70 | } 71 | } 72 | 73 | // Try to use libjpeg/libwebp shrink-on-load 74 | supportsShrinkOnLoad := imageType == WEBP && VipsMajorVersion >= 8 && VipsMinorVersion >= 3 75 | supportsShrinkOnLoad = supportsShrinkOnLoad || imageType == JPEG 76 | if supportsShrinkOnLoad && shrink >= 2 { 77 | tmpImage, factor, err := shrinkOnLoad(buf, image, imageType, factor, shrink) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | image = tmpImage 83 | factor = math.Max(factor, 1.0) 84 | shrink = int(math.Floor(factor)) 85 | residual = float64(shrink) / factor 86 | } 87 | 88 | // Zoom image, if necessary 89 | image, err = zoomImage(image, o.Zoom) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | // Transform image, if necessary 95 | if shouldTransformImage(o, inWidth, inHeight) { 96 | image, err = transformImage(image, o, shrink, residual) 97 | if err != nil { 98 | return nil, err 99 | } 100 | } 101 | 102 | // Apply effects, if necessary 103 | if shouldApplyEffects(o) { 104 | image, err = applyEffects(image, o) 105 | if err != nil { 106 | return nil, err 107 | } 108 | } 109 | 110 | // Add watermark, if necessary 111 | image, err = watermarkImageWithText(image, o.Watermark) 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | // Add watermark, if necessary 117 | image, err = watermarkImageWithAnotherImage(image, o.WatermarkImage) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | // Flatten image on a background, if necessary 123 | image, err = imageFlatten(image, imageType, o) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | return saveImage(image, o) 129 | } 130 | 131 | func loadImage(buf []byte) (*C.VipsImage, ImageType, error) { 132 | if len(buf) == 0 { 133 | return nil, JPEG, errors.New("Image buffer is empty") 134 | } 135 | 136 | image, imageType, err := vipsRead(buf) 137 | if err != nil { 138 | return nil, JPEG, err 139 | } 140 | 141 | return image, imageType, nil 142 | } 143 | 144 | func applyDefaults(o Options, imageType ImageType) Options { 145 | if o.Quality == 0 { 146 | o.Quality = Quality 147 | } 148 | if o.Compression == 0 { 149 | o.Compression = 6 150 | } 151 | if o.Type == 0 { 152 | o.Type = imageType 153 | } 154 | if o.Interpretation == 0 { 155 | o.Interpretation = InterpretationSRGB 156 | } 157 | return o 158 | } 159 | 160 | func saveImage(image *C.VipsImage, o Options) ([]byte, error) { 161 | saveOptions := vipsSaveOptions{ 162 | Quality: o.Quality, 163 | Type: o.Type, 164 | Compression: o.Compression, 165 | Interlace: o.Interlace, 166 | NoProfile: o.NoProfile, 167 | Interpretation: o.Interpretation, 168 | OutputICC: o.OutputICC, 169 | StripMetadata: o.StripMetadata, 170 | Lossless: o.Lossless, 171 | } 172 | // Finally get the resultant buffer 173 | return vipsSave(image, saveOptions) 174 | } 175 | 176 | func normalizeOperation(o *Options, inWidth, inHeight int) { 177 | if !o.Force && !o.Crop && !o.Embed && !o.Enlarge && o.Rotate == 0 && (o.Width > 0 || o.Height > 0) { 178 | o.Force = true 179 | } 180 | } 181 | 182 | func shouldTransformImage(o Options, inWidth, inHeight int) bool { 183 | return o.Force || (o.Width > 0 && o.Width != inWidth) || 184 | (o.Height > 0 && o.Height != inHeight) || o.AreaWidth > 0 || o.AreaHeight > 0 || 185 | o.Trim 186 | } 187 | 188 | func shouldApplyEffects(o Options) bool { 189 | return o.GaussianBlur.Sigma > 0 || o.GaussianBlur.MinAmpl > 0 || o.Sharpen.Radius > 0 && o.Sharpen.Y2 > 0 || o.Sharpen.Y3 > 0 190 | } 191 | 192 | func transformImage(image *C.VipsImage, o Options, shrink int, residual float64) (*C.VipsImage, error) { 193 | var err error 194 | // Use vips_shrink with the integral reduction 195 | if shrink > 1 { 196 | image, residual, err = shrinkImage(image, o, residual, shrink) 197 | if err != nil { 198 | return nil, err 199 | } 200 | } 201 | 202 | residualx, residualy := residual, residual 203 | if o.Force { 204 | residualx = float64(o.Width) / float64(image.Xsize) 205 | residualy = float64(o.Height) / float64(image.Ysize) 206 | } 207 | 208 | if o.Force || residual != 0 { 209 | if residualx < 1 && residualy < 1 { 210 | image, err = vipsReduce(image, 1/residualx, 1/residualy) 211 | } else { 212 | image, err = vipsAffine(image, residualx, residualy, o.Interpolator) 213 | } 214 | if err != nil { 215 | return nil, err 216 | } 217 | } 218 | 219 | if o.Force { 220 | o.Crop = false 221 | o.Embed = false 222 | } 223 | 224 | image, err = extractOrEmbedImage(image, o) 225 | if err != nil { 226 | return nil, err 227 | } 228 | 229 | return image, nil 230 | } 231 | 232 | func applyEffects(image *C.VipsImage, o Options) (*C.VipsImage, error) { 233 | var err error 234 | 235 | if o.GaussianBlur.Sigma > 0 || o.GaussianBlur.MinAmpl > 0 { 236 | image, err = vipsGaussianBlur(image, o.GaussianBlur) 237 | if err != nil { 238 | return nil, err 239 | } 240 | } 241 | 242 | if o.Sharpen.Radius > 0 && o.Sharpen.Y2 > 0 || o.Sharpen.Y3 > 0 { 243 | image, err = vipsSharpen(image, o.Sharpen) 244 | if err != nil { 245 | return nil, err 246 | } 247 | } 248 | 249 | return image, nil 250 | } 251 | 252 | func extractOrEmbedImage(image *C.VipsImage, o Options) (*C.VipsImage, error) { 253 | var err error 254 | inWidth := int(image.Xsize) 255 | inHeight := int(image.Ysize) 256 | 257 | switch { 258 | case o.Gravity == GravitySmart, o.SmartCrop: 259 | image, err = vipsSmartCrop(image, o.Width, o.Height) 260 | break 261 | case o.Crop: 262 | width := int(math.Min(float64(inWidth), float64(o.Width))) 263 | height := int(math.Min(float64(inHeight), float64(o.Height))) 264 | left, top := calculateCrop(inWidth, inHeight, o.Width, o.Height, o.Gravity) 265 | left, top = int(math.Max(float64(left), 0)), int(math.Max(float64(top), 0)) 266 | image, err = vipsExtract(image, left, top, width, height) 267 | break 268 | case o.Embed: 269 | left, top := (o.Width-inWidth)/2, (o.Height-inHeight)/2 270 | image, err = vipsEmbed(image, left, top, o.Width, o.Height, o.Extend, o.Background) 271 | break 272 | case o.Trim: 273 | left, top, width, height, err := vipsTrim(image, o.Background, o.Threshold) 274 | if err == nil { 275 | image, err = vipsExtract(image, left, top, width, height) 276 | } 277 | break 278 | case o.Top != 0 || o.Left != 0 || o.AreaWidth != 0 || o.AreaHeight != 0: 279 | if o.AreaWidth == 0 { 280 | o.AreaWidth = o.Width 281 | } 282 | if o.AreaHeight == 0 { 283 | o.AreaHeight = o.Height 284 | } 285 | if o.AreaWidth == 0 || o.AreaHeight == 0 { 286 | return nil, errors.New("Extract area width/height params are required") 287 | } 288 | image, err = vipsExtract(image, o.Left, o.Top, o.AreaWidth, o.AreaHeight) 289 | break 290 | } 291 | 292 | return image, err 293 | } 294 | 295 | func rotateAndFlipImage(image *C.VipsImage, o Options) (*C.VipsImage, bool, error) { 296 | var err error 297 | var rotated bool 298 | 299 | if o.NoAutoRotate == false { 300 | rotation, flip := calculateRotationAndFlip(image, o.Rotate) 301 | if flip { 302 | o.Flip = flip 303 | } 304 | if rotation > 0 && o.Rotate == 0 { 305 | o.Rotate = rotation 306 | } 307 | } 308 | 309 | if o.Rotate > 0 { 310 | rotated = true 311 | image, err = vipsRotate(image, getAngle(o.Rotate)) 312 | } 313 | 314 | if o.Flip { 315 | rotated = true 316 | image, err = vipsFlip(image, Vertical) 317 | } 318 | 319 | if o.Flop { 320 | rotated = true 321 | image, err = vipsFlip(image, Horizontal) 322 | } 323 | return image, rotated, err 324 | } 325 | 326 | func watermarkImageWithText(image *C.VipsImage, w Watermark) (*C.VipsImage, error) { 327 | if w.Text == "" { 328 | return image, nil 329 | } 330 | 331 | // Defaults 332 | if w.Font == "" { 333 | w.Font = WatermarkFont 334 | } 335 | if w.Width == 0 { 336 | w.Width = int(math.Floor(float64(image.Xsize / 6))) 337 | } 338 | if w.DPI == 0 { 339 | w.DPI = 150 340 | } 341 | if w.Margin == 0 { 342 | w.Margin = w.Width 343 | } 344 | if w.Opacity == 0 { 345 | w.Opacity = 0.25 346 | } else if w.Opacity > 1 { 347 | w.Opacity = 1 348 | } 349 | 350 | image, err := vipsWatermark(image, w) 351 | if err != nil { 352 | return nil, err 353 | } 354 | 355 | return image, nil 356 | } 357 | 358 | func watermarkImageWithAnotherImage(image *C.VipsImage, w WatermarkImage) (*C.VipsImage, error) { 359 | 360 | if len(w.Buf) == 0 { 361 | return image, nil 362 | } 363 | 364 | if w.Opacity == 0.0 { 365 | w.Opacity = 1.0 366 | } 367 | 368 | image, err := vipsDrawWatermark(image, w) 369 | 370 | if err != nil { 371 | return nil, err 372 | } 373 | 374 | return image, nil 375 | } 376 | 377 | func imageFlatten(image *C.VipsImage, imageType ImageType, o Options) (*C.VipsImage, error) { 378 | // Only PNG images are supported for now 379 | if imageType != PNG || o.Background == ColorBlack { 380 | return image, nil 381 | } 382 | return vipsFlattenBackground(image, o.Background) 383 | } 384 | 385 | func zoomImage(image *C.VipsImage, zoom int) (*C.VipsImage, error) { 386 | if zoom == 0 { 387 | return image, nil 388 | } 389 | return vipsZoom(image, zoom+1) 390 | } 391 | 392 | func shrinkImage(image *C.VipsImage, o Options, residual float64, shrink int) (*C.VipsImage, float64, error) { 393 | // Use vips_shrink with the integral reduction 394 | image, err := vipsShrink(image, shrink) 395 | if err != nil { 396 | return nil, 0, err 397 | } 398 | 399 | // Recalculate residual float based on dimensions of required vs shrunk images 400 | residualx := float64(o.Width) / float64(image.Xsize) 401 | residualy := float64(o.Height) / float64(image.Ysize) 402 | 403 | if o.Crop { 404 | residual = math.Max(residualx, residualy) 405 | } else { 406 | residual = math.Min(residualx, residualy) 407 | } 408 | 409 | return image, residual, nil 410 | } 411 | 412 | func shrinkOnLoad(buf []byte, input *C.VipsImage, imageType ImageType, factor float64, shrink int) (*C.VipsImage, float64, error) { 413 | var image *C.VipsImage 414 | var err error 415 | 416 | // Reload input using shrink-on-load 417 | if imageType == JPEG && shrink >= 2 { 418 | shrinkOnLoad := 1 419 | // Recalculate integral shrink and double residual 420 | switch { 421 | case shrink >= 8: 422 | factor = factor / 8 423 | shrinkOnLoad = 8 424 | case shrink >= 4: 425 | factor = factor / 4 426 | shrinkOnLoad = 4 427 | case shrink >= 2: 428 | factor = factor / 2 429 | shrinkOnLoad = 2 430 | } 431 | 432 | image, err = vipsShrinkJpeg(buf, input, shrinkOnLoad) 433 | } else if imageType == WEBP { 434 | image, err = vipsShrinkWebp(buf, input, shrink) 435 | } else { 436 | return nil, 0, fmt.Errorf("%v doesn't support shrink on load", ImageTypeName(imageType)) 437 | } 438 | 439 | return image, factor, err 440 | } 441 | 442 | func imageCalculations(o *Options, inWidth, inHeight int) float64 { 443 | factor := 1.0 444 | xfactor := float64(inWidth) / float64(o.Width) 445 | yfactor := float64(inHeight) / float64(o.Height) 446 | 447 | switch { 448 | // Fixed width and height 449 | case o.Width > 0 && o.Height > 0: 450 | if o.Crop { 451 | factor = math.Min(xfactor, yfactor) 452 | } else { 453 | factor = math.Max(xfactor, yfactor) 454 | } 455 | // Fixed width, auto height 456 | case o.Width > 0: 457 | if o.Crop { 458 | o.Height = inHeight 459 | } else { 460 | factor = xfactor 461 | o.Height = roundFloat(float64(inHeight) / factor) 462 | } 463 | // Fixed height, auto width 464 | case o.Height > 0: 465 | if o.Crop { 466 | o.Width = inWidth 467 | } else { 468 | factor = yfactor 469 | o.Width = roundFloat(float64(inWidth) / factor) 470 | } 471 | // Identity transform 472 | default: 473 | o.Width = inWidth 474 | o.Height = inHeight 475 | break 476 | } 477 | 478 | return factor 479 | } 480 | 481 | func roundFloat(f float64) int { 482 | if f < 0 { 483 | return int(math.Ceil(f - 0.5)) 484 | } 485 | return int(math.Floor(f + 0.5)) 486 | } 487 | 488 | func calculateCrop(inWidth, inHeight, outWidth, outHeight int, gravity Gravity) (int, int) { 489 | left, top := 0, 0 490 | 491 | switch gravity { 492 | case GravityNorth: 493 | left = (inWidth - outWidth + 1) / 2 494 | case GravityEast: 495 | left = inWidth - outWidth 496 | top = (inHeight - outHeight + 1) / 2 497 | case GravitySouth: 498 | left = (inWidth - outWidth + 1) / 2 499 | top = inHeight - outHeight 500 | case GravityWest: 501 | top = (inHeight - outHeight + 1) / 2 502 | default: 503 | left = (inWidth - outWidth + 1) / 2 504 | top = (inHeight - outHeight + 1) / 2 505 | } 506 | 507 | return left, top 508 | } 509 | 510 | func calculateRotationAndFlip(image *C.VipsImage, angle Angle) (Angle, bool) { 511 | rotate := D0 512 | flip := false 513 | 514 | if angle > 0 { 515 | return rotate, flip 516 | } 517 | 518 | switch vipsExifOrientation(image) { 519 | case 6: 520 | rotate = D90 521 | break 522 | case 3: 523 | rotate = D180 524 | break 525 | case 8: 526 | rotate = D270 527 | break 528 | case 2: 529 | flip = true 530 | break // flip 1 531 | case 7: 532 | flip = true 533 | rotate = D270 534 | break // flip 6 535 | case 4: 536 | flip = true 537 | rotate = D180 538 | break // flip 3 539 | case 5: 540 | flip = true 541 | rotate = D90 542 | break // flip 8 543 | } 544 | 545 | return rotate, flip 546 | } 547 | 548 | func calculateShrink(factor float64, i Interpolator) int { 549 | var shrink float64 550 | 551 | // Calculate integral box shrink 552 | windowSize := vipsWindowSize(i.String()) 553 | if factor >= 2 && windowSize > 3 { 554 | // Shrink less, affine more with interpolators that use at least 4x4 pixel window, e.g. bicubic 555 | shrink = float64(math.Floor(factor * 3.0 / windowSize)) 556 | } else { 557 | shrink = math.Floor(factor) 558 | } 559 | 560 | return int(math.Max(shrink, 1)) 561 | } 562 | 563 | func calculateResidual(factor float64, shrink int) float64 { 564 | return float64(shrink) / factor 565 | } 566 | 567 | func getAngle(angle Angle) Angle { 568 | divisor := angle % 90 569 | if divisor != 0 { 570 | angle = angle - divisor 571 | } 572 | return Angle(math.Min(float64(angle), 270)) 573 | } 574 | 575 | func windowcropfixed(buf []byte, o Options) ([]byte, error) { 576 | defer C.vips_thread_shutdown() 577 | 578 | image, imageType, err := loadImage(buf) 579 | if err != nil { 580 | return nil, err 581 | } 582 | 583 | o = applyDefaults(o, imageType) 584 | 585 | // check if we need to crop the image 586 | if o.Left != 0 || o.Top != 0 || int(image.Xsize) != o.AreaWidth || int(image.Ysize) != o.AreaHeight { 587 | left, top, width, height := o.Left, o.Top, o.AreaWidth, o.AreaHeight 588 | // first make sure the cropping area fits the image area 589 | if o.Left < 0 { 590 | left = 0 591 | width += o.Left 592 | } 593 | if o.Top < 0 { 594 | top = 0 595 | height += o.Top 596 | } 597 | if left+width > int(image.Xsize) { 598 | width = int(image.Xsize) - left 599 | } 600 | if top+height > int(image.Ysize) { 601 | height = int(image.Ysize) - top 602 | } 603 | if width > 0 && height > 0 { // sanity check 604 | image, err = vipsExtract(image, left, top, width, height) 605 | if err != nil { 606 | return nil, err 607 | } 608 | } 609 | // in case the window area was exceeding the image boundaries, embed the image in the window area 610 | if int(image.Xsize) < o.AreaWidth || int(image.Ysize) < o.AreaHeight { 611 | left, top := 0, 0 // where to put the cropped image 612 | if o.Left < 0 { 613 | left = -o.Left 614 | } 615 | if o.Top < 0 { 616 | top = -o.Top 617 | } 618 | image, err = vipsEmbed(image, left, top, o.AreaWidth, o.AreaHeight, o.Extend, o.Background) 619 | if err != nil { 620 | return nil, err 621 | } 622 | } 623 | } 624 | 625 | // now we have the cropped image of o.AreaWidth, o.AreaHeight size; resize it to match the target size 626 | inWidth := int(image.Xsize) 627 | inHeight := int(image.Ysize) 628 | if inWidth != o.Width || inHeight != o.Height { 629 | // check if we need to scale the image 630 | if !((inWidth == o.Width && inHeight < o.Height) || 631 | (inHeight == o.Height && inWidth < o.Width)) { 632 | scaleX, scaleY := float64(o.Width)/float64(inWidth), float64(o.Height)/float64(inHeight) 633 | image, err = vipsResize(image, math.Min(scaleX, scaleY)) 634 | if err != nil { 635 | return nil, err 636 | } 637 | inWidth = int(image.Xsize) 638 | inHeight = int(image.Ysize) 639 | } 640 | 641 | // in case the output image does not match the target area (different aspect ration), embed the image 642 | if inWidth != o.Width || inHeight != o.Height { 643 | left := int(math.Round(float64(o.Width-inWidth) / 2.0)) 644 | top := int(math.Round(float64(o.Height-inHeight) / 2.0)) 645 | image, err = vipsEmbed(image, left, top, o.Width, o.Height, o.Extend, o.Background) 646 | if err != nil { 647 | return nil, err 648 | } 649 | } 650 | 651 | } 652 | 653 | image, err = imageFlatten(image, imageType, o) 654 | if err != nil { 655 | return nil, err 656 | } 657 | 658 | return saveImage(image, o) 659 | } 660 | -------------------------------------------------------------------------------- /vips.go: -------------------------------------------------------------------------------- 1 | package bimg 2 | 3 | /* 4 | #cgo pkg-config: vips 5 | #include "vips.h" 6 | */ 7 | import "C" 8 | 9 | import ( 10 | "errors" 11 | "fmt" 12 | "math" 13 | "os" 14 | "runtime" 15 | "strings" 16 | "sync" 17 | "unsafe" 18 | ) 19 | 20 | // VipsVersion exposes the current libvips semantic version 21 | const VipsVersion = string(C.VIPS_VERSION) 22 | 23 | // VipsMajorVersion exposes the current libvips major version number 24 | const VipsMajorVersion = int(C.VIPS_MAJOR_VERSION) 25 | 26 | // VipsMinorVersion exposes the current libvips minor version number 27 | const VipsMinorVersion = int(C.VIPS_MINOR_VERSION) 28 | 29 | const ( 30 | maxCacheMem = 100 * 1024 * 1024 31 | maxCacheSize = 500 32 | ) 33 | 34 | var ( 35 | m sync.Mutex 36 | initialized bool 37 | ) 38 | 39 | // VipsMemoryInfo represents the memory stats provided by libvips. 40 | type VipsMemoryInfo struct { 41 | Memory int64 42 | MemoryHighwater int64 43 | Allocations int64 44 | } 45 | 46 | // vipsSaveOptions represents the internal option used to talk with libvips. 47 | type vipsSaveOptions struct { 48 | Quality int 49 | Compression int 50 | Type ImageType 51 | Interlace bool 52 | NoProfile bool 53 | StripMetadata bool 54 | Lossless bool 55 | OutputICC string // Absolute path to the output ICC profile 56 | Interpretation Interpretation 57 | } 58 | 59 | type vipsWatermarkOptions struct { 60 | Width C.int 61 | DPI C.int 62 | Margin C.int 63 | NoReplicate C.int 64 | Opacity C.float 65 | Background [3]C.double 66 | } 67 | 68 | type vipsWatermarkImageOptions struct { 69 | Left C.int 70 | Top C.int 71 | Opacity C.float 72 | } 73 | 74 | type vipsWatermarkTextOptions struct { 75 | Text *C.char 76 | Font *C.char 77 | } 78 | 79 | func init() { 80 | Initialize() 81 | } 82 | 83 | // Initialize is used to explicitly start libvips in thread-safe way. 84 | // Only call this function if you have previously turned off libvips. 85 | func Initialize() { 86 | if C.VIPS_MAJOR_VERSION <= 7 && C.VIPS_MINOR_VERSION < 40 { 87 | panic("unsupported libvips version!") 88 | } 89 | 90 | m.Lock() 91 | runtime.LockOSThread() 92 | defer m.Unlock() 93 | defer runtime.UnlockOSThread() 94 | 95 | err := C.vips_init(C.CString("bimg")) 96 | if err != 0 { 97 | panic("unable to start vips!") 98 | } 99 | 100 | // Set libvips cache params 101 | C.vips_cache_set_max_mem(maxCacheMem) 102 | C.vips_cache_set_max(maxCacheSize) 103 | 104 | // Define a custom thread concurrency limit in libvips (this may generate thread-unsafe issues) 105 | // See: https://github.com/jcupitt/libvips/issues/261#issuecomment-92850414 106 | if os.Getenv("VIPS_CONCURRENCY") == "" { 107 | C.vips_concurrency_set(1) 108 | } 109 | 110 | // Enable libvips cache tracing 111 | if os.Getenv("VIPS_TRACE") != "" { 112 | C.vips_enable_cache_set_trace() 113 | } 114 | 115 | initialized = true 116 | } 117 | 118 | // Shutdown is used to shutdown libvips in a thread-safe way. 119 | // You can call this to drop caches as well. 120 | // If libvips was already initialized, the function is no-op 121 | func Shutdown() { 122 | m.Lock() 123 | defer m.Unlock() 124 | 125 | if initialized { 126 | C.vips_shutdown() 127 | initialized = false 128 | } 129 | } 130 | 131 | // VipsCacheSetMaxMem Sets the maximum amount of tracked memory allowed before the vips operation cache 132 | // begins to drop entries. 133 | func VipsCacheSetMaxMem(maxCacheMem int) { 134 | C.vips_cache_set_max_mem(C.size_t(maxCacheMem)) 135 | } 136 | 137 | // VipsCacheSetMax sets the maximum number of operations to keep in the vips operation cache. 138 | func VipsCacheSetMax(maxCacheSize int) { 139 | C.vips_cache_set_max(C.int(maxCacheSize)) 140 | } 141 | 142 | // VipsCacheDropAll drops the vips operation cache, freeing the allocated memory. 143 | func VipsCacheDropAll() { 144 | C.vips_cache_drop_all() 145 | } 146 | 147 | // VipsDebugInfo outputs to stdout libvips collected data. Useful for debugging. 148 | func VipsDebugInfo() { 149 | C.im__print_all() 150 | } 151 | 152 | // VipsMemory gets memory info stats from libvips (cache size, memory allocs...) 153 | func VipsMemory() VipsMemoryInfo { 154 | return VipsMemoryInfo{ 155 | Memory: int64(C.vips_tracked_get_mem()), 156 | MemoryHighwater: int64(C.vips_tracked_get_mem_highwater()), 157 | Allocations: int64(C.vips_tracked_get_allocs()), 158 | } 159 | } 160 | 161 | // VipsIsTypeSupported returns true if the given image type 162 | // is supported by the current libvips compilation. 163 | func VipsIsTypeSupported(t ImageType) bool { 164 | if t == JPEG { 165 | return int(C.vips_type_find_bridge(C.JPEG)) != 0 166 | } 167 | if t == WEBP { 168 | return int(C.vips_type_find_bridge(C.WEBP)) != 0 169 | } 170 | if t == PNG { 171 | return int(C.vips_type_find_bridge(C.PNG)) != 0 172 | } 173 | if t == GIF { 174 | return int(C.vips_type_find_bridge(C.GIF)) != 0 175 | } 176 | if t == PDF { 177 | return int(C.vips_type_find_bridge(C.PDF)) != 0 178 | } 179 | if t == SVG { 180 | return int(C.vips_type_find_bridge(C.SVG)) != 0 181 | } 182 | if t == TIFF { 183 | return int(C.vips_type_find_bridge(C.TIFF)) != 0 184 | } 185 | if t == MAGICK { 186 | return int(C.vips_type_find_bridge(C.MAGICK)) != 0 187 | } 188 | return false 189 | } 190 | 191 | // VipsIsTypeSupportedSave returns true if the given image type 192 | // is supported by the current libvips compilation for the 193 | // save operation. 194 | func VipsIsTypeSupportedSave(t ImageType) bool { 195 | if t == JPEG { 196 | return int(C.vips_type_find_save_bridge(C.JPEG)) != 0 197 | } 198 | if t == WEBP { 199 | return int(C.vips_type_find_save_bridge(C.WEBP)) != 0 200 | } 201 | if t == PNG { 202 | return int(C.vips_type_find_save_bridge(C.PNG)) != 0 203 | } 204 | if t == TIFF { 205 | return int(C.vips_type_find_save_bridge(C.TIFF)) != 0 206 | } 207 | return false 208 | } 209 | 210 | func vipsExifOrientation(image *C.VipsImage) int { 211 | return int(C.vips_exif_orientation(image)) 212 | } 213 | 214 | func vipsHasAlpha(image *C.VipsImage) bool { 215 | return int(C.has_alpha_channel(image)) > 0 216 | } 217 | 218 | func vipsHasProfile(image *C.VipsImage) bool { 219 | return int(C.has_profile_embed(image)) > 0 220 | } 221 | 222 | func vipsWindowSize(name string) float64 { 223 | cname := C.CString(name) 224 | defer C.free(unsafe.Pointer(cname)) 225 | return float64(C.interpolator_window_size(cname)) 226 | } 227 | 228 | func vipsSpace(image *C.VipsImage) string { 229 | return C.GoString(C.vips_enum_nick_bridge(image)) 230 | } 231 | 232 | func vipsRotate(image *C.VipsImage, angle Angle) (*C.VipsImage, error) { 233 | var out *C.VipsImage 234 | defer C.g_object_unref(C.gpointer(image)) 235 | 236 | err := C.vips_rotate_bimg(image, &out, C.int(angle)) 237 | if err != 0 { 238 | return nil, catchVipsError() 239 | } 240 | 241 | return out, nil 242 | } 243 | 244 | func vipsFlip(image *C.VipsImage, direction Direction) (*C.VipsImage, error) { 245 | var out *C.VipsImage 246 | defer C.g_object_unref(C.gpointer(image)) 247 | 248 | err := C.vips_flip_bridge(image, &out, C.int(direction)) 249 | if err != 0 { 250 | return nil, catchVipsError() 251 | } 252 | 253 | return out, nil 254 | } 255 | 256 | func vipsZoom(image *C.VipsImage, zoom int) (*C.VipsImage, error) { 257 | var out *C.VipsImage 258 | defer C.g_object_unref(C.gpointer(image)) 259 | 260 | err := C.vips_zoom_bridge(image, &out, C.int(zoom), C.int(zoom)) 261 | if err != 0 { 262 | return nil, catchVipsError() 263 | } 264 | 265 | return out, nil 266 | } 267 | 268 | func vipsResize(image *C.VipsImage, scale float64) (*C.VipsImage, error) { 269 | var out *C.VipsImage 270 | defer C.g_object_unref(C.gpointer(image)) 271 | 272 | err := C.vips_resize_bridge(image, &out, C.double(scale)) 273 | if err != 0 { 274 | return nil, catchVipsError() 275 | } 276 | 277 | return out, nil 278 | } 279 | 280 | func vipsWatermark(image *C.VipsImage, w Watermark) (*C.VipsImage, error) { 281 | var out *C.VipsImage 282 | 283 | // Defaults 284 | noReplicate := 0 285 | if w.NoReplicate { 286 | noReplicate = 1 287 | } 288 | 289 | text := C.CString(w.Text) 290 | font := C.CString(w.Font) 291 | background := [3]C.double{C.double(w.Background.R), C.double(w.Background.G), C.double(w.Background.B)} 292 | 293 | textOpts := vipsWatermarkTextOptions{text, font} 294 | opts := vipsWatermarkOptions{C.int(w.Width), C.int(w.DPI), C.int(w.Margin), C.int(noReplicate), C.float(w.Opacity), background} 295 | 296 | defer C.free(unsafe.Pointer(text)) 297 | defer C.free(unsafe.Pointer(font)) 298 | 299 | err := C.vips_watermark(image, &out, (*C.WatermarkTextOptions)(unsafe.Pointer(&textOpts)), (*C.WatermarkOptions)(unsafe.Pointer(&opts))) 300 | if err != 0 { 301 | return nil, catchVipsError() 302 | } 303 | 304 | return out, nil 305 | } 306 | 307 | func vipsRead(buf []byte) (*C.VipsImage, ImageType, error) { 308 | var image *C.VipsImage 309 | imageType := vipsImageType(buf) 310 | 311 | if imageType == UNKNOWN { 312 | return nil, UNKNOWN, errors.New("Unsupported image format") 313 | } 314 | 315 | length := C.size_t(len(buf)) 316 | imageBuf := unsafe.Pointer(&buf[0]) 317 | 318 | err := C.vips_init_image(imageBuf, length, C.int(imageType), &image) 319 | if err != 0 { 320 | return nil, UNKNOWN, catchVipsError() 321 | } 322 | 323 | return image, imageType, nil 324 | } 325 | 326 | func vipsColourspaceIsSupportedBuffer(buf []byte) (bool, error) { 327 | image, _, err := vipsRead(buf) 328 | if err != nil { 329 | return false, err 330 | } 331 | C.g_object_unref(C.gpointer(image)) 332 | return vipsColourspaceIsSupported(image), nil 333 | } 334 | 335 | func vipsColourspaceIsSupported(image *C.VipsImage) bool { 336 | return int(C.vips_colourspace_issupported_bridge(image)) == 1 337 | } 338 | 339 | func vipsInterpretationBuffer(buf []byte) (Interpretation, error) { 340 | image, _, err := vipsRead(buf) 341 | if err != nil { 342 | return InterpretationError, err 343 | } 344 | C.g_object_unref(C.gpointer(image)) 345 | return vipsInterpretation(image), nil 346 | } 347 | 348 | func vipsInterpretation(image *C.VipsImage) Interpretation { 349 | return Interpretation(C.vips_image_guess_interpretation_bridge(image)) 350 | } 351 | 352 | func vipsFlattenBackground(image *C.VipsImage, background Color) (*C.VipsImage, error) { 353 | var outImage *C.VipsImage 354 | 355 | backgroundC := [3]C.double{ 356 | C.double(background.R), 357 | C.double(background.G), 358 | C.double(background.B), 359 | } 360 | 361 | if vipsHasAlpha(image) { 362 | err := C.vips_flatten_background_brigde(image, &outImage, 363 | backgroundC[0], backgroundC[1], backgroundC[2]) 364 | if int(err) != 0 { 365 | return nil, catchVipsError() 366 | } 367 | C.g_object_unref(C.gpointer(image)) 368 | image = outImage 369 | } 370 | 371 | return image, nil 372 | } 373 | 374 | func vipsPreSave(image *C.VipsImage, o *vipsSaveOptions) (*C.VipsImage, error) { 375 | var outImage *C.VipsImage 376 | // Remove ICC profile metadata 377 | if o.NoProfile { 378 | C.remove_profile(image) 379 | } 380 | 381 | // Use a default interpretation and cast it to C type 382 | if o.Interpretation == 0 { 383 | o.Interpretation = InterpretationSRGB 384 | } 385 | interpretation := C.VipsInterpretation(o.Interpretation) 386 | 387 | // Apply the proper colour space 388 | if vipsColourspaceIsSupported(image) { 389 | err := C.vips_colourspace_bridge(image, &outImage, interpretation) 390 | if int(err) != 0 { 391 | return nil, catchVipsError() 392 | } 393 | image = outImage 394 | } 395 | 396 | if o.OutputICC != "" && vipsHasProfile(image) { 397 | outputIccPath := C.CString(o.OutputICC) 398 | defer C.free(unsafe.Pointer(outputIccPath)) 399 | 400 | err := C.vips_icc_transform_bridge(image, &outImage, outputIccPath) 401 | if int(err) != 0 { 402 | return nil, catchVipsError() 403 | } 404 | C.g_object_unref(C.gpointer(image)) 405 | image = outImage 406 | } 407 | 408 | return image, nil 409 | } 410 | 411 | func vipsSave(image *C.VipsImage, o vipsSaveOptions) ([]byte, error) { 412 | defer C.g_object_unref(C.gpointer(image)) 413 | 414 | tmpImage, err := vipsPreSave(image, &o) 415 | if err != nil { 416 | return nil, err 417 | } 418 | 419 | // When an image has an unsupported color space, vipsPreSave 420 | // returns the pointer of the image passed to it unmodified. 421 | // When this occurs, we must take care to not dereference the 422 | // original image a second time; we may otherwise erroneously 423 | // free the object twice. 424 | if tmpImage != image { 425 | defer C.g_object_unref(C.gpointer(tmpImage)) 426 | } 427 | 428 | length := C.size_t(0) 429 | saveErr := C.int(0) 430 | interlace := C.int(boolToInt(o.Interlace)) 431 | quality := C.int(o.Quality) 432 | strip := C.int(boolToInt(o.StripMetadata)) 433 | lossless := C.int(boolToInt(o.Lossless)) 434 | 435 | if o.Type != 0 && !IsTypeSupportedSave(o.Type) { 436 | return nil, fmt.Errorf("VIPS cannot save to %#v", ImageTypes[o.Type]) 437 | } 438 | var ptr unsafe.Pointer 439 | switch o.Type { 440 | case WEBP: 441 | saveErr = C.vips_webpsave_bridge(tmpImage, &ptr, &length, strip, quality, lossless) 442 | case PNG: 443 | saveErr = C.vips_pngsave_bridge(tmpImage, &ptr, &length, strip, C.int(o.Compression), quality, interlace) 444 | case TIFF: 445 | saveErr = C.vips_tiffsave_bridge(tmpImage, &ptr, &length) 446 | default: 447 | saveErr = C.vips_jpegsave_bridge(tmpImage, &ptr, &length, strip, quality, interlace) 448 | } 449 | 450 | if int(saveErr) != 0 { 451 | return nil, catchVipsError() 452 | } 453 | 454 | buf := C.GoBytes(ptr, C.int(length)) 455 | 456 | // Clean up 457 | C.g_free(C.gpointer(ptr)) 458 | C.vips_error_clear() 459 | 460 | return buf, nil 461 | } 462 | 463 | func getImageBuffer(image *C.VipsImage) ([]byte, error) { 464 | var ptr unsafe.Pointer 465 | 466 | length := C.size_t(0) 467 | interlace := C.int(0) 468 | quality := C.int(100) 469 | 470 | err := C.int(0) 471 | err = C.vips_jpegsave_bridge(image, &ptr, &length, 1, quality, interlace) 472 | if int(err) != 0 { 473 | return nil, catchVipsError() 474 | } 475 | 476 | defer C.g_free(C.gpointer(ptr)) 477 | defer C.vips_error_clear() 478 | 479 | return C.GoBytes(ptr, C.int(length)), nil 480 | } 481 | 482 | func vipsExtract(image *C.VipsImage, left, top, width, height int) (*C.VipsImage, error) { 483 | var buf *C.VipsImage 484 | defer C.g_object_unref(C.gpointer(image)) 485 | 486 | if width > MaxSize || height > MaxSize { 487 | return nil, errors.New("Maximum image size exceeded") 488 | } 489 | 490 | top, left = max(top), max(left) 491 | err := C.vips_extract_area_bridge(image, &buf, C.int(left), C.int(top), C.int(width), C.int(height)) 492 | if err != 0 { 493 | return nil, catchVipsError() 494 | } 495 | 496 | return buf, nil 497 | } 498 | 499 | func vipsSmartCrop(image *C.VipsImage, width, height int) (*C.VipsImage, error) { 500 | var buf *C.VipsImage 501 | defer C.g_object_unref(C.gpointer(image)) 502 | 503 | if width > MaxSize || height > MaxSize { 504 | return nil, errors.New("Maximum image size exceeded") 505 | } 506 | 507 | err := C.vips_smartcrop_bridge(image, &buf, C.int(width), C.int(height)) 508 | if err != 0 { 509 | return nil, catchVipsError() 510 | } 511 | 512 | return buf, nil 513 | } 514 | 515 | func vipsTrim(image *C.VipsImage, background Color, threshold float64) (int, int, int, int, error) { 516 | defer C.g_object_unref(C.gpointer(image)) 517 | 518 | top := C.int(0) 519 | left := C.int(0) 520 | width := C.int(0) 521 | height := C.int(0) 522 | 523 | err := C.vips_find_trim_bridge(image, 524 | &top, &left, &width, &height, 525 | C.double(background.R), C.double(background.G), C.double(background.B), 526 | C.double(threshold)) 527 | if err != 0 { 528 | return 0, 0, 0, 0, catchVipsError() 529 | } 530 | 531 | return int(top), int(left), int(width), int(height), nil 532 | } 533 | 534 | func vipsShrinkJpeg(buf []byte, input *C.VipsImage, shrink int) (*C.VipsImage, error) { 535 | var image *C.VipsImage 536 | var ptr = unsafe.Pointer(&buf[0]) 537 | defer C.g_object_unref(C.gpointer(input)) 538 | 539 | err := C.vips_jpegload_buffer_shrink(ptr, C.size_t(len(buf)), &image, C.int(shrink)) 540 | if err != 0 { 541 | return nil, catchVipsError() 542 | } 543 | 544 | return image, nil 545 | } 546 | 547 | func vipsShrinkWebp(buf []byte, input *C.VipsImage, shrink int) (*C.VipsImage, error) { 548 | var image *C.VipsImage 549 | var ptr = unsafe.Pointer(&buf[0]) 550 | defer C.g_object_unref(C.gpointer(input)) 551 | 552 | err := C.vips_webpload_buffer_shrink(ptr, C.size_t(len(buf)), &image, C.int(shrink)) 553 | if err != 0 { 554 | return nil, catchVipsError() 555 | } 556 | 557 | return image, nil 558 | } 559 | 560 | func vipsShrink(input *C.VipsImage, shrink int) (*C.VipsImage, error) { 561 | var image *C.VipsImage 562 | defer C.g_object_unref(C.gpointer(input)) 563 | 564 | err := C.vips_shrink_bridge(input, &image, C.double(float64(shrink)), C.double(float64(shrink))) 565 | if err != 0 { 566 | return nil, catchVipsError() 567 | } 568 | 569 | return image, nil 570 | } 571 | 572 | func vipsReduce(input *C.VipsImage, xshrink float64, yshrink float64) (*C.VipsImage, error) { 573 | var image *C.VipsImage 574 | defer C.g_object_unref(C.gpointer(input)) 575 | 576 | err := C.vips_reduce_bridge(input, &image, C.double(xshrink), C.double(yshrink)) 577 | if err != 0 { 578 | return nil, catchVipsError() 579 | } 580 | 581 | return image, nil 582 | } 583 | 584 | func vipsEmbed(input *C.VipsImage, left, top, width, height int, extend Extend, background Color) (*C.VipsImage, error) { 585 | var image *C.VipsImage 586 | 587 | // Max extend value, see: https://jcupitt.github.io/libvips/API/current/libvips-conversion.html#VipsExtend 588 | if extend > 5 { 589 | extend = ExtendBackground 590 | } 591 | 592 | defer C.g_object_unref(C.gpointer(input)) 593 | err := C.vips_embed_bridge(input, &image, C.int(left), C.int(top), C.int(width), 594 | C.int(height), C.int(extend), C.double(background.R), C.double(background.G), C.double(background.B)) 595 | if err != 0 { 596 | return nil, catchVipsError() 597 | } 598 | 599 | return image, nil 600 | } 601 | 602 | func vipsAffine(input *C.VipsImage, residualx, residualy float64, i Interpolator) (*C.VipsImage, error) { 603 | var image *C.VipsImage 604 | cstring := C.CString(i.String()) 605 | interpolator := C.vips_interpolate_new(cstring) 606 | 607 | defer C.free(unsafe.Pointer(cstring)) 608 | defer C.g_object_unref(C.gpointer(input)) 609 | defer C.g_object_unref(C.gpointer(interpolator)) 610 | 611 | err := C.vips_affine_interpolator(input, &image, C.double(residualx), 0, 0, C.double(residualy), interpolator) 612 | if err != 0 { 613 | return nil, catchVipsError() 614 | } 615 | 616 | return image, nil 617 | } 618 | 619 | func vipsImageType(buf []byte) ImageType { 620 | if len(buf) < 12 { 621 | return UNKNOWN 622 | } 623 | if buf[0] == 0xFF && buf[1] == 0xD8 && buf[2] == 0xFF { 624 | return JPEG 625 | } 626 | if IsTypeSupported(GIF) && buf[0] == 0x47 && buf[1] == 0x49 && buf[2] == 0x46 { 627 | return GIF 628 | } 629 | if buf[0] == 0x89 && buf[1] == 0x50 && buf[2] == 0x4E && buf[3] == 0x47 { 630 | return PNG 631 | } 632 | if IsTypeSupported(TIFF) && 633 | ((buf[0] == 0x49 && buf[1] == 0x49 && buf[2] == 0x2A && buf[3] == 0x0) || 634 | (buf[0] == 0x4D && buf[1] == 0x4D && buf[2] == 0x0 && buf[3] == 0x2A)) { 635 | return TIFF 636 | } 637 | if IsTypeSupported(PDF) && buf[0] == 0x25 && buf[1] == 0x50 && buf[2] == 0x44 && buf[3] == 0x46 { 638 | return PDF 639 | } 640 | if IsTypeSupported(WEBP) && buf[8] == 0x57 && buf[9] == 0x45 && buf[10] == 0x42 && buf[11] == 0x50 { 641 | return WEBP 642 | } 643 | if IsTypeSupported(SVG) && IsSVGImage(buf) { 644 | return SVG 645 | } 646 | if IsTypeSupported(MAGICK) && strings.HasSuffix(readImageType(buf), "MagickBuffer") { 647 | return MAGICK 648 | } 649 | 650 | return UNKNOWN 651 | } 652 | 653 | func readImageType(buf []byte) string { 654 | length := C.size_t(len(buf)) 655 | imageBuf := unsafe.Pointer(&buf[0]) 656 | load := C.vips_foreign_find_load_buffer(imageBuf, length) 657 | return C.GoString(load) 658 | } 659 | 660 | func catchVipsError() error { 661 | s := C.GoString(C.vips_error_buffer()) 662 | C.vips_error_clear() 663 | C.vips_thread_shutdown() 664 | return errors.New(s) 665 | } 666 | 667 | func boolToInt(b bool) int { 668 | if b { 669 | return 1 670 | } 671 | return 0 672 | } 673 | 674 | func vipsGaussianBlur(image *C.VipsImage, o GaussianBlur) (*C.VipsImage, error) { 675 | var out *C.VipsImage 676 | defer C.g_object_unref(C.gpointer(image)) 677 | 678 | err := C.vips_gaussblur_bridge(image, &out, C.double(o.Sigma), C.double(o.MinAmpl)) 679 | if err != 0 { 680 | return nil, catchVipsError() 681 | } 682 | return out, nil 683 | } 684 | 685 | func vipsSharpen(image *C.VipsImage, o Sharpen) (*C.VipsImage, error) { 686 | var out *C.VipsImage 687 | defer C.g_object_unref(C.gpointer(image)) 688 | 689 | err := C.vips_sharpen_bridge(image, &out, C.int(o.Radius), C.double(o.X1), C.double(o.Y2), C.double(o.Y3), C.double(o.M1), C.double(o.M2)) 690 | if err != 0 { 691 | return nil, catchVipsError() 692 | } 693 | return out, nil 694 | } 695 | 696 | func max(x int) int { 697 | return int(math.Max(float64(x), 0)) 698 | } 699 | 700 | func vipsDrawWatermark(image *C.VipsImage, o WatermarkImage) (*C.VipsImage, error) { 701 | var out *C.VipsImage 702 | 703 | watermark, _, e := vipsRead(o.Buf) 704 | if e != nil { 705 | return nil, e 706 | } 707 | 708 | opts := vipsWatermarkImageOptions{C.int(o.Left), C.int(o.Top), C.float(o.Opacity)} 709 | 710 | err := C.vips_watermark_image(image, watermark, &out, (*C.WatermarkImageOptions)(unsafe.Pointer(&opts))) 711 | 712 | if err != 0 { 713 | return nil, catchVipsError() 714 | } 715 | 716 | return out, nil 717 | } 718 | -------------------------------------------------------------------------------- /resizer_test.go: -------------------------------------------------------------------------------- 1 | package bimg 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "fmt" 7 | "image" 8 | "image/jpeg" 9 | "io/ioutil" 10 | "os" 11 | "path" 12 | "testing" 13 | ) 14 | 15 | func TestResize(t *testing.T) { 16 | options := Options{Width: 800, Height: 600} 17 | buf, _ := Read("testdata/test.jpg") 18 | 19 | newImg, err := Resize(buf, options) 20 | if err != nil { 21 | t.Errorf("Resize(imgData, %#v) error: %#v", options, err) 22 | } 23 | 24 | if DetermineImageType(newImg) != JPEG { 25 | t.Fatal("Image is not jpeg") 26 | } 27 | 28 | size, _ := Size(newImg) 29 | if size.Height != options.Height || size.Width != options.Width { 30 | t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) 31 | } 32 | 33 | Write("testdata/test_out.jpg", newImg) 34 | } 35 | 36 | func TestWindowCropFixed(t *testing.T) { 37 | buf, _ := Read("testdata/test.jpg") // 1680 x 1050 38 | sourceType := JPEG 39 | tests := []Options{ 40 | {Width: 768, Height: 480, Left: 0, Top: 0, AreaWidth: 1680, AreaHeight: 1050}, // just scale down the image 41 | {Width: 768, Height: 480, Left: 384, Top: 240, AreaWidth: 840, AreaHeight: 525}, // crop and scale down the image 42 | {Width: 768, Height: 800, Left: 384, Top: 240, AreaWidth: 840, AreaHeight: 525}, // vertical padding 43 | {Width: 1024, Height: 480, Left: 384, Top: 240, AreaWidth: 840, AreaHeight: 525}, // horizontal padding 44 | {Width: 768, Height: 768, Left: 1000, Top: 800, AreaWidth: 840, AreaHeight: 525}, // crop outside of the image 45 | } 46 | for i, options := range tests { 47 | options.Quality = 80 48 | image, err := WindowCropFixed(buf, options) 49 | if err != nil { 50 | t.Errorf("Resize(imgData, %#v) error: %#v", options, err) 51 | } 52 | format := DetermineImageType(image) 53 | if format != sourceType { 54 | t.Fatalf("Image format is invalid. Expected: %#v got %v", ImageTypeName(sourceType), ImageTypeName(format)) 55 | } 56 | 57 | size, _ := Size(image) 58 | if options.Height > 0 && size.Height != options.Height { 59 | t.Fatalf("Invalid height: %d (%v)", size.Height, options) 60 | } 61 | if options.Width > 0 && size.Width != options.Width { 62 | t.Fatalf("Invalid width: %d (%v)", size.Width, options) 63 | } 64 | 65 | Write( 66 | fmt.Sprintf( 67 | "testdata/test_windowcropfixed_%d.%s", 68 | i, 69 | ImageTypeName(sourceType)), 70 | image) 71 | } 72 | } 73 | 74 | func TestResizeVerticalImage(t *testing.T) { 75 | tests := []Options{ 76 | {Width: 800, Height: 600}, 77 | {Width: 1000, Height: 1000}, 78 | {Width: 1000, Height: 1500}, 79 | {Width: 1000}, 80 | {Height: 1500}, 81 | {Width: 100, Height: 50}, 82 | {Width: 2000, Height: 2000}, 83 | {Width: 500, Height: 1000}, 84 | {Width: 500}, 85 | {Height: 500}, 86 | {Crop: true, Width: 500, Height: 1000}, 87 | {Crop: true, Enlarge: true, Width: 2000, Height: 1400}, 88 | {Enlarge: true, Force: true, Width: 2000, Height: 2000}, 89 | {Force: true, Width: 2000, Height: 2000}, 90 | } 91 | 92 | bufJpeg, err := Read("testdata/vertical.jpg") 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | bufWebp, err := Read("testdata/vertical.webp") 97 | if err != nil { 98 | t.Fatal(err) 99 | } 100 | 101 | images := []struct { 102 | format ImageType 103 | buf []byte 104 | }{ 105 | {JPEG, bufJpeg}, 106 | {WEBP, bufWebp}, 107 | } 108 | 109 | for _, source := range images { 110 | for _, options := range tests { 111 | image, err := Resize(source.buf, options) 112 | if err != nil { 113 | t.Errorf("Resize(imgData, %#v) error: %#v", options, err) 114 | } 115 | 116 | format := DetermineImageType(image) 117 | if format != source.format { 118 | t.Fatalf("Image format is invalid. Expected: %#v got %v", ImageTypeName(source.format), ImageTypeName(format)) 119 | } 120 | 121 | size, _ := Size(image) 122 | if options.Height > 0 && size.Height != options.Height { 123 | t.Fatalf("Invalid height: %d", size.Height) 124 | } 125 | if options.Width > 0 && size.Width != options.Width { 126 | t.Fatalf("Invalid width: %d", size.Width) 127 | } 128 | 129 | Write( 130 | fmt.Sprintf( 131 | "testdata/test_vertical_%dx%d_out.%s", 132 | options.Width, 133 | options.Height, 134 | ImageTypeName(source.format)), 135 | image) 136 | } 137 | } 138 | } 139 | 140 | func TestResizeCustomSizes(t *testing.T) { 141 | tests := []Options{ 142 | {Width: 800, Height: 600}, 143 | {Width: 1000, Height: 1000}, 144 | {Width: 100, Height: 50}, 145 | {Width: 2000, Height: 2000}, 146 | {Width: 500, Height: 1000}, 147 | {Width: 500}, 148 | {Height: 500}, 149 | {Crop: true, Width: 500, Height: 1000}, 150 | {Crop: true, Enlarge: true, Width: 2000, Height: 1400}, 151 | {Enlarge: true, Force: true, Width: 2000, Height: 2000}, 152 | {Force: true, Width: 2000, Height: 2000}, 153 | } 154 | 155 | bufJpeg, err := Read("testdata/test.jpg") 156 | if err != nil { 157 | t.Fatal(err) 158 | } 159 | bufWebp, err := Read("testdata/test.webp") 160 | if err != nil { 161 | t.Fatal(err) 162 | } 163 | 164 | images := []struct { 165 | format ImageType 166 | buf []byte 167 | }{ 168 | {JPEG, bufJpeg}, 169 | {WEBP, bufWebp}, 170 | } 171 | 172 | for _, source := range images { 173 | for _, options := range tests { 174 | image, err := Resize(source.buf, options) 175 | if err != nil { 176 | t.Errorf("Resize(imgData, %#v) error: %#v", options, err) 177 | } 178 | 179 | if DetermineImageType(image) != source.format { 180 | t.Fatalf("Image format is invalid. Expected: %#v", source.format) 181 | } 182 | 183 | size, _ := Size(image) 184 | 185 | invalidHeight := options.Height > 0 && size.Height != options.Height 186 | if !options.Crop && invalidHeight { 187 | t.Fatalf("Invalid height: %d, expected %d", size.Height, options.Height) 188 | } 189 | 190 | invalidWidth := options.Width > 0 && size.Width != options.Width 191 | if !options.Crop && invalidWidth { 192 | t.Fatalf("Invalid width: %d, expected %d", size.Width, options.Width) 193 | } 194 | 195 | if options.Crop && invalidHeight && invalidWidth { 196 | t.Fatalf("Invalid width or height: %dx%d, expected %dx%d (crop)", size.Width, size.Height, options.Width, options.Height) 197 | } 198 | } 199 | } 200 | } 201 | 202 | func TestResizePrecision(t *testing.T) { 203 | // see https://github.com/h2non/bimg/issues/99 204 | img := image.NewGray16(image.Rect(0, 0, 1920, 1080)) 205 | input := &bytes.Buffer{} 206 | jpeg.Encode(input, img, nil) 207 | 208 | opts := Options{Width: 300} 209 | newImg, err := Resize(input.Bytes(), opts) 210 | if err != nil { 211 | t.Fatalf("Resize(imgData, %#v) error: %#v", opts, err) 212 | } 213 | 214 | size, _ := Size(newImg) 215 | if size.Width != opts.Width { 216 | t.Fatalf("Invalid width: %d", size.Width) 217 | } 218 | } 219 | 220 | func TestRotate(t *testing.T) { 221 | options := Options{Width: 800, Height: 600, Rotate: 270, Crop: true} 222 | buf, _ := Read("testdata/test.jpg") 223 | 224 | newImg, err := Resize(buf, options) 225 | if err != nil { 226 | t.Errorf("Resize(imgData, %#v) error: %#v", options, err) 227 | } 228 | 229 | if DetermineImageType(newImg) != JPEG { 230 | t.Error("Image is not jpeg") 231 | } 232 | 233 | size, _ := Size(newImg) 234 | if size.Width != options.Width || size.Height != options.Height { 235 | t.Errorf("Invalid image size: %dx%d", size.Width, size.Height) 236 | } 237 | 238 | Write("testdata/test_rotate_out.jpg", newImg) 239 | } 240 | 241 | func TestInvalidRotateDegrees(t *testing.T) { 242 | options := Options{Width: 800, Height: 600, Rotate: 111, Crop: true} 243 | buf, _ := Read("testdata/test.jpg") 244 | 245 | newImg, err := Resize(buf, options) 246 | if err != nil { 247 | t.Errorf("Resize(imgData, %#v) error: %#v", options, err) 248 | } 249 | 250 | if DetermineImageType(newImg) != JPEG { 251 | t.Errorf("Image is not jpeg") 252 | } 253 | 254 | size, _ := Size(newImg) 255 | if size.Width != options.Width || size.Height != options.Height { 256 | t.Errorf("Invalid image size: %dx%d", size.Width, size.Height) 257 | } 258 | 259 | Write("testdata/test_rotate_invalid_out.jpg", newImg) 260 | } 261 | 262 | func TestCorruptedImage(t *testing.T) { 263 | options := Options{Width: 800, Height: 600} 264 | buf, _ := Read("testdata/corrupt.jpg") 265 | 266 | newImg, err := Resize(buf, options) 267 | if err != nil { 268 | t.Errorf("Resize(imgData, %#v) error: %#v", options, err) 269 | } 270 | 271 | if DetermineImageType(newImg) != JPEG { 272 | t.Fatal("Image is not jpeg") 273 | } 274 | 275 | size, _ := Size(newImg) 276 | if size.Height != options.Height || size.Width != options.Width { 277 | t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) 278 | } 279 | 280 | Write("testdata/test_corrupt_out.jpg", newImg) 281 | } 282 | 283 | func TestNoColorProfile(t *testing.T) { 284 | options := Options{Width: 800, Height: 600, NoProfile: true} 285 | buf, _ := Read("testdata/test.jpg") 286 | 287 | newImg, err := Resize(buf, options) 288 | if err != nil { 289 | t.Errorf("Resize(imgData, %#v) error: %#v", options, err) 290 | } 291 | 292 | metadata, err := Metadata(newImg) 293 | if metadata.Profile == true { 294 | t.Fatal("Invalid profile data") 295 | } 296 | 297 | size, _ := Size(newImg) 298 | if size.Height != options.Height || size.Width != options.Width { 299 | t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) 300 | } 301 | } 302 | 303 | func TestEmbedExtendColor(t *testing.T) { 304 | options := Options{Width: 400, Height: 600, Crop: false, Embed: true, Extend: ExtendWhite, Background: Color{255, 20, 10}} 305 | buf, _ := Read("testdata/test_issue.jpg") 306 | 307 | newImg, err := Resize(buf, options) 308 | if err != nil { 309 | t.Errorf("Resize(imgData, %#v) error: %#v", options, err) 310 | } 311 | 312 | size, _ := Size(newImg) 313 | if size.Height != options.Height || size.Width != options.Width { 314 | t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) 315 | } 316 | 317 | Write("testdata/test_extend_white_out.jpg", newImg) 318 | } 319 | 320 | func TestEmbedExtendWithCustomColor(t *testing.T) { 321 | options := Options{Width: 400, Height: 600, Crop: false, Embed: true, Extend: 5, Background: Color{255, 20, 10}} 322 | buf, _ := Read("testdata/test_issue.jpg") 323 | 324 | newImg, err := Resize(buf, options) 325 | if err != nil { 326 | t.Errorf("Resize(imgData, %#v) error: %#v", options, err) 327 | } 328 | 329 | size, _ := Size(newImg) 330 | if size.Height != options.Height || size.Width != options.Width { 331 | t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) 332 | } 333 | 334 | Write("testdata/test_extend_background_out.jpg", newImg) 335 | } 336 | 337 | func TestGaussianBlur(t *testing.T) { 338 | options := Options{Width: 800, Height: 600, GaussianBlur: GaussianBlur{Sigma: 5}} 339 | buf, _ := Read("testdata/test.jpg") 340 | 341 | newImg, err := Resize(buf, options) 342 | if err != nil { 343 | t.Errorf("Resize(imgData, %#v) error: %#v", options, err) 344 | } 345 | 346 | size, _ := Size(newImg) 347 | if size.Height != options.Height || size.Width != options.Width { 348 | t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) 349 | } 350 | 351 | Write("testdata/test_gaussian_out.jpg", newImg) 352 | } 353 | 354 | func TestSharpen(t *testing.T) { 355 | options := Options{Width: 800, Height: 600, Sharpen: Sharpen{Radius: 1, X1: 1.5, Y2: 20, Y3: 50, M1: 1, M2: 2}} 356 | buf, _ := Read("testdata/test.jpg") 357 | 358 | newImg, err := Resize(buf, options) 359 | if err != nil { 360 | t.Errorf("Resize(imgData, %#v) error: %#v", options, err) 361 | } 362 | 363 | size, _ := Size(newImg) 364 | if size.Height != options.Height || size.Width != options.Width { 365 | t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) 366 | } 367 | 368 | Write("testdata/test_sharpen_out.jpg", newImg) 369 | } 370 | 371 | func TestExtractWithDefaultAxis(t *testing.T) { 372 | options := Options{AreaWidth: 200, AreaHeight: 200} 373 | buf, _ := Read("testdata/test.jpg") 374 | 375 | newImg, err := Resize(buf, options) 376 | if err != nil { 377 | t.Errorf("Resize(imgData, %#v) error: %#v", options, err) 378 | } 379 | 380 | size, _ := Size(newImg) 381 | if size.Height != options.AreaHeight || size.Width != options.AreaWidth { 382 | t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) 383 | } 384 | 385 | Write("testdata/test_extract_defaults_out.jpg", newImg) 386 | } 387 | 388 | func TestExtractCustomAxis(t *testing.T) { 389 | options := Options{Top: 100, Left: 100, AreaWidth: 200, AreaHeight: 200} 390 | buf, _ := Read("testdata/test.jpg") 391 | 392 | newImg, err := Resize(buf, options) 393 | if err != nil { 394 | t.Errorf("Resize(imgData, %#v) error: %#v", options, err) 395 | } 396 | 397 | size, _ := Size(newImg) 398 | if size.Height != options.AreaHeight || size.Width != options.AreaWidth { 399 | t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) 400 | } 401 | 402 | Write("testdata/test_extract_custom_axis_out.jpg", newImg) 403 | } 404 | 405 | func TestExtractOrEmbedImage(t *testing.T) { 406 | buf, _ := Read("testdata/test.jpg") 407 | input, _, err := loadImage(buf) 408 | if err != nil { 409 | t.Fatalf("Unable to load image %s", err) 410 | } 411 | 412 | o := Options{ 413 | Top: 10, 414 | Left: 10, 415 | Width: 100, 416 | Height: 200, 417 | 418 | // Fields to test 419 | AreaHeight: 0, 420 | AreaWidth: 0, 421 | 422 | Quality: 100, /* Needs a value to satisfy libvips */ 423 | } 424 | 425 | result, err := extractOrEmbedImage(input, o) 426 | if err != nil { 427 | if err == ErrExtractAreaParamsRequired { 428 | t.Fatalf("Expecting AreaWidth and AreaHeight to have been defined") 429 | } 430 | 431 | t.Fatalf("Unknown error occurred %s", err) 432 | } 433 | 434 | image, err := saveImage(result, o) 435 | if err != nil { 436 | t.Fatalf("Failed saving image %s", err) 437 | } 438 | 439 | test, err := Size(image) 440 | if err != nil { 441 | t.Fatalf("Failed fetching the size %s", err) 442 | } 443 | 444 | if test.Height != o.Height { 445 | t.Errorf("Extract failed, resulting Height %d doesn't match %d", test.Height, o.Height) 446 | } 447 | 448 | if test.Width != o.Width { 449 | t.Errorf("Extract failed, resulting Width %d doesn't match %d", test.Width, o.Width) 450 | } 451 | } 452 | 453 | func TestConvert(t *testing.T) { 454 | width, height := 300, 240 455 | formats := [3]ImageType{PNG, WEBP, JPEG} 456 | 457 | files := []string{ 458 | "test.jpg", 459 | "test.png", 460 | "test.webp", 461 | } 462 | 463 | for _, file := range files { 464 | img, err := os.Open("testdata/" + file) 465 | if err != nil { 466 | t.Fatal(err) 467 | } 468 | 469 | buf, err := ioutil.ReadAll(img) 470 | if err != nil { 471 | t.Fatal(err) 472 | } 473 | img.Close() 474 | 475 | for _, format := range formats { 476 | options := Options{Width: width, Height: height, Crop: true, Type: format} 477 | 478 | newImg, err := Resize(buf, options) 479 | if err != nil { 480 | t.Errorf("Resize(imgData, %#v) error: %#v", options, err) 481 | } 482 | 483 | if DetermineImageType(newImg) != format { 484 | t.Fatal("Image is not png") 485 | } 486 | 487 | size, _ := Size(newImg) 488 | if size.Height != height || size.Width != width { 489 | t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) 490 | } 491 | } 492 | } 493 | } 494 | 495 | func TestResizePngWithTransparency(t *testing.T) { 496 | width, height := 300, 240 497 | 498 | options := Options{Width: width, Height: height, Crop: true} 499 | img, err := os.Open("testdata/transparent.png") 500 | if err != nil { 501 | t.Fatal(err) 502 | } 503 | defer img.Close() 504 | 505 | buf, err := ioutil.ReadAll(img) 506 | if err != nil { 507 | t.Fatal(err) 508 | } 509 | 510 | newImg, err := Resize(buf, options) 511 | if err != nil { 512 | t.Errorf("Resize(imgData, %#v) error: %#v", options, err) 513 | } 514 | 515 | if DetermineImageType(newImg) != PNG { 516 | t.Fatal("Image is not png") 517 | } 518 | 519 | size, _ := Size(newImg) 520 | if size.Height != height || size.Width != width { 521 | t.Fatal("Invalid image size") 522 | } 523 | 524 | Write("testdata/transparent_out.png", newImg) 525 | } 526 | 527 | func TestRotationAndFlip(t *testing.T) { 528 | files := []struct { 529 | Name string 530 | Angle Angle 531 | Flip bool 532 | }{ 533 | {"Landscape_1", 0, false}, 534 | {"Landscape_2", 0, true}, 535 | {"Landscape_3", D180, false}, 536 | {"Landscape_4", D180, true}, 537 | {"Landscape_5", D90, true}, 538 | {"Landscape_6", D90, false}, 539 | {"Landscape_7", D270, true}, 540 | {"Landscape_8", D270, false}, 541 | {"Portrait_1", 0, false}, 542 | {"Portrait_2", 0, true}, 543 | {"Portrait_3", D180, false}, 544 | {"Portrait_4", D180, true}, 545 | {"Portrait_5", D90, true}, 546 | {"Portrait_6", D90, false}, 547 | {"Portrait_7", D270, true}, 548 | {"Portrait_8", D270, false}, 549 | } 550 | 551 | for _, file := range files { 552 | img, err := os.Open(fmt.Sprintf("testdata/exif/%s.jpg", file.Name)) 553 | if err != nil { 554 | t.Fatal(err) 555 | } 556 | 557 | buf, err := ioutil.ReadAll(img) 558 | if err != nil { 559 | t.Fatal(err) 560 | } 561 | img.Close() 562 | 563 | image, _, err := loadImage(buf) 564 | if err != nil { 565 | t.Fatal(err) 566 | } 567 | 568 | angle, flip := calculateRotationAndFlip(image, D0) 569 | if angle != file.Angle { 570 | t.Errorf("Rotation for %v expected to be %v. got %v", file.Name, file.Angle, angle) 571 | } 572 | if flip != file.Flip { 573 | t.Errorf("Flip for %v expected to be %v. got %v", file.Name, file.Flip, flip) 574 | } 575 | 576 | // Visual debugging. 577 | newImg, err := Resize(buf, Options{}) 578 | if err != nil { 579 | t.Fatal(err) 580 | } 581 | 582 | Write(fmt.Sprintf("testdata/exif/%s_out.jpg", file.Name), newImg) 583 | } 584 | } 585 | 586 | func TestIfBothSmartCropOptionsAreIdentical(t *testing.T) { 587 | if !(VipsMajorVersion >= 8 && VipsMinorVersion > 4) { 588 | t.Skipf("Skipping this test, libvips doesn't meet version requirement %s > 8.4", VipsVersion) 589 | } 590 | 591 | benchmarkOptions := Options{Width: 100, Height: 100, Crop: true} 592 | smartCropOptions := Options{Width: 100, Height: 100, Crop: true, SmartCrop: true} 593 | gravityOptions := Options{Width: 100, Height: 100, Crop: true, Gravity: GravitySmart} 594 | 595 | testImg, err := os.Open("testdata/northern_cardinal_bird.jpg") 596 | if err != nil { 597 | t.Fatal(err) 598 | } 599 | defer testImg.Close() 600 | 601 | testImgByte, err := ioutil.ReadAll(testImg) 602 | if err != nil { 603 | t.Fatal(err) 604 | } 605 | 606 | scImg, err := Resize(testImgByte, smartCropOptions) 607 | if err != nil { 608 | t.Fatal(err) 609 | } 610 | 611 | gImg, err := Resize(testImgByte, gravityOptions) 612 | if err != nil { 613 | t.Fatal(err) 614 | } 615 | 616 | benchmarkImg, err := Resize(testImgByte, benchmarkOptions) 617 | if err != nil { 618 | t.Fatal(err) 619 | } 620 | 621 | sch, gh, bh := md5.Sum(scImg), md5.Sum(gImg), md5.Sum(benchmarkImg) 622 | if gh == bh || sch == bh { 623 | t.Error("Expected both options produce a different result from a standard crop.") 624 | } 625 | 626 | if sch != gh { 627 | t.Errorf("Expected both options to result in the same output, %x != %x", sch, gh) 628 | } 629 | } 630 | 631 | func runBenchmarkResize(file string, o Options, b *testing.B) { 632 | buf, _ := Read(path.Join("testdata", file)) 633 | 634 | for n := 0; n < b.N; n++ { 635 | Resize(buf, o) 636 | } 637 | } 638 | 639 | func BenchmarkRotateJpeg(b *testing.B) { 640 | options := Options{Rotate: 180} 641 | runBenchmarkResize("test.jpg", options, b) 642 | } 643 | 644 | func BenchmarkResizeLargeJpeg(b *testing.B) { 645 | options := Options{ 646 | Width: 800, 647 | Height: 600, 648 | } 649 | runBenchmarkResize("test.jpg", options, b) 650 | } 651 | 652 | func BenchmarkResizePng(b *testing.B) { 653 | options := Options{ 654 | Width: 200, 655 | Height: 200, 656 | } 657 | runBenchmarkResize("test.png", options, b) 658 | } 659 | 660 | func BenchmarkResizeWebp(b *testing.B) { 661 | options := Options{ 662 | Width: 200, 663 | Height: 200, 664 | } 665 | runBenchmarkResize("test.webp", options, b) 666 | } 667 | 668 | func BenchmarkConvertToJpeg(b *testing.B) { 669 | options := Options{Type: JPEG} 670 | runBenchmarkResize("test.png", options, b) 671 | } 672 | 673 | func BenchmarkConvertToPng(b *testing.B) { 674 | options := Options{Type: PNG} 675 | runBenchmarkResize("test.jpg", options, b) 676 | } 677 | 678 | func BenchmarkConvertToWebp(b *testing.B) { 679 | options := Options{Type: WEBP} 680 | runBenchmarkResize("test.jpg", options, b) 681 | } 682 | 683 | func BenchmarkCropJpeg(b *testing.B) { 684 | options := Options{ 685 | Width: 800, 686 | Height: 600, 687 | } 688 | runBenchmarkResize("test.jpg", options, b) 689 | } 690 | 691 | func BenchmarkCropPng(b *testing.B) { 692 | options := Options{ 693 | Width: 800, 694 | Height: 600, 695 | } 696 | runBenchmarkResize("test.png", options, b) 697 | } 698 | 699 | func BenchmarkCropWebp(b *testing.B) { 700 | options := Options{ 701 | Width: 800, 702 | Height: 600, 703 | } 704 | runBenchmarkResize("test.webp", options, b) 705 | } 706 | 707 | func BenchmarkExtractJpeg(b *testing.B) { 708 | options := Options{ 709 | Top: 100, 710 | Left: 50, 711 | AreaWidth: 600, 712 | AreaHeight: 480, 713 | } 714 | runBenchmarkResize("test.jpg", options, b) 715 | } 716 | 717 | func BenchmarkExtractPng(b *testing.B) { 718 | options := Options{ 719 | Top: 100, 720 | Left: 50, 721 | AreaWidth: 600, 722 | AreaHeight: 480, 723 | } 724 | runBenchmarkResize("test.png", options, b) 725 | } 726 | 727 | func BenchmarkExtractWebp(b *testing.B) { 728 | options := Options{ 729 | Top: 100, 730 | Left: 50, 731 | AreaWidth: 600, 732 | AreaHeight: 480, 733 | } 734 | runBenchmarkResize("test.webp", options, b) 735 | } 736 | 737 | func BenchmarkZoomJpeg(b *testing.B) { 738 | options := Options{Zoom: 1} 739 | runBenchmarkResize("test.jpg", options, b) 740 | } 741 | 742 | func BenchmarkZoomPng(b *testing.B) { 743 | options := Options{Zoom: 1} 744 | runBenchmarkResize("test.png", options, b) 745 | } 746 | 747 | func BenchmarkZoomWebp(b *testing.B) { 748 | options := Options{Zoom: 1} 749 | runBenchmarkResize("test.webp", options, b) 750 | } 751 | 752 | func BenchmarkWatermarkJpeg(b *testing.B) { 753 | options := Options{ 754 | Watermark: Watermark{ 755 | Text: "Chuck Norris (c) 2315", 756 | Opacity: 0.25, 757 | Width: 200, 758 | DPI: 100, 759 | Margin: 150, 760 | Font: "sans bold 12", 761 | Background: Color{255, 255, 255}, 762 | }, 763 | } 764 | runBenchmarkResize("test.jpg", options, b) 765 | } 766 | 767 | func BenchmarkWatermarkPng(b *testing.B) { 768 | options := Options{ 769 | Watermark: Watermark{ 770 | Text: "Chuck Norris (c) 2315", 771 | Opacity: 0.25, 772 | Width: 200, 773 | DPI: 100, 774 | Margin: 150, 775 | Font: "sans bold 12", 776 | Background: Color{255, 255, 255}, 777 | }, 778 | } 779 | runBenchmarkResize("test.png", options, b) 780 | } 781 | 782 | func BenchmarkWatermarkWebp(b *testing.B) { 783 | options := Options{ 784 | Watermark: Watermark{ 785 | Text: "Chuck Norris (c) 2315", 786 | Opacity: 0.25, 787 | Width: 200, 788 | DPI: 100, 789 | Margin: 150, 790 | Font: "sans bold 12", 791 | Background: Color{255, 255, 255}, 792 | }, 793 | } 794 | runBenchmarkResize("test.webp", options, b) 795 | } 796 | 797 | func BenchmarkWatermarkImageJpeg(b *testing.B) { 798 | watermark := readFile("transparent.png") 799 | options := Options{ 800 | WatermarkImage: WatermarkImage{ 801 | Buf: watermark, 802 | Opacity: 0.25, 803 | Left: 100, 804 | Top: 100, 805 | }, 806 | } 807 | runBenchmarkResize("test.jpg", options, b) 808 | } 809 | 810 | func BenchmarkWatermarkImagePng(b *testing.B) { 811 | watermark := readFile("transparent.png") 812 | options := Options{ 813 | WatermarkImage: WatermarkImage{ 814 | Buf: watermark, 815 | Opacity: 0.25, 816 | Left: 100, 817 | Top: 100, 818 | }, 819 | } 820 | runBenchmarkResize("test.png", options, b) 821 | } 822 | 823 | func BenchmarkWatermarkImageWebp(b *testing.B) { 824 | watermark := readFile("transparent.png") 825 | options := Options{ 826 | WatermarkImage: WatermarkImage{ 827 | Buf: watermark, 828 | Opacity: 0.25, 829 | Left: 100, 830 | Top: 100, 831 | }, 832 | } 833 | runBenchmarkResize("test.webp", options, b) 834 | } 835 | --------------------------------------------------------------------------------