diff --git a/media-api-server-config.yaml b/media-api-server-config.yaml new file mode 100644 index 00000000..c222fe8f --- /dev/null +++ b/media-api-server-config.yaml @@ -0,0 +1,38 @@ +# The name of the server. This is usually the domain name, e.g 'matrix.org', 'localhost'. +server_name: "example.com" + +# The base path to where the media files will be stored. May be relative or absolute. +base_path: /var/dendrite/media + +# The maximum file size in bytes that is allowed to be stored on this server. +# Note: if max_file_size_bytes is set to 0, the size is unlimited. +# Note: if max_file_size_bytes is not set, it will default to 10485760 (10MB) +max_file_size_bytes: 10485760 + +# The postgres connection config for connecting to the database e.g a postgres:// URI +database: "postgres://dendrite:itsasecret@localhost/mediaapi?sslmode=disable" + +# Whether to dynamically generate thumbnails on-the-fly if the requested resolution is not already generated +# NOTE: This is a possible denial-of-service attack vector - use at your own risk +dynamic_thumbnails: false + +# A list of thumbnail sizes to be pre-generated for downloaded remote / uploaded content +# method is one of crop or scale. If omitted, it will default to scale. +# crop scales to fill the requested dimensions and crops the excess. +# scale scales to fit the requested dimensions and one dimension may be smaller than requested. +thumbnail_sizes: +- width: 32 + height: 32 + method: crop +- width: 96 + height: 96 + method: crop +- width: 320 + height: 240 + method: scale +- width: 640 + height: 480 + method: scale +- width: 800 + height: 600 + method: scale diff --git a/src/github.com/matrix-org/dendrite/cmd/dendrite-media-api-server/main.go b/src/github.com/matrix-org/dendrite/cmd/dendrite-media-api-server/main.go index 29876248..e5017937 100644 --- a/src/github.com/matrix-org/dendrite/cmd/dendrite-media-api-server/main.go +++ b/src/github.com/matrix-org/dendrite/cmd/dendrite-media-api-server/main.go @@ -15,10 +15,14 @@ package main import ( + "fmt" + "io/ioutil" "net/http" "os" + "os/user" "path/filepath" "strconv" + "strings" "github.com/matrix-org/dendrite/common" "github.com/matrix-org/dendrite/mediaapi/config" @@ -28,6 +32,7 @@ import ( "github.com/matrix-org/gomatrixserverlib" log "github.com/Sirupsen/logrus" + yaml "gopkg.in/yaml.v2" ) var ( @@ -38,36 +43,25 @@ var ( basePath = os.Getenv("BASE_PATH") // Note: if the MAX_FILE_SIZE_BYTES is set to 0, it will be unlimited maxFileSizeBytesString = os.Getenv("MAX_FILE_SIZE_BYTES") + configPath = os.Getenv("CONFIG_PATH") ) func main() { common.SetupLogging(logDir) - if bindAddr == "" { - log.Panic("No BIND_ADDRESS environment variable found.") - } - if basePath == "" { - log.Panic("No BASE_PATH environment variable found.") - } - absBasePath, err := filepath.Abs(basePath) - if err != nil { - log.WithError(err).WithField("BASE_PATH", basePath).Panic("BASE_PATH is invalid (must be able to make absolute)") - } + log.WithFields(log.Fields{ + "BIND_ADDRESS": bindAddr, + "DATABASE": dataSource, + "LOG_DIR": logDir, + "SERVER_NAME": serverName, + "BASE_PATH": basePath, + "MAX_FILE_SIZE_BYTES": maxFileSizeBytesString, + "CONFIG_PATH": configPath, + }).Info("Loading configuration based on config file and environment variables") - if serverName == "" { - serverName = "localhost" - } - maxFileSizeBytes, err := strconv.ParseInt(maxFileSizeBytesString, 10, 64) + cfg, err := configureServer() if err != nil { - maxFileSizeBytes = 10 * 1024 * 1024 - log.WithError(err).WithField("MAX_FILE_SIZE_BYTES", maxFileSizeBytesString).Warnf("Failed to parse MAX_FILE_SIZE_BYTES. Defaulting to %v bytes.", maxFileSizeBytes) - } - - cfg := &config.MediaAPI{ - ServerName: gomatrixserverlib.ServerName(serverName), - AbsBasePath: types.Path(absBasePath), - MaxFileSizeBytes: types.FileSizeBytes(maxFileSizeBytes), - DataSource: dataSource, + log.WithError(err).Fatal("Invalid configuration") } db, err := storage.Open(cfg.DataSource) @@ -76,14 +70,182 @@ func main() { } log.WithFields(log.Fields{ - "BASE_PATH": absBasePath, - "BIND_ADDRESS": bindAddr, - "DATABASE": dataSource, - "LOG_DIR": logDir, - "MAX_FILE_SIZE_BYTES": maxFileSizeBytes, - "SERVER_NAME": serverName, - }).Info("Starting mediaapi") + "BIND_ADDRESS": bindAddr, + "LOG_DIR": logDir, + "CONFIG_PATH": configPath, + "ServerName": cfg.ServerName, + "AbsBasePath": cfg.AbsBasePath, + "MaxFileSizeBytes": *cfg.MaxFileSizeBytes, + "DataSource": cfg.DataSource, + "DynamicThumbnails": cfg.DynamicThumbnails, + "MaxThumbnailGenerators": cfg.MaxThumbnailGenerators, + "ThumbnailSizes": cfg.ThumbnailSizes, + }).Info("Starting mediaapi server with configuration") routing.Setup(http.DefaultServeMux, http.DefaultClient, cfg, db) log.Fatal(http.ListenAndServe(bindAddr, nil)) } + +// configureServer loads configuration from a yaml file and overrides with environment variables +func configureServer() (*config.MediaAPI, error) { + cfg, err := loadConfig(configPath) + if err != nil { + log.WithError(err).Fatal("Invalid config file") + } + + // override values from environment variables + applyOverrides(cfg) + + if err := validateConfig(cfg); err != nil { + return nil, err + } + + return cfg, nil +} + +// FIXME: make common somehow? copied from sync api +func loadConfig(configPath string) (*config.MediaAPI, error) { + contents, err := ioutil.ReadFile(configPath) + if err != nil { + return nil, err + } + var cfg config.MediaAPI + if err = yaml.Unmarshal(contents, &cfg); err != nil { + return nil, err + } + return &cfg, nil +} + +func applyOverrides(cfg *config.MediaAPI) { + if serverName != "" { + if cfg.ServerName != "" { + log.WithFields(log.Fields{ + "server_name": cfg.ServerName, + "SERVER_NAME": serverName, + }).Info("Overriding server_name from config file with environment variable") + } + cfg.ServerName = gomatrixserverlib.ServerName(serverName) + } + if cfg.ServerName == "" { + log.Info("ServerName not set. Defaulting to 'localhost'.") + cfg.ServerName = "localhost" + } + + if basePath != "" { + if cfg.BasePath != "" { + log.WithFields(log.Fields{ + "base_path": cfg.BasePath, + "BASE_PATH": basePath, + }).Info("Overriding base_path from config file with environment variable") + } + cfg.BasePath = types.Path(basePath) + } + + if maxFileSizeBytesString != "" { + if cfg.MaxFileSizeBytes != nil { + log.WithFields(log.Fields{ + "max_file_size_bytes": *cfg.MaxFileSizeBytes, + "MAX_FILE_SIZE_BYTES": maxFileSizeBytesString, + }).Info("Overriding max_file_size_bytes from config file with environment variable") + } + maxFileSizeBytesInt, err := strconv.ParseInt(maxFileSizeBytesString, 10, 64) + if err != nil { + maxFileSizeBytesInt = 10 * 1024 * 1024 + log.WithError(err).WithField( + "MAX_FILE_SIZE_BYTES", maxFileSizeBytesString, + ).Infof("MAX_FILE_SIZE_BYTES not set? Defaulting to %v bytes.", maxFileSizeBytesInt) + } + maxFileSizeBytes := types.FileSizeBytes(maxFileSizeBytesInt) + cfg.MaxFileSizeBytes = &maxFileSizeBytes + } + + if dataSource != "" { + if cfg.DataSource != "" { + log.WithFields(log.Fields{ + "database": cfg.DataSource, + "DATABASE": dataSource, + }).Info("Overriding database from config file with environment variable") + } + cfg.DataSource = dataSource + } + + if cfg.MaxThumbnailGenerators == 0 { + log.WithField( + "max_thumbnail_generators", cfg.MaxThumbnailGenerators, + ).Info("Using default max_thumbnail_generators") + cfg.MaxThumbnailGenerators = 10 + } +} + +func validateConfig(cfg *config.MediaAPI) error { + if bindAddr == "" { + return fmt.Errorf("no BIND_ADDRESS environment variable found") + } + + absBasePath, err := getAbsolutePath(cfg.BasePath) + if err != nil { + return fmt.Errorf("invalid base path (%v): %q", cfg.BasePath, err) + } + cfg.AbsBasePath = types.Path(absBasePath) + + if *cfg.MaxFileSizeBytes < 0 { + return fmt.Errorf("invalid max file size bytes (%v)", *cfg.MaxFileSizeBytes) + } + + if cfg.DataSource == "" { + return fmt.Errorf("invalid database (%v)", cfg.DataSource) + } + + for _, config := range cfg.ThumbnailSizes { + if config.Width <= 0 || config.Height <= 0 { + return fmt.Errorf("invalid thumbnail size %vx%v", config.Width, config.Height) + } + } + + return nil +} + +func getAbsolutePath(basePath types.Path) (types.Path, error) { + var err error + if basePath == "" { + var wd string + wd, err = os.Getwd() + return types.Path(wd), err + } + // Note: If we got here len(basePath) >= 1 + if basePath[0] == '~' { + basePath, err = expandHomeDir(basePath) + if err != nil { + return "", err + } + } + absBasePath, err := filepath.Abs(string(basePath)) + return types.Path(absBasePath), err +} + +// expandHomeDir parses paths beginning with ~/path or ~user/path and replaces the home directory part +func expandHomeDir(basePath types.Path) (types.Path, error) { + slash := strings.Index(string(basePath), "/") + if slash == -1 { + // pretend the slash is after the path as none was found within the string + // simplifies code using slash below + slash = len(basePath) + } + var usr *user.User + var err error + if slash == 1 { + // basePath is ~ or ~/path + usr, err = user.Current() + if err != nil { + return "", fmt.Errorf("failed to get user's home directory: %q", err) + } + } else { + // slash > 1 + // basePath is ~user or ~user/path + usr, err = user.Lookup(string(basePath[1:slash])) + if err != nil { + return "", fmt.Errorf("failed to get user's home directory: %q", err) + } + } + return types.Path(filepath.Join(usr.HomeDir, string(basePath[slash:]))), nil +} diff --git a/src/github.com/matrix-org/dendrite/mediaapi/README.md b/src/github.com/matrix-org/dendrite/mediaapi/README.md new file mode 100644 index 00000000..8d6cc627 --- /dev/null +++ b/src/github.com/matrix-org/dendrite/mediaapi/README.md @@ -0,0 +1,27 @@ +# Media API + +This server is responsible for serving `/media` requests as per: + +http://matrix.org/docs/spec/client_server/r0.2.0.html#id43 + +## Scaling libraries + +### nfnt/resize (default) + +Thumbnailing uses https://github.com/nfnt/resize by default which is a pure golang image scaling library relying on image codecs from the standard library. It is ISC-licensed. + +It is multi-threaded and uses Lanczos3 so produces sharp images. Using Lanczos3 all the way makes it slower than some other approaches like bimg. (~845ms in total for pre-generating 32x32-crop, 96x96-crop, 320x240-scale, 640x480-scale and 800x600-scale from a given JPEG image on a given machine.) + +See the sample below for image quality with nfnt/resize: + +![](nfnt-96x96-crop.jpg) + +### bimg (uses libvips C library) + +Alternatively one can use `gb build -tags bimg` to use bimg from https://github.com/h2non/bimg (MIT-licensed) which uses libvips from https://github.com/jcupitt/libvips (LGPL v2.1+ -licensed). libvips is a C library and must be installed/built separately. See the github page for details. Also note that libvips in turn has dependencies with a selection of FOSS licenses. + +bimg and libvips have significantly better performance than nfnt/resize but produce slightly less-sharp images. bimg uses a box filter for downscaling to within about 200% of the target scale and then uses Lanczos3 for the last bit. This is a much faster approach but comes at the expense of sharpness. (~295ms in total for pre-generating 32x32-crop, 96x96-crop, 320x240-scale, 640x480-scale and 800x600-scale from a given JPEG image on a given machine.) + +See the sample below for image quality with bimg: + +![](bimg-96x96-crop.jpg) diff --git a/src/github.com/matrix-org/dendrite/mediaapi/bimg-96x96-crop.jpg b/src/github.com/matrix-org/dendrite/mediaapi/bimg-96x96-crop.jpg new file mode 100644 index 00000000..f6521893 Binary files /dev/null and b/src/github.com/matrix-org/dendrite/mediaapi/bimg-96x96-crop.jpg differ diff --git a/src/github.com/matrix-org/dendrite/mediaapi/config/config.go b/src/github.com/matrix-org/dendrite/mediaapi/config/config.go index 5c514194..14c12e86 100644 --- a/src/github.com/matrix-org/dendrite/mediaapi/config/config.go +++ b/src/github.com/matrix-org/dendrite/mediaapi/config/config.go @@ -23,12 +23,20 @@ import ( type MediaAPI struct { // The name of the server. This is usually the domain name, e.g 'matrix.org', 'localhost'. ServerName gomatrixserverlib.ServerName `yaml:"server_name"` + // The base path to where the media files will be stored. May be relative or absolute. + BasePath types.Path `yaml:"base_path"` // The absolute base path to where media files will be stored. - AbsBasePath types.Path `yaml:"abs_base_path"` + AbsBasePath types.Path `yaml:"-"` // The maximum file size in bytes that is allowed to be stored on this server. // Note: if MaxFileSizeBytes is set to 0, the size is unlimited. // Note: if max_file_size_bytes is not set, it will default to 10485760 (10MB) - MaxFileSizeBytes types.FileSizeBytes `yaml:"max_file_size_bytes"` + MaxFileSizeBytes *types.FileSizeBytes `yaml:"max_file_size_bytes,omitempty"` // The postgres connection config for connecting to the database e.g a postgres:// URI DataSource string `yaml:"database"` + // Whether to dynamically generate thumbnails on-the-fly if the requested resolution is not already generated + DynamicThumbnails bool `yaml:"dynamic_thumbnails"` + // The maximum number of simultaneous thumbnail generators. default: 10 + MaxThumbnailGenerators int `yaml:"max_thumbnail_generators"` + // A list of thumbnail sizes to be pre-generated for downloaded remote / uploaded content + ThumbnailSizes []types.ThumbnailSize `yaml:"thumbnail_sizes"` } diff --git a/src/github.com/matrix-org/dendrite/mediaapi/nfnt-96x96-crop.jpg b/src/github.com/matrix-org/dendrite/mediaapi/nfnt-96x96-crop.jpg new file mode 100644 index 00000000..1e424cd8 Binary files /dev/null and b/src/github.com/matrix-org/dendrite/mediaapi/nfnt-96x96-crop.jpg differ diff --git a/src/github.com/matrix-org/dendrite/mediaapi/routing/routing.go b/src/github.com/matrix-org/dendrite/mediaapi/routing/routing.go index 7641109c..47985fc1 100644 --- a/src/github.com/matrix-org/dendrite/mediaapi/routing/routing.go +++ b/src/github.com/matrix-org/dendrite/mediaapi/routing/routing.go @@ -35,28 +35,40 @@ const pathPrefixR0 = "/_matrix/media/v1" func Setup(servMux *http.ServeMux, httpClient *http.Client, cfg *config.MediaAPI, db *storage.Database) { apiMux := mux.NewRouter() r0mux := apiMux.PathPrefix(pathPrefixR0).Subrouter() + + activeThumbnailGeneration := &types.ActiveThumbnailGeneration{ + PathToResult: map[string]*types.ThumbnailGenerationResult{}, + } + // FIXME: /upload should use common.MakeAuthAPI() r0mux.Handle("/upload", common.MakeAPI("upload", func(req *http.Request) util.JSONResponse { - return writers.Upload(req, cfg, db) + return writers.Upload(req, cfg, db, activeThumbnailGeneration) })) activeRemoteRequests := &types.ActiveRemoteRequests{ MXCToResult: map[string]*types.RemoteRequestResult{}, } r0mux.Handle("/download/{serverName}/{mediaId}", - prometheus.InstrumentHandler("download", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - req = util.RequestWithLogging(req) - - // Set common headers returned regardless of the outcome of the request - util.SetCORSHeaders(w) - // Content-Type will be overridden in case of returning file data, else we respond with JSON-formatted errors - w.Header().Set("Content-Type", "application/json") - - vars := mux.Vars(req) - writers.Download(w, req, gomatrixserverlib.ServerName(vars["serverName"]), types.MediaID(vars["mediaId"]), cfg, db, activeRemoteRequests) - })), + makeDownloadAPI("download", cfg, db, activeRemoteRequests, activeThumbnailGeneration), + ) + r0mux.Handle("/thumbnail/{serverName}/{mediaId}", + makeDownloadAPI("thumbnail", cfg, db, activeRemoteRequests, activeThumbnailGeneration), ) servMux.Handle("/metrics", prometheus.Handler()) servMux.Handle("/api/", http.StripPrefix("/api", apiMux)) } + +func makeDownloadAPI(name string, cfg *config.MediaAPI, db *storage.Database, activeRemoteRequests *types.ActiveRemoteRequests, activeThumbnailGeneration *types.ActiveThumbnailGeneration) http.HandlerFunc { + return prometheus.InstrumentHandler(name, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + req = util.RequestWithLogging(req) + + // Set common headers returned regardless of the outcome of the request + util.SetCORSHeaders(w) + // Content-Type will be overridden in case of returning file data, else we respond with JSON-formatted errors + w.Header().Set("Content-Type", "application/json") + + vars := mux.Vars(req) + writers.Download(w, req, gomatrixserverlib.ServerName(vars["serverName"]), types.MediaID(vars["mediaId"]), cfg, db, activeRemoteRequests, activeThumbnailGeneration, name == "thumbnail") + })) +} diff --git a/src/github.com/matrix-org/dendrite/mediaapi/storage/sql.go b/src/github.com/matrix-org/dendrite/mediaapi/storage/sql.go index e992e073..024ab8bb 100644 --- a/src/github.com/matrix-org/dendrite/mediaapi/storage/sql.go +++ b/src/github.com/matrix-org/dendrite/mediaapi/storage/sql.go @@ -19,13 +19,17 @@ import ( ) type statements struct { - mediaStatements + media mediaStatements + thumbnail thumbnailStatements } func (s *statements) prepare(db *sql.DB) error { var err error - if err = s.mediaStatements.prepare(db); err != nil { + if err = s.media.prepare(db); err != nil { + return err + } + if err = s.thumbnail.prepare(db); err != nil { return err } diff --git a/src/github.com/matrix-org/dendrite/mediaapi/storage/storage.go b/src/github.com/matrix-org/dendrite/mediaapi/storage/storage.go index cb27ccc9..93358382 100644 --- a/src/github.com/matrix-org/dendrite/mediaapi/storage/storage.go +++ b/src/github.com/matrix-org/dendrite/mediaapi/storage/storage.go @@ -45,16 +45,44 @@ func Open(dataSourceName string) (*Database, error) { // StoreMediaMetadata inserts the metadata about the uploaded media into the database. // Returns an error if the combination of MediaID and Origin are not unique in the table. func (d *Database) StoreMediaMetadata(mediaMetadata *types.MediaMetadata) error { - return d.statements.insertMedia(mediaMetadata) + return d.statements.media.insertMedia(mediaMetadata) } // GetMediaMetadata returns metadata about media stored on this server. // The media could have been uploaded to this server or fetched from another server and cached here. // Returns nil metadata if there is no metadata associated with this media. func (d *Database) GetMediaMetadata(mediaID types.MediaID, mediaOrigin gomatrixserverlib.ServerName) (*types.MediaMetadata, error) { - mediaMetadata, err := d.statements.selectMedia(mediaID, mediaOrigin) + mediaMetadata, err := d.statements.media.selectMedia(mediaID, mediaOrigin) if err != nil && err == sql.ErrNoRows { return nil, nil } return mediaMetadata, err } + +// StoreThumbnail inserts the metadata about the thumbnail into the database. +// Returns an error if the combination of MediaID and Origin are not unique in the table. +func (d *Database) StoreThumbnail(thumbnailMetadata *types.ThumbnailMetadata) error { + return d.statements.thumbnail.insertThumbnail(thumbnailMetadata) +} + +// GetThumbnail returns metadata about a specific thumbnail. +// The media could have been uploaded to this server or fetched from another server and cached here. +// Returns nil metadata if there is no metadata associated with this thumbnail. +func (d *Database) GetThumbnail(mediaID types.MediaID, mediaOrigin gomatrixserverlib.ServerName, width, height int, resizeMethod string) (*types.ThumbnailMetadata, error) { + thumbnailMetadata, err := d.statements.thumbnail.selectThumbnail(mediaID, mediaOrigin, width, height, resizeMethod) + if err != nil && err == sql.ErrNoRows { + return nil, nil + } + return thumbnailMetadata, err +} + +// GetThumbnails returns metadata about all thumbnails for a specific media stored on this server. +// The media could have been uploaded to this server or fetched from another server and cached here. +// Returns nil metadata if there are no thumbnails associated with this media. +func (d *Database) GetThumbnails(mediaID types.MediaID, mediaOrigin gomatrixserverlib.ServerName) ([]*types.ThumbnailMetadata, error) { + thumbnails, err := d.statements.thumbnail.selectThumbnails(mediaID, mediaOrigin) + if err != nil && err == sql.ErrNoRows { + return nil, nil + } + return thumbnails, err +} diff --git a/src/github.com/matrix-org/dendrite/mediaapi/storage/thumbnail_table.go b/src/github.com/matrix-org/dendrite/mediaapi/storage/thumbnail_table.go new file mode 100644 index 00000000..1284dbd4 --- /dev/null +++ b/src/github.com/matrix-org/dendrite/mediaapi/storage/thumbnail_table.go @@ -0,0 +1,157 @@ +// Copyright 2017 Vector Creations Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package storage + +import ( + "database/sql" + "time" + + "github.com/matrix-org/dendrite/mediaapi/types" + "github.com/matrix-org/gomatrixserverlib" +) + +const thumbnailSchema = ` +-- The thumbnail table holds metadata for each thumbnail file stored and accessible to the local server, +-- the actual file is stored separately. +CREATE TABLE IF NOT EXISTS thumbnail ( + -- The id used to refer to the media. + -- For uploads to this server this is a base64-encoded sha256 hash of the file data + -- For media from remote servers, this can be any unique identifier string + media_id TEXT NOT NULL, + -- The origin of the media as requested by the client. Should be a homeserver domain. + media_origin TEXT NOT NULL, + -- The MIME-type of the thumbnail file. + content_type TEXT NOT NULL, + -- Size of the thumbnail file in bytes. + file_size_bytes BIGINT NOT NULL, + -- When the thumbnail was generated in UNIX epoch ms. + creation_ts BIGINT NOT NULL, + -- The width of the thumbnail + width INTEGER NOT NULL, + -- The height of the thumbnail + height INTEGER NOT NULL, + -- The resize method used to generate the thumbnail. Can be crop or scale. + resize_method TEXT NOT NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS thumbnail_index ON thumbnail (media_id, media_origin, width, height, resize_method); +` + +const insertThumbnailSQL = ` +INSERT INTO thumbnail (media_id, media_origin, content_type, file_size_bytes, creation_ts, width, height, resize_method) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) +` + +// Note: this selects one specific thumbnail +const selectThumbnailSQL = ` +SELECT content_type, file_size_bytes, creation_ts FROM thumbnail WHERE media_id = $1 AND media_origin = $2 AND width = $3 AND height = $4 AND resize_method = $5 +` + +// Note: this selects all thumbnails for a media_origin and media_id +const selectThumbnailsSQL = ` +SELECT content_type, file_size_bytes, creation_ts, width, height, resize_method FROM thumbnail WHERE media_id = $1 AND media_origin = $2 +` + +type thumbnailStatements struct { + insertThumbnailStmt *sql.Stmt + selectThumbnailStmt *sql.Stmt + selectThumbnailsStmt *sql.Stmt +} + +func (s *thumbnailStatements) prepare(db *sql.DB) (err error) { + _, err = db.Exec(thumbnailSchema) + if err != nil { + return + } + + return statementList{ + {&s.insertThumbnailStmt, insertThumbnailSQL}, + {&s.selectThumbnailStmt, selectThumbnailSQL}, + {&s.selectThumbnailsStmt, selectThumbnailsSQL}, + }.prepare(db) +} + +func (s *thumbnailStatements) insertThumbnail(thumbnailMetadata *types.ThumbnailMetadata) error { + thumbnailMetadata.MediaMetadata.CreationTimestamp = types.UnixMs(time.Now().UnixNano() / 1000000) + _, err := s.insertThumbnailStmt.Exec( + thumbnailMetadata.MediaMetadata.MediaID, + thumbnailMetadata.MediaMetadata.Origin, + thumbnailMetadata.MediaMetadata.ContentType, + thumbnailMetadata.MediaMetadata.FileSizeBytes, + thumbnailMetadata.MediaMetadata.CreationTimestamp, + thumbnailMetadata.ThumbnailSize.Width, + thumbnailMetadata.ThumbnailSize.Height, + thumbnailMetadata.ThumbnailSize.ResizeMethod, + ) + return err +} + +func (s *thumbnailStatements) selectThumbnail(mediaID types.MediaID, mediaOrigin gomatrixserverlib.ServerName, width, height int, resizeMethod string) (*types.ThumbnailMetadata, error) { + thumbnailMetadata := types.ThumbnailMetadata{ + MediaMetadata: &types.MediaMetadata{ + MediaID: mediaID, + Origin: mediaOrigin, + }, + ThumbnailSize: types.ThumbnailSize{ + Width: width, + Height: height, + ResizeMethod: resizeMethod, + }, + } + err := s.selectThumbnailStmt.QueryRow( + thumbnailMetadata.MediaMetadata.MediaID, + thumbnailMetadata.MediaMetadata.Origin, + thumbnailMetadata.ThumbnailSize.Width, + thumbnailMetadata.ThumbnailSize.Height, + thumbnailMetadata.ThumbnailSize.ResizeMethod, + ).Scan( + &thumbnailMetadata.MediaMetadata.ContentType, + &thumbnailMetadata.MediaMetadata.FileSizeBytes, + &thumbnailMetadata.MediaMetadata.CreationTimestamp, + ) + return &thumbnailMetadata, err +} + +func (s *thumbnailStatements) selectThumbnails(mediaID types.MediaID, mediaOrigin gomatrixserverlib.ServerName) ([]*types.ThumbnailMetadata, error) { + rows, err := s.selectThumbnailsStmt.Query( + mediaID, mediaOrigin, + ) + if err != nil { + return nil, err + } + + var thumbnails []*types.ThumbnailMetadata + for rows.Next() { + thumbnailMetadata := types.ThumbnailMetadata{ + MediaMetadata: &types.MediaMetadata{ + MediaID: mediaID, + Origin: mediaOrigin, + }, + } + err = rows.Scan( + &thumbnailMetadata.MediaMetadata.ContentType, + &thumbnailMetadata.MediaMetadata.FileSizeBytes, + &thumbnailMetadata.MediaMetadata.CreationTimestamp, + &thumbnailMetadata.ThumbnailSize.Width, + &thumbnailMetadata.ThumbnailSize.Height, + &thumbnailMetadata.ThumbnailSize.ResizeMethod, + ) + if err != nil { + return nil, err + } + thumbnails = append(thumbnails, &thumbnailMetadata) + } + + return thumbnails, err +} diff --git a/src/github.com/matrix-org/dendrite/mediaapi/thumbnailer/thumbnailer.go b/src/github.com/matrix-org/dendrite/mediaapi/thumbnailer/thumbnailer.go new file mode 100644 index 00000000..0a1e507e --- /dev/null +++ b/src/github.com/matrix-org/dendrite/mediaapi/thumbnailer/thumbnailer.go @@ -0,0 +1,221 @@ +// Copyright 2017 Vector Creations Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package thumbnailer + +import ( + "fmt" + "math" + "path/filepath" + "sync" + + log "github.com/Sirupsen/logrus" + "github.com/matrix-org/dendrite/mediaapi/types" +) + +type thumbnailFitness struct { + isSmaller int + aspect float64 + size float64 + methodMismatch int + fileSize types.FileSizeBytes +} + +// thumbnailTemplate is the filename template for thumbnails +const thumbnailTemplate = "thumbnail-%vx%v-%v" + +// GetThumbnailPath returns the path to a thumbnail given the absolute src path and thumbnail size configuration +func GetThumbnailPath(src types.Path, config types.ThumbnailSize) types.Path { + srcDir := filepath.Dir(string(src)) + return types.Path(filepath.Join( + srcDir, + fmt.Sprintf(thumbnailTemplate, config.Width, config.Height, config.ResizeMethod), + )) +} + +// SelectThumbnail compares the (potentially) available thumbnails with the desired thumbnail and returns the best match +// The algorithm is very similar to what was implemented in Synapse +// In order of priority unless absolute, the following metrics are compared; the image is: +// * the same size or larger than requested +// * if a cropped image is desired, has an aspect ratio close to requested +// * has a size close to requested +// * if a cropped image is desired, prefer the same method, if scaled is desired, absolutely require scaled +// * has a small file size +// If a pre-generated thumbnail size is the best match, but it has not been generated yet, the caller can use the returned size to generate it. +// Returns nil if no thumbnail matches the criteria +func SelectThumbnail(desired types.ThumbnailSize, thumbnails []*types.ThumbnailMetadata, thumbnailSizes []types.ThumbnailSize) (*types.ThumbnailMetadata, *types.ThumbnailSize) { + var chosenThumbnail *types.ThumbnailMetadata + var chosenThumbnailSize *types.ThumbnailSize + bestFit := newThumbnailFitness() + + for _, thumbnail := range thumbnails { + if desired.ResizeMethod == "scale" && thumbnail.ThumbnailSize.ResizeMethod != "scale" { + continue + } + fitness := calcThumbnailFitness(thumbnail.ThumbnailSize, thumbnail.MediaMetadata, desired) + if isBetter := fitness.betterThan(bestFit, desired.ResizeMethod == "crop"); isBetter { + bestFit = fitness + chosenThumbnail = thumbnail + } + } + + for _, thumbnailSize := range thumbnailSizes { + if desired.ResizeMethod == "scale" && thumbnailSize.ResizeMethod != "scale" { + continue + } + fitness := calcThumbnailFitness(thumbnailSize, nil, desired) + if isBetter := fitness.betterThan(bestFit, desired.ResizeMethod == "crop"); isBetter { + bestFit = fitness + chosenThumbnailSize = &types.ThumbnailSize{ + Width: thumbnailSize.Width, + Height: thumbnailSize.Height, + ResizeMethod: thumbnailSize.ResizeMethod, + } + } + } + + return chosenThumbnail, chosenThumbnailSize +} + +// getActiveThumbnailGeneration checks for active thumbnail generation +func getActiveThumbnailGeneration(dst types.Path, config types.ThumbnailSize, activeThumbnailGeneration *types.ActiveThumbnailGeneration, maxThumbnailGenerators int, logger *log.Entry) (isActive bool, busy bool, errorReturn error) { + // Check if there is active thumbnail generation. + activeThumbnailGeneration.Lock() + defer activeThumbnailGeneration.Unlock() + if activeThumbnailGenerationResult, ok := activeThumbnailGeneration.PathToResult[string(dst)]; ok { + logger.Info("Waiting for another goroutine to generate the thumbnail.") + + // NOTE: Wait unlocks and locks again internally. There is still a deferred Unlock() that will unlock this. + activeThumbnailGenerationResult.Cond.Wait() + // Note: either there is an error or it is nil, either way returning it is correct + return false, false, activeThumbnailGenerationResult.Err + } + + // Only allow thumbnail generation up to a maximum configured number. Above this we fall back to serving the + // original. Or in the case of pre-generation, they maybe get generated on the first request for a thumbnail if + // load has subsided. + if len(activeThumbnailGeneration.PathToResult) >= maxThumbnailGenerators { + return false, true, nil + } + + // No active thumbnail generation so create one + activeThumbnailGeneration.PathToResult[string(dst)] = &types.ThumbnailGenerationResult{ + Cond: &sync.Cond{L: activeThumbnailGeneration}, + } + + return true, false, nil +} + +// broadcastGeneration broadcasts that thumbnail generation completed and the error to all waiting goroutines +// Note: This should only be called by the owner of the activeThumbnailGenerationResult +func broadcastGeneration(dst types.Path, activeThumbnailGeneration *types.ActiveThumbnailGeneration, config types.ThumbnailSize, errorReturn error, logger *log.Entry) { + activeThumbnailGeneration.Lock() + defer activeThumbnailGeneration.Unlock() + if activeThumbnailGenerationResult, ok := activeThumbnailGeneration.PathToResult[string(dst)]; ok { + logger.Info("Signalling other goroutines waiting for this goroutine to generate the thumbnail.") + // Note: errorReturn is a named return value error that is signalled from here to waiting goroutines + activeThumbnailGenerationResult.Err = errorReturn + activeThumbnailGenerationResult.Cond.Broadcast() + } + delete(activeThumbnailGeneration.PathToResult, string(dst)) +} + +// init with worst values +func newThumbnailFitness() thumbnailFitness { + return thumbnailFitness{ + isSmaller: 1, + aspect: math.Inf(1), + size: math.Inf(1), + methodMismatch: 0, + fileSize: types.FileSizeBytes(math.MaxInt64), + } +} + +func calcThumbnailFitness(size types.ThumbnailSize, metadata *types.MediaMetadata, desired types.ThumbnailSize) thumbnailFitness { + dW := desired.Width + dH := desired.Height + tW := size.Width + tH := size.Height + + fitness := newThumbnailFitness() + // In all cases, a larger metric value is a worse fit. + // compare size: thumbnail smaller is true and gives 1, larger is false and gives 0 + fitness.isSmaller = boolToInt(tW < dW || tH < dH) + // comparison of aspect ratios only makes sense for a request for desired cropped + fitness.aspect = math.Abs(float64(dW*tH - dH*tW)) + // compare sizes + fitness.size = math.Abs(float64((dW - tW) * (dH - tH))) + // compare resize method + fitness.methodMismatch = boolToInt(size.ResizeMethod != desired.ResizeMethod) + if metadata != nil { + // file size + fitness.fileSize = metadata.FileSizeBytes + } + + return fitness +} + +func boolToInt(b bool) int { + if b { + return 1 + } + return 0 +} + +func (a thumbnailFitness) betterThan(b thumbnailFitness, desiredCrop bool) bool { + // preference means returning -1 + + // prefer images that are not smaller + // e.g. isSmallerDiff > 0 means b is smaller than desired and a is not smaller + if a.isSmaller > b.isSmaller { + return false + } else if a.isSmaller < b.isSmaller { + return true + } + + // prefer aspect ratios closer to desired only if desired cropped + // only cropped images have differing aspect ratios + // desired scaled only accepts scaled images + if desiredCrop { + if a.aspect > b.aspect { + return false + } else if a.aspect < b.aspect { + return true + } + } + + // prefer closer in size + if a.size > b.size { + return false + } else if a.size < b.size { + return true + } + + // prefer images using the same method + // e.g. methodMismatchDiff > 0 means b's method is different from desired and a's matches the desired method + if a.methodMismatch > b.methodMismatch { + return false + } else if a.methodMismatch < b.methodMismatch { + return true + } + + // prefer smaller files + if a.fileSize > b.fileSize { + return false + } else if a.fileSize < b.fileSize { + return true + } + + return false +} diff --git a/src/github.com/matrix-org/dendrite/mediaapi/thumbnailer/thumbnailer_bimg.go b/src/github.com/matrix-org/dendrite/mediaapi/thumbnailer/thumbnailer_bimg.go new file mode 100644 index 00000000..2880c47d --- /dev/null +++ b/src/github.com/matrix-org/dendrite/mediaapi/thumbnailer/thumbnailer_bimg.go @@ -0,0 +1,217 @@ +// Copyright 2017 Vector Creations Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build bimg + +package thumbnailer + +import ( + "fmt" + "os" + "time" + + log "github.com/Sirupsen/logrus" + "github.com/matrix-org/dendrite/mediaapi/storage" + "github.com/matrix-org/dendrite/mediaapi/types" + "gopkg.in/h2non/bimg.v1" +) + +// GenerateThumbnails generates the configured thumbnail sizes for the source file +func GenerateThumbnails(src types.Path, configs []types.ThumbnailSize, mediaMetadata *types.MediaMetadata, activeThumbnailGeneration *types.ActiveThumbnailGeneration, maxThumbnailGenerators int, db *storage.Database, logger *log.Entry) (busy bool, errorReturn error) { + buffer, err := bimg.Read(string(src)) + if err != nil { + logger.WithError(err).WithField("src", src).Error("Failed to read src file") + return false, err + } + for _, config := range configs { + // Note: createThumbnail does locking based on activeThumbnailGeneration + busy, err = createThumbnail(src, buffer, config, mediaMetadata, activeThumbnailGeneration, maxThumbnailGenerators, db, logger) + if err != nil { + logger.WithError(err).WithField("src", src).Error("Failed to generate thumbnails") + return false, err + } + if busy { + return true, nil + } + } + return false, nil +} + +// GenerateThumbnail generates the configured thumbnail size for the source file +func GenerateThumbnail(src types.Path, config types.ThumbnailSize, mediaMetadata *types.MediaMetadata, activeThumbnailGeneration *types.ActiveThumbnailGeneration, maxThumbnailGenerators int, db *storage.Database, logger *log.Entry) (busy bool, errorReturn error) { + buffer, err := bimg.Read(string(src)) + if err != nil { + logger.WithError(err).WithFields(log.Fields{ + "src": src, + }).Error("Failed to read src file") + return false, err + } + // Note: createThumbnail does locking based on activeThumbnailGeneration + busy, err = createThumbnail(src, buffer, config, mediaMetadata, activeThumbnailGeneration, maxThumbnailGenerators, db, logger) + if err != nil { + logger.WithError(err).WithFields(log.Fields{ + "src": src, + }).Error("Failed to generate thumbnails") + return false, err + } + if busy { + return true, nil + } + return false, nil +} + +// createThumbnail checks if the thumbnail exists, and if not, generates it +// Thumbnail generation is only done once for each non-existing thumbnail. +func createThumbnail(src types.Path, buffer []byte, config types.ThumbnailSize, mediaMetadata *types.MediaMetadata, activeThumbnailGeneration *types.ActiveThumbnailGeneration, maxThumbnailGenerators int, db *storage.Database, logger *log.Entry) (busy bool, errorReturn error) { + logger = logger.WithFields(log.Fields{ + "Width": config.Width, + "Height": config.Height, + "ResizeMethod": config.ResizeMethod, + }) + + dst := GetThumbnailPath(src, config) + + // Note: getActiveThumbnailGeneration uses mutexes and conditions from activeThumbnailGeneration + isActive, busy, err := getActiveThumbnailGeneration(dst, config, activeThumbnailGeneration, maxThumbnailGenerators, logger) + if err != nil { + return false, err + } + if busy { + return true, nil + } + + if isActive { + // Note: This is an active request that MUST broadcastGeneration to wake up waiting goroutines! + // Note: broadcastGeneration uses mutexes and conditions from activeThumbnailGeneration + defer func() { + // Note: errorReturn is the named return variable so we wrap this in a closure to re-evaluate the arguments at defer-time + if err := recover(); err != nil { + broadcastGeneration(dst, activeThumbnailGeneration, config, err.(error), logger) + panic(err) + } + broadcastGeneration(dst, activeThumbnailGeneration, config, errorReturn, logger) + }() + } + + // Check if the thumbnail exists. + thumbnailMetadata, err := db.GetThumbnail(mediaMetadata.MediaID, mediaMetadata.Origin, config.Width, config.Height, config.ResizeMethod) + if err != nil { + logger.Error("Failed to query database for thumbnail.") + return false, err + } + if thumbnailMetadata != nil { + return false, nil + } + // Note: The double-negative is intentional as os.IsExist(err) != !os.IsNotExist(err). + // The functions are error checkers to be used in different cases. + if _, err = os.Stat(string(dst)); !os.IsNotExist(err) { + // Thumbnail exists + return false, nil + } + + if isActive == false { + // Note: This should not happen, but we check just in case. + logger.Error("Failed to stat file but this is not the active thumbnail generator. This should not happen.") + return false, fmt.Errorf("Not active thumbnail generator. Stat error: %q", err) + } + + start := time.Now() + width, height, err := resize(dst, buffer, config.Width, config.Height, config.ResizeMethod == "crop", logger) + if err != nil { + return false, err + } + logger.WithFields(log.Fields{ + "ActualWidth": width, + "ActualHeight": height, + "processTime": time.Now().Sub(start), + }).Info("Generated thumbnail") + + stat, err := os.Stat(string(dst)) + if err != nil { + return false, err + } + + thumbnailMetadata = &types.ThumbnailMetadata{ + MediaMetadata: &types.MediaMetadata{ + MediaID: mediaMetadata.MediaID, + Origin: mediaMetadata.Origin, + // Note: the code currently always creates a JPEG thumbnail + ContentType: types.ContentType("image/jpeg"), + FileSizeBytes: types.FileSizeBytes(stat.Size()), + }, + ThumbnailSize: types.ThumbnailSize{ + Width: width, + Height: height, + ResizeMethod: config.ResizeMethod, + }, + } + + err = db.StoreThumbnail(thumbnailMetadata) + if err != nil { + logger.WithError(err).WithFields(log.Fields{ + "ActualWidth": width, + "ActualHeight": height, + }).Error("Failed to store thumbnail metadata in database.") + return false, err + } + + return false, nil +} + +// resize scales an image to fit within the provided width and height +// If the source aspect ratio is different to the target dimensions, one edge will be smaller than requested +// If crop is set to true, the image will be scaled to fill the width and height with any excess being cropped off +func resize(dst types.Path, buffer []byte, w, h int, crop bool, logger *log.Entry) (int, int, error) { + inImage := bimg.NewImage(buffer) + + inSize, err := inImage.Size() + if err != nil { + return -1, -1, err + } + + options := bimg.Options{ + Type: bimg.JPEG, + Quality: 85, + } + if crop { + options.Width = w + options.Height = h + options.Crop = true + } else { + inAR := float64(inSize.Width) / float64(inSize.Height) + outAR := float64(w) / float64(h) + + if inAR > outAR { + // input has wider AR than requested output so use requested width and calculate height to match input AR + options.Width = w + options.Height = int(float64(w) / inAR) + } else { + // input has narrower AR than requested output so use requested height and calculate width to match input AR + options.Width = int(float64(h) * inAR) + options.Height = h + } + } + + newImage, err := inImage.Process(options) + if err != nil { + return -1, -1, err + } + + if err = bimg.Write(string(dst), newImage); err != nil { + logger.WithError(err).Error("Failed to resize image") + return -1, -1, err + } + + return options.Width, options.Height, nil +} diff --git a/src/github.com/matrix-org/dendrite/mediaapi/thumbnailer/thumbnailer_nfnt.go b/src/github.com/matrix-org/dendrite/mediaapi/thumbnailer/thumbnailer_nfnt.go new file mode 100644 index 00000000..c342bb98 --- /dev/null +++ b/src/github.com/matrix-org/dendrite/mediaapi/thumbnailer/thumbnailer_nfnt.go @@ -0,0 +1,249 @@ +// Copyright 2017 Vector Creations Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build !bimg + +package thumbnailer + +import ( + "fmt" + "image" + "image/draw" + // Imported for gif codec + _ "image/gif" + "image/jpeg" + // Imported for png codec + _ "image/png" + "os" + "time" + + log "github.com/Sirupsen/logrus" + "github.com/matrix-org/dendrite/mediaapi/storage" + "github.com/matrix-org/dendrite/mediaapi/types" + "github.com/nfnt/resize" +) + +// GenerateThumbnails generates the configured thumbnail sizes for the source file +func GenerateThumbnails(src types.Path, configs []types.ThumbnailSize, mediaMetadata *types.MediaMetadata, activeThumbnailGeneration *types.ActiveThumbnailGeneration, maxThumbnailGenerators int, db *storage.Database, logger *log.Entry) (busy bool, errorReturn error) { + img, err := readFile(string(src)) + if err != nil { + logger.WithError(err).WithField("src", src).Error("Failed to read src file") + return false, err + } + for _, config := range configs { + // Note: createThumbnail does locking based on activeThumbnailGeneration + busy, err = createThumbnail(src, img, config, mediaMetadata, activeThumbnailGeneration, maxThumbnailGenerators, db, logger) + if err != nil { + logger.WithError(err).WithField("src", src).Error("Failed to generate thumbnails") + return false, err + } + if busy { + return true, nil + } + } + return false, nil +} + +// GenerateThumbnail generates the configured thumbnail size for the source file +func GenerateThumbnail(src types.Path, config types.ThumbnailSize, mediaMetadata *types.MediaMetadata, activeThumbnailGeneration *types.ActiveThumbnailGeneration, maxThumbnailGenerators int, db *storage.Database, logger *log.Entry) (busy bool, errorReturn error) { + img, err := readFile(string(src)) + if err != nil { + logger.WithError(err).WithFields(log.Fields{ + "src": src, + }).Error("Failed to read src file") + return false, err + } + // Note: createThumbnail does locking based on activeThumbnailGeneration + busy, err = createThumbnail(src, img, config, mediaMetadata, activeThumbnailGeneration, maxThumbnailGenerators, db, logger) + if err != nil { + logger.WithError(err).WithFields(log.Fields{ + "src": src, + }).Error("Failed to generate thumbnails") + return false, err + } + if busy { + return true, nil + } + return false, nil +} + +func readFile(src string) (image.Image, error) { + file, err := os.Open(src) + if err != nil { + return nil, err + } + defer file.Close() + + img, _, err := image.Decode(file) + if err != nil { + return nil, err + } + + return img, nil +} + +func writeFile(img image.Image, dst string) error { + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + return jpeg.Encode(out, img, &jpeg.Options{ + Quality: 85, + }) +} + +// createThumbnail checks if the thumbnail exists, and if not, generates it +// Thumbnail generation is only done once for each non-existing thumbnail. +func createThumbnail(src types.Path, img image.Image, config types.ThumbnailSize, mediaMetadata *types.MediaMetadata, activeThumbnailGeneration *types.ActiveThumbnailGeneration, maxThumbnailGenerators int, db *storage.Database, logger *log.Entry) (busy bool, errorReturn error) { + logger = logger.WithFields(log.Fields{ + "Width": config.Width, + "Height": config.Height, + "ResizeMethod": config.ResizeMethod, + }) + + dst := GetThumbnailPath(src, config) + + // Note: getActiveThumbnailGeneration uses mutexes and conditions from activeThumbnailGeneration + isActive, busy, err := getActiveThumbnailGeneration(dst, config, activeThumbnailGeneration, maxThumbnailGenerators, logger) + if err != nil { + return false, err + } + if busy { + return true, nil + } + + if isActive { + // Note: This is an active request that MUST broadcastGeneration to wake up waiting goroutines! + // Note: broadcastGeneration uses mutexes and conditions from activeThumbnailGeneration + defer func() { + // Note: errorReturn is the named return variable so we wrap this in a closure to re-evaluate the arguments at defer-time + // if err := recover(); err != nil { + // broadcastGeneration(dst, activeThumbnailGeneration, config, err.(error), logger) + // panic(err) + // } + broadcastGeneration(dst, activeThumbnailGeneration, config, errorReturn, logger) + }() + } + + // Check if the thumbnail exists. + thumbnailMetadata, err := db.GetThumbnail(mediaMetadata.MediaID, mediaMetadata.Origin, config.Width, config.Height, config.ResizeMethod) + if err != nil { + logger.Error("Failed to query database for thumbnail.") + return false, err + } + if thumbnailMetadata != nil { + return false, nil + } + // Note: The double-negative is intentional as os.IsExist(err) != !os.IsNotExist(err). + // The functions are error checkers to be used in different cases. + if _, err = os.Stat(string(dst)); !os.IsNotExist(err) { + // Thumbnail exists + return false, nil + } + + if isActive == false { + // Note: This should not happen, but we check just in case. + logger.Error("Failed to stat file but this is not the active thumbnail generator. This should not happen.") + return false, fmt.Errorf("Not active thumbnail generator. Stat error: %q", err) + } + + start := time.Now() + width, height, err := adjustSize(dst, img, config.Width, config.Height, config.ResizeMethod == "crop", logger) + if err != nil { + return false, err + } + logger.WithFields(log.Fields{ + "ActualWidth": width, + "ActualHeight": height, + "processTime": time.Now().Sub(start), + }).Info("Generated thumbnail") + + stat, err := os.Stat(string(dst)) + if err != nil { + return false, err + } + + thumbnailMetadata = &types.ThumbnailMetadata{ + MediaMetadata: &types.MediaMetadata{ + MediaID: mediaMetadata.MediaID, + Origin: mediaMetadata.Origin, + // Note: the code currently always creates a JPEG thumbnail + ContentType: types.ContentType("image/jpeg"), + FileSizeBytes: types.FileSizeBytes(stat.Size()), + }, + ThumbnailSize: types.ThumbnailSize{ + Width: width, + Height: height, + ResizeMethod: config.ResizeMethod, + }, + } + + err = db.StoreThumbnail(thumbnailMetadata) + if err != nil { + logger.WithError(err).WithFields(log.Fields{ + "ActualWidth": width, + "ActualHeight": height, + }).Error("Failed to store thumbnail metadata in database.") + return false, err + } + + return false, nil +} + +// adjustSize scales an image to fit within the provided width and height +// If the source aspect ratio is different to the target dimensions, one edge will be smaller than requested +// If crop is set to true, the image will be scaled to fill the width and height with any excess being cropped off +func adjustSize(dst types.Path, img image.Image, w, h int, crop bool, logger *log.Entry) (int, int, error) { + var out image.Image + var err error + if crop { + inAR := float64(img.Bounds().Dx()) / float64(img.Bounds().Dy()) + outAR := float64(w) / float64(h) + + var scaleW, scaleH uint + if inAR > outAR { + // input has shorter AR than requested output so use requested height and calculate width to match input AR + scaleW = uint(float64(h) * inAR) + scaleH = uint(h) + } else { + // input has taller AR than requested output so use requested width and calculate height to match input AR + scaleW = uint(w) + scaleH = uint(float64(w) / inAR) + } + + scaled := resize.Resize(scaleW, scaleH, img, resize.Lanczos3) + + xoff := (scaled.Bounds().Dx() - w) / 2 + yoff := (scaled.Bounds().Dy() - h) / 2 + + tr := image.Rect(0, 0, w, h) + target := image.NewRGBA(tr) + draw.Draw(target, tr, scaled, image.Pt(xoff, yoff), draw.Src) + out = target + } else { + out = resize.Thumbnail(uint(w), uint(h), img, resize.Lanczos3) + if err != nil { + return -1, -1, err + } + } + + if err = writeFile(out, string(dst)); err != nil { + logger.WithError(err).Error("Failed to encode and write image") + return -1, -1, err + } + + return out.Bounds().Max.X, out.Bounds().Max.Y, nil +} diff --git a/src/github.com/matrix-org/dendrite/mediaapi/types/types.go b/src/github.com/matrix-org/dendrite/mediaapi/types/types.go index d54bcdf6..3a0888f8 100644 --- a/src/github.com/matrix-org/dendrite/mediaapi/types/types.go +++ b/src/github.com/matrix-org/dendrite/mediaapi/types/types.go @@ -77,3 +77,37 @@ type ActiveRemoteRequests struct { // The string key is an mxc:// URL MXCToResult map[string]*RemoteRequestResult } + +// ThumbnailSize contains a single thumbnail size configuration +type ThumbnailSize struct { + // Maximum width of the thumbnail image + Width int `yaml:"width"` + // Maximum height of the thumbnail image + Height int `yaml:"height"` + // ResizeMethod is one of crop or scale. + // crop scales to fill the requested dimensions and crops the excess. + // scale scales to fit the requested dimensions and one dimension may be smaller than requested. + ResizeMethod string `yaml:"method,omitempty"` +} + +// ThumbnailMetadata contains the metadata about an individual thumbnail +type ThumbnailMetadata struct { + MediaMetadata *MediaMetadata + ThumbnailSize ThumbnailSize +} + +// ThumbnailGenerationResult is used for broadcasting the result of thumbnail generation to routines waiting on the condition +type ThumbnailGenerationResult struct { + // Condition used for the generator to signal the result to all other routines waiting on this condition + Cond *sync.Cond + // Resulting error from the generation attempt + Err error +} + +// ActiveThumbnailGeneration is a lockable map of file paths being thumbnailed +// It is used to ensure thumbnails are only generated once. +type ActiveThumbnailGeneration struct { + sync.Mutex + // The string key is a thumbnail file path + PathToResult map[string]*ThumbnailGenerationResult +} diff --git a/src/github.com/matrix-org/dendrite/mediaapi/writers/download.go b/src/github.com/matrix-org/dendrite/mediaapi/writers/download.go index af31535b..90f6ca35 100644 --- a/src/github.com/matrix-org/dendrite/mediaapi/writers/download.go +++ b/src/github.com/matrix-org/dendrite/mediaapi/writers/download.go @@ -24,6 +24,7 @@ import ( "path/filepath" "regexp" "strconv" + "strings" "sync" log "github.com/Sirupsen/logrus" @@ -31,6 +32,7 @@ import ( "github.com/matrix-org/dendrite/mediaapi/config" "github.com/matrix-org/dendrite/mediaapi/fileutils" "github.com/matrix-org/dendrite/mediaapi/storage" + "github.com/matrix-org/dendrite/mediaapi/thumbnailer" "github.com/matrix-org/dendrite/mediaapi/types" "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/util" @@ -41,31 +43,56 @@ const mediaIDCharacters = "A-Za-z0-9_=-" // Note: unfortunately regex.MustCompile() cannot be assigned to a const var mediaIDRegex = regexp.MustCompile("[" + mediaIDCharacters + "]+") -// downloadRequest metadata included in or derivable from an download request +// downloadRequest metadata included in or derivable from a download or thumbnail request // https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-media-r0-download-servername-mediaid +// http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-media-r0-thumbnail-servername-mediaid type downloadRequest struct { - MediaMetadata *types.MediaMetadata - Logger *log.Entry + MediaMetadata *types.MediaMetadata + IsThumbnailRequest bool + ThumbnailSize types.ThumbnailSize + Logger *log.Entry } -// Download implements /download +// Download implements /download amd /thumbnail // Files from this server (i.e. origin == cfg.ServerName) are served directly // Files from remote servers (i.e. origin != cfg.ServerName) are cached locally. // If they are present in the cache, they are served directly. // If they are not present in the cache, they are obtained from the remote server and // simultaneously served back to the client and written into the cache. -func Download(w http.ResponseWriter, req *http.Request, origin gomatrixserverlib.ServerName, mediaID types.MediaID, cfg *config.MediaAPI, db *storage.Database, activeRemoteRequests *types.ActiveRemoteRequests) { +func Download(w http.ResponseWriter, req *http.Request, origin gomatrixserverlib.ServerName, mediaID types.MediaID, cfg *config.MediaAPI, db *storage.Database, activeRemoteRequests *types.ActiveRemoteRequests, activeThumbnailGeneration *types.ActiveThumbnailGeneration, isThumbnailRequest bool) { r := &downloadRequest{ MediaMetadata: &types.MediaMetadata{ MediaID: mediaID, Origin: origin, }, + IsThumbnailRequest: isThumbnailRequest, Logger: util.GetLogger(req.Context()).WithFields(log.Fields{ "Origin": origin, "MediaID": mediaID, }), } + if r.IsThumbnailRequest { + width, err := strconv.Atoi(req.FormValue("width")) + if err != nil { + width = -1 + } + height, err := strconv.Atoi(req.FormValue("height")) + if err != nil { + height = -1 + } + r.ThumbnailSize = types.ThumbnailSize{ + Width: width, + Height: height, + ResizeMethod: strings.ToLower(req.FormValue("method")), + } + r.Logger.WithFields(log.Fields{ + "RequestedWidth": r.ThumbnailSize.Width, + "RequestedHeight": r.ThumbnailSize.Height, + "RequestedResizeMethod": r.ThumbnailSize.ResizeMethod, + }) + } + // request validation if req.Method != "GET" { r.jsonErrorResponse(w, util.JSONResponse{ @@ -80,7 +107,7 @@ func Download(w http.ResponseWriter, req *http.Request, origin gomatrixserverlib return } - if resErr := r.doDownload(w, cfg, db, activeRemoteRequests); resErr != nil { + if resErr := r.doDownload(w, cfg, db, activeRemoteRequests, activeThumbnailGeneration); resErr != nil { r.jsonErrorResponse(w, *resErr) return } @@ -118,10 +145,29 @@ func (r *downloadRequest) Validate() *util.JSONResponse { JSON: jsonerror.NotFound("serverName must be a non-empty string"), } } + + if r.IsThumbnailRequest { + if r.ThumbnailSize.Width <= 0 || r.ThumbnailSize.Height <= 0 { + return &util.JSONResponse{ + Code: 400, + JSON: jsonerror.Unknown("width and height must be greater than 0"), + } + } + // Default method to scale if not set + if r.ThumbnailSize.ResizeMethod == "" { + r.ThumbnailSize.ResizeMethod = "scale" + } + if r.ThumbnailSize.ResizeMethod != "crop" && r.ThumbnailSize.ResizeMethod != "scale" { + return &util.JSONResponse{ + Code: 400, + JSON: jsonerror.Unknown("method must be one of crop or scale"), + } + } + } return nil } -func (r *downloadRequest) doDownload(w http.ResponseWriter, cfg *config.MediaAPI, db *storage.Database, activeRemoteRequests *types.ActiveRemoteRequests) *util.JSONResponse { +func (r *downloadRequest) doDownload(w http.ResponseWriter, cfg *config.MediaAPI, db *storage.Database, activeRemoteRequests *types.ActiveRemoteRequests, activeThumbnailGeneration *types.ActiveThumbnailGeneration) *util.JSONResponse { // check if we have a record of the media in our database mediaMetadata, err := db.GetMediaMetadata(r.MediaMetadata.MediaID, r.MediaMetadata.Origin) if err != nil { @@ -138,7 +184,7 @@ func (r *downloadRequest) doDownload(w http.ResponseWriter, cfg *config.MediaAPI } } // If we do not have a record and the origin is remote, we need to fetch it and respond with that file - resErr := r.getRemoteFile(cfg, db, activeRemoteRequests) + resErr := r.getRemoteFile(cfg, db, activeRemoteRequests, activeThumbnailGeneration) if resErr != nil { return resErr } @@ -146,12 +192,12 @@ func (r *downloadRequest) doDownload(w http.ResponseWriter, cfg *config.MediaAPI // If we have a record, we can respond from the local file r.MediaMetadata = mediaMetadata } - return r.respondFromLocalFile(w, cfg.AbsBasePath) + return r.respondFromLocalFile(w, cfg.AbsBasePath, activeThumbnailGeneration, cfg.MaxThumbnailGenerators, db, cfg.DynamicThumbnails, cfg.ThumbnailSizes) } // respondFromLocalFile reads a file from local storage and writes it to the http.ResponseWriter // Returns a util.JSONResponse error in case of error -func (r *downloadRequest) respondFromLocalFile(w http.ResponseWriter, absBasePath types.Path) *util.JSONResponse { +func (r *downloadRequest) respondFromLocalFile(w http.ResponseWriter, absBasePath types.Path, activeThumbnailGeneration *types.ActiveThumbnailGeneration, maxThumbnailGenerators int, db *storage.Database, dynamicThumbnails bool, thumbnailSizes []types.ThumbnailSize) *util.JSONResponse { filePath, err := fileutils.GetPathFromBase64Hash(r.MediaMetadata.Base64Hash, absBasePath) if err != nil { r.Logger.WithError(err).Error("Failed to get file path from metadata") @@ -181,15 +227,43 @@ func (r *downloadRequest) respondFromLocalFile(w http.ResponseWriter, absBasePat return &resErr } - r.Logger.WithFields(log.Fields{ - "UploadName": r.MediaMetadata.UploadName, - "Base64Hash": r.MediaMetadata.Base64Hash, - "FileSizeBytes": r.MediaMetadata.FileSizeBytes, - "Content-Type": r.MediaMetadata.ContentType, - }).Info("Responding with file") + var responseFile *os.File + var responseMetadata *types.MediaMetadata + if r.IsThumbnailRequest { + thumbFile, thumbMetadata, resErr := r.getThumbnailFile(types.Path(filePath), activeThumbnailGeneration, maxThumbnailGenerators, db, dynamicThumbnails, thumbnailSizes) + if thumbFile != nil { + defer thumbFile.Close() + } + if resErr != nil { + return resErr + } + if thumbFile == nil { + r.Logger.WithFields(log.Fields{ + "UploadName": r.MediaMetadata.UploadName, + "Base64Hash": r.MediaMetadata.Base64Hash, + "FileSizeBytes": r.MediaMetadata.FileSizeBytes, + "ContentType": r.MediaMetadata.ContentType, + }).Info("No good thumbnail found. Responding with original file.") + responseFile = file + responseMetadata = r.MediaMetadata + } else { + r.Logger.Info("Responding with thumbnail") + responseFile = thumbFile + responseMetadata = thumbMetadata.MediaMetadata + } + } else { + r.Logger.WithFields(log.Fields{ + "UploadName": r.MediaMetadata.UploadName, + "Base64Hash": r.MediaMetadata.Base64Hash, + "FileSizeBytes": r.MediaMetadata.FileSizeBytes, + "ContentType": r.MediaMetadata.ContentType, + }).Info("Responding with file") + responseFile = file + responseMetadata = r.MediaMetadata + } - w.Header().Set("Content-Type", string(r.MediaMetadata.ContentType)) - w.Header().Set("Content-Length", strconv.FormatInt(int64(r.MediaMetadata.FileSizeBytes), 10)) + w.Header().Set("Content-Type", string(responseMetadata.ContentType)) + w.Header().Set("Content-Length", strconv.FormatInt(int64(responseMetadata.FileSizeBytes), 10)) contentSecurityPolicy := "default-src 'none';" + " script-src 'none';" + " plugin-types application/pdf;" + @@ -197,7 +271,7 @@ func (r *downloadRequest) respondFromLocalFile(w http.ResponseWriter, absBasePat " object-src 'self';" w.Header().Set("Content-Security-Policy", contentSecurityPolicy) - if bytesResponded, err := io.Copy(w, file); err != nil { + if bytesResponded, err := io.Copy(w, responseFile); err != nil { r.Logger.WithError(err).Warn("Failed to copy from cache") if bytesResponded == 0 { resErr := jsonerror.InternalServerError() @@ -209,12 +283,107 @@ func (r *downloadRequest) respondFromLocalFile(w http.ResponseWriter, absBasePat return nil } +// Note: Thumbnail generation may be ongoing asynchronously. +func (r *downloadRequest) getThumbnailFile(filePath types.Path, activeThumbnailGeneration *types.ActiveThumbnailGeneration, maxThumbnailGenerators int, db *storage.Database, dynamicThumbnails bool, thumbnailSizes []types.ThumbnailSize) (*os.File, *types.ThumbnailMetadata, *util.JSONResponse) { + var thumbnail *types.ThumbnailMetadata + var resErr *util.JSONResponse + + if dynamicThumbnails { + thumbnail, resErr = r.generateThumbnail(filePath, r.ThumbnailSize, activeThumbnailGeneration, maxThumbnailGenerators, db) + if resErr != nil { + return nil, nil, resErr + } + } + // If dynamicThumbnails is true but there are too many thumbnails being actively generated, we can fall back + // to trying to use a pre-generated thumbnail + if thumbnail == nil { + thumbnails, err := db.GetThumbnails(r.MediaMetadata.MediaID, r.MediaMetadata.Origin) + if err != nil { + r.Logger.WithError(err).Error("Error looking up thumbnails") + resErr := jsonerror.InternalServerError() + return nil, nil, &resErr + } + + // If we get a thumbnailSize, a pre-generated thumbnail would be best but it is not yet generated. + // If we get a thumbnail, we're done. + var thumbnailSize *types.ThumbnailSize + thumbnail, thumbnailSize = thumbnailer.SelectThumbnail(r.ThumbnailSize, thumbnails, thumbnailSizes) + // If dynamicThumbnails is true and we are not over-loaded then we would have generated what was requested above. + // So we don't try to generate a pre-generated thumbnail here. + if thumbnailSize != nil && dynamicThumbnails == false { + r.Logger.WithFields(log.Fields{ + "Width": thumbnailSize.Width, + "Height": thumbnailSize.Height, + "ResizeMethod": thumbnailSize.ResizeMethod, + }).Info("Pre-generating thumbnail for immediate response.") + thumbnail, resErr = r.generateThumbnail(filePath, *thumbnailSize, activeThumbnailGeneration, maxThumbnailGenerators, db) + if resErr != nil { + return nil, nil, resErr + } + } + } + if thumbnail == nil { + return nil, nil, nil + } + r.Logger = r.Logger.WithFields(log.Fields{ + "Width": thumbnail.ThumbnailSize.Width, + "Height": thumbnail.ThumbnailSize.Height, + "ResizeMethod": thumbnail.ThumbnailSize.ResizeMethod, + "FileSizeBytes": thumbnail.MediaMetadata.FileSizeBytes, + "ContentType": thumbnail.MediaMetadata.ContentType, + }) + thumbPath := string(thumbnailer.GetThumbnailPath(types.Path(filePath), thumbnail.ThumbnailSize)) + thumbFile, err := os.Open(string(thumbPath)) + if err != nil { + r.Logger.WithError(err).Warn("Failed to open file") + resErr := jsonerror.InternalServerError() + return thumbFile, nil, &resErr + } + thumbStat, err := thumbFile.Stat() + if err != nil { + r.Logger.WithError(err).Warn("Failed to stat file") + resErr := jsonerror.InternalServerError() + return thumbFile, nil, &resErr + } + if types.FileSizeBytes(thumbStat.Size()) != thumbnail.MediaMetadata.FileSizeBytes { + r.Logger.WithError(err).Warn("Thumbnail file sizes on disk and in database differ") + resErr := jsonerror.InternalServerError() + return thumbFile, nil, &resErr + } + return thumbFile, thumbnail, nil +} + +func (r *downloadRequest) generateThumbnail(filePath types.Path, thumbnailSize types.ThumbnailSize, activeThumbnailGeneration *types.ActiveThumbnailGeneration, maxThumbnailGenerators int, db *storage.Database) (*types.ThumbnailMetadata, *util.JSONResponse) { + logger := r.Logger.WithFields(log.Fields{ + "Width": thumbnailSize.Width, + "Height": thumbnailSize.Height, + "ResizeMethod": thumbnailSize.ResizeMethod, + }) + busy, err := thumbnailer.GenerateThumbnail(filePath, thumbnailSize, r.MediaMetadata, activeThumbnailGeneration, maxThumbnailGenerators, db, r.Logger) + if err != nil { + logger.WithError(err).Error("Error creating thumbnail") + resErr := jsonerror.InternalServerError() + return nil, &resErr + } + if busy { + return nil, nil + } + var thumbnail *types.ThumbnailMetadata + thumbnail, err = db.GetThumbnail(r.MediaMetadata.MediaID, r.MediaMetadata.Origin, thumbnailSize.Width, thumbnailSize.Height, thumbnailSize.ResizeMethod) + if err != nil { + logger.WithError(err).Error("Error looking up thumbnails") + resErr := jsonerror.InternalServerError() + return nil, &resErr + } + return thumbnail, nil +} + // getRemoteFile fetches the remote file and caches it locally // A hash map of active remote requests to a struct containing a sync.Cond is used to only download remote files once, // regardless of how many download requests are received. // Note: The named errorResponse return variable is used in a deferred broadcast of the metadata and error response to waiting goroutines. // Returns a util.JSONResponse error in case of error -func (r *downloadRequest) getRemoteFile(cfg *config.MediaAPI, db *storage.Database, activeRemoteRequests *types.ActiveRemoteRequests) (errorResponse *util.JSONResponse) { +func (r *downloadRequest) getRemoteFile(cfg *config.MediaAPI, db *storage.Database, activeRemoteRequests *types.ActiveRemoteRequests, activeThumbnailGeneration *types.ActiveThumbnailGeneration) (errorResponse *util.JSONResponse) { // Note: getMediaMetadataFromActiveRequest uses mutexes and conditions from activeRemoteRequests mediaMetadata, resErr := r.getMediaMetadataFromActiveRequest(activeRemoteRequests) if resErr != nil { @@ -245,7 +414,7 @@ func (r *downloadRequest) getRemoteFile(cfg *config.MediaAPI, db *storage.Databa if mediaMetadata == nil { // If we do not have a record, we need to fetch the remote file first and then respond from the local file - resErr := r.fetchRemoteFileAndStoreMetadata(cfg.AbsBasePath, cfg.MaxFileSizeBytes, db) + resErr := r.fetchRemoteFileAndStoreMetadata(cfg.AbsBasePath, *cfg.MaxFileSizeBytes, db, cfg.ThumbnailSizes, activeThumbnailGeneration, cfg.MaxThumbnailGenerators) if resErr != nil { return resErr } @@ -307,7 +476,7 @@ func (r *downloadRequest) broadcastMediaMetadata(activeRemoteRequests *types.Act } // fetchRemoteFileAndStoreMetadata fetches the file from the remote server and stores its metadata in the database -func (r *downloadRequest) fetchRemoteFileAndStoreMetadata(absBasePath types.Path, maxFileSizeBytes types.FileSizeBytes, db *storage.Database) *util.JSONResponse { +func (r *downloadRequest) fetchRemoteFileAndStoreMetadata(absBasePath types.Path, maxFileSizeBytes types.FileSizeBytes, db *storage.Database, thumbnailSizes []types.ThumbnailSize, activeThumbnailGeneration *types.ActiveThumbnailGeneration, maxThumbnailGenerators int) *util.JSONResponse { finalPath, duplicate, resErr := r.fetchRemoteFile(absBasePath, maxFileSizeBytes) if resErr != nil { return resErr @@ -317,7 +486,7 @@ func (r *downloadRequest) fetchRemoteFileAndStoreMetadata(absBasePath types.Path "Base64Hash": r.MediaMetadata.Base64Hash, "UploadName": r.MediaMetadata.UploadName, "FileSizeBytes": r.MediaMetadata.FileSizeBytes, - "Content-Type": r.MediaMetadata.ContentType, + "ContentType": r.MediaMetadata.ContentType, }).Info("Storing file metadata to media repository database") // FIXME: timeout db request @@ -335,13 +504,21 @@ func (r *downloadRequest) fetchRemoteFileAndStoreMetadata(absBasePath types.Path return &resErr } - // TODO: generate thumbnails + go func() { + busy, err := thumbnailer.GenerateThumbnails(finalPath, thumbnailSizes, r.MediaMetadata, activeThumbnailGeneration, maxThumbnailGenerators, db, r.Logger) + if err != nil { + r.Logger.WithError(err).Warn("Error generating thumbnails") + } + if busy { + r.Logger.Warn("Maximum number of active thumbnail generators reached. Skipping pre-generation.") + } + }() r.Logger.WithFields(log.Fields{ "UploadName": r.MediaMetadata.UploadName, "Base64Hash": r.MediaMetadata.Base64Hash, "FileSizeBytes": r.MediaMetadata.FileSizeBytes, - "Content-Type": r.MediaMetadata.ContentType, + "ContentType": r.MediaMetadata.ContentType, }).Infof("Remote file cached") return nil diff --git a/src/github.com/matrix-org/dendrite/mediaapi/writers/upload.go b/src/github.com/matrix-org/dendrite/mediaapi/writers/upload.go index f1838a55..7071b417 100644 --- a/src/github.com/matrix-org/dendrite/mediaapi/writers/upload.go +++ b/src/github.com/matrix-org/dendrite/mediaapi/writers/upload.go @@ -27,6 +27,7 @@ import ( "github.com/matrix-org/dendrite/mediaapi/config" "github.com/matrix-org/dendrite/mediaapi/fileutils" "github.com/matrix-org/dendrite/mediaapi/storage" + "github.com/matrix-org/dendrite/mediaapi/thumbnailer" "github.com/matrix-org/dendrite/mediaapi/types" "github.com/matrix-org/util" ) @@ -50,13 +51,13 @@ type uploadResponse struct { // This implementation supports a configurable maximum file size limit in bytes. If a user tries to upload more than this, they will receive an error that their upload is too large. // Uploaded files are processed piece-wise to avoid DoS attacks which would starve the server of memory. // TODO: We should time out requests if they have not received any data within a configured timeout period. -func Upload(req *http.Request, cfg *config.MediaAPI, db *storage.Database) util.JSONResponse { +func Upload(req *http.Request, cfg *config.MediaAPI, db *storage.Database, activeThumbnailGeneration *types.ActiveThumbnailGeneration) util.JSONResponse { r, resErr := parseAndValidateRequest(req, cfg) if resErr != nil { return *resErr } - if resErr = r.doUpload(req.Body, cfg, db); resErr != nil { + if resErr = r.doUpload(req.Body, cfg, db, activeThumbnailGeneration); resErr != nil { return *resErr } @@ -89,14 +90,14 @@ func parseAndValidateRequest(req *http.Request, cfg *config.MediaAPI) (*uploadRe Logger: util.GetLogger(req.Context()).WithField("Origin", cfg.ServerName), } - if resErr := r.Validate(cfg.MaxFileSizeBytes); resErr != nil { + if resErr := r.Validate(*cfg.MaxFileSizeBytes); resErr != nil { return nil, resErr } return r, nil } -func (r *uploadRequest) doUpload(reqReader io.Reader, cfg *config.MediaAPI, db *storage.Database) *util.JSONResponse { +func (r *uploadRequest) doUpload(reqReader io.Reader, cfg *config.MediaAPI, db *storage.Database, activeThumbnailGeneration *types.ActiveThumbnailGeneration) *util.JSONResponse { r.Logger.WithFields(log.Fields{ "UploadName": r.MediaMetadata.UploadName, "FileSizeBytes": r.MediaMetadata.FileSizeBytes, @@ -107,10 +108,10 @@ func (r *uploadRequest) doUpload(reqReader io.Reader, cfg *config.MediaAPI, db * // method of deduplicating files to save storage, as well as a way to conduct // integrity checks on the file data in the repository. // Data is truncated to maxFileSizeBytes. Content-Length was reported as 0 < Content-Length <= maxFileSizeBytes so this is OK. - hash, bytesWritten, tmpDir, err := fileutils.WriteTempFile(reqReader, cfg.MaxFileSizeBytes, cfg.AbsBasePath) + hash, bytesWritten, tmpDir, err := fileutils.WriteTempFile(reqReader, *cfg.MaxFileSizeBytes, cfg.AbsBasePath) if err != nil { r.Logger.WithError(err).WithFields(log.Fields{ - "MaxFileSizeBytes": cfg.MaxFileSizeBytes, + "MaxFileSizeBytes": *cfg.MaxFileSizeBytes, }).Warn("Error while transferring file") fileutils.RemoveDir(tmpDir, r.Logger) return &util.JSONResponse{ @@ -151,9 +152,7 @@ func (r *uploadRequest) doUpload(reqReader io.Reader, cfg *config.MediaAPI, db * } } - // TODO: generate thumbnails - - if resErr := r.storeFileAndMetadata(tmpDir, cfg.AbsBasePath, db); resErr != nil { + if resErr := r.storeFileAndMetadata(tmpDir, cfg.AbsBasePath, db, cfg.ThumbnailSizes, activeThumbnailGeneration, cfg.MaxThumbnailGenerators); resErr != nil { return resErr } @@ -216,7 +215,7 @@ func (r *uploadRequest) Validate(maxFileSizeBytes types.FileSizeBytes) *util.JSO // The order of operations is important as it avoids metadata entering the database before the file // is ready, and if we fail to move the file, it never gets added to the database. // Returns a util.JSONResponse error and cleans up directories in case of error. -func (r *uploadRequest) storeFileAndMetadata(tmpDir types.Path, absBasePath types.Path, db *storage.Database) *util.JSONResponse { +func (r *uploadRequest) storeFileAndMetadata(tmpDir types.Path, absBasePath types.Path, db *storage.Database, thumbnailSizes []types.ThumbnailSize, activeThumbnailGeneration *types.ActiveThumbnailGeneration, maxThumbnailGenerators int) *util.JSONResponse { finalPath, duplicate, err := fileutils.MoveFileWithHashCheck(tmpDir, r.MediaMetadata, absBasePath, r.Logger) if err != nil { r.Logger.WithError(err).Error("Failed to move file.") @@ -243,5 +242,15 @@ func (r *uploadRequest) storeFileAndMetadata(tmpDir types.Path, absBasePath type } } + go func() { + busy, err := thumbnailer.GenerateThumbnails(finalPath, thumbnailSizes, r.MediaMetadata, activeThumbnailGeneration, maxThumbnailGenerators, db, r.Logger) + if err != nil { + r.Logger.WithError(err).Warn("Error generating thumbnails") + } + if busy { + r.Logger.Warn("Maximum number of active thumbnail generators reached. Skipping pre-generation.") + } + }() + return nil } diff --git a/vendor/manifest b/vendor/manifest index b5725ca3..edd1c12b 100644 --- a/vendor/manifest +++ b/vendor/manifest @@ -114,6 +114,12 @@ "branch": "master", "path": "/pbutil" }, + { + "importpath": "github.com/nfnt/resize", + "repository": "https://github.com/nfnt/resize", + "revision": "891127d8d1b52734debe1b3c3d7e747502b6c366", + "branch": "master" + }, { "importpath": "github.com/pierrec/lz4", "repository": "https://github.com/pierrec/lz4", @@ -179,6 +185,12 @@ "revision": "61e43dc76f7ee59a82bdf3d71033dc12bea4c77d", "branch": "master" }, + { + "importpath": "github.com/tj/go-debug", + "repository": "https://github.com/tj/go-debug", + "revision": "ff4a55a20a86994118644bbddc6a216da193cc13", + "branch": "master" + }, { "importpath": "golang.org/x/crypto/bcrypt", "repository": "https://go.googlesource.com/crypto", @@ -225,6 +237,12 @@ "revision": "bfee1239d796830ca346767650cce5ba90d58c57", "branch": "master" }, + { + "importpath": "gopkg.in/h2non/bimg.v1", + "repository": "https://gopkg.in/h2non/bimg.v1", + "revision": "45f8993550e71ee7b8001d40c681c6c9fa822357", + "branch": "master" + }, { "importpath": "gopkg.in/yaml.v2", "repository": "https://gopkg.in/yaml.v2", diff --git a/vendor/src/github.com/nfnt/resize/LICENSE b/vendor/src/github.com/nfnt/resize/LICENSE new file mode 100644 index 00000000..7836cad5 --- /dev/null +++ b/vendor/src/github.com/nfnt/resize/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2012, Jan Schlicht + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. diff --git a/vendor/src/github.com/nfnt/resize/README.md b/vendor/src/github.com/nfnt/resize/README.md new file mode 100644 index 00000000..2aefa75c --- /dev/null +++ b/vendor/src/github.com/nfnt/resize/README.md @@ -0,0 +1,149 @@ +Resize +====== + +Image resizing for the [Go programming language](http://golang.org) with common interpolation methods. + +[![Build Status](https://travis-ci.org/nfnt/resize.svg)](https://travis-ci.org/nfnt/resize) + +Installation +------------ + +```bash +$ go get github.com/nfnt/resize +``` + +It's that easy! + +Usage +----- + +This package needs at least Go 1.1. Import package with + +```go +import "github.com/nfnt/resize" +``` + +The resize package provides 2 functions: + +* `resize.Resize` creates a scaled image with new dimensions (`width`, `height`) using the interpolation function `interp`. + If either `width` or `height` is set to 0, it will be set to an aspect ratio preserving value. +* `resize.Thumbnail` downscales an image preserving its aspect ratio to the maximum dimensions (`maxWidth`, `maxHeight`). + It will return the original image if original sizes are smaller than the provided dimensions. + +```go +resize.Resize(width, height uint, img image.Image, interp resize.InterpolationFunction) image.Image +resize.Thumbnail(maxWidth, maxHeight uint, img image.Image, interp resize.InterpolationFunction) image.Image +``` + +The provided interpolation functions are (from fast to slow execution time) + +- `NearestNeighbor`: [Nearest-neighbor interpolation](http://en.wikipedia.org/wiki/Nearest-neighbor_interpolation) +- `Bilinear`: [Bilinear interpolation](http://en.wikipedia.org/wiki/Bilinear_interpolation) +- `Bicubic`: [Bicubic interpolation](http://en.wikipedia.org/wiki/Bicubic_interpolation) +- `MitchellNetravali`: [Mitchell-Netravali interpolation](http://dl.acm.org/citation.cfm?id=378514) +- `Lanczos2`: [Lanczos resampling](http://en.wikipedia.org/wiki/Lanczos_resampling) with a=2 +- `Lanczos3`: [Lanczos resampling](http://en.wikipedia.org/wiki/Lanczos_resampling) with a=3 + +Which of these methods gives the best results depends on your use case. + +Sample usage: + +```go +package main + +import ( + "github.com/nfnt/resize" + "image/jpeg" + "log" + "os" +) + +func main() { + // open "test.jpg" + file, err := os.Open("test.jpg") + if err != nil { + log.Fatal(err) + } + + // decode jpeg into image.Image + img, err := jpeg.Decode(file) + if err != nil { + log.Fatal(err) + } + file.Close() + + // resize to width 1000 using Lanczos resampling + // and preserve aspect ratio + m := resize.Resize(1000, 0, img, resize.Lanczos3) + + out, err := os.Create("test_resized.jpg") + if err != nil { + log.Fatal(err) + } + defer out.Close() + + // write new image to file + jpeg.Encode(out, m, nil) +} +``` + +Caveats +------- + +* Optimized access routines are used for `image.RGBA`, `image.NRGBA`, `image.RGBA64`, `image.NRGBA64`, `image.YCbCr`, `image.Gray`, and `image.Gray16` types. All other image types are accessed in a generic way that will result in slow processing speed. +* JPEG images are stored in `image.YCbCr`. This image format stores data in a way that will decrease processing speed. A resize may be up to 2 times slower than with `image.RGBA`. + + +Downsizing Samples +------- + +Downsizing is not as simple as it might look like. Images have to be filtered before they are scaled down, otherwise aliasing might occur. +Filtering is highly subjective: Applying too much will blur the whole image, too little will make aliasing become apparent. +Resize tries to provide sane defaults that should suffice in most cases. + +### Artificial sample + +Original image +![Rings](http://nfnt.github.com/img/rings_lg_orig.png) + + + + + + + + + + + + + + +

Nearest-Neighbor

Bilinear

Bicubic

Mitchell-Netravali

Lanczos2

Lanczos3
+ +### Real-Life sample + +Original image +![Original](http://nfnt.github.com/img/IMG_3694_720.jpg) + + + + + + + + + + + + + + +

Nearest-Neighbor

Bilinear

Bicubic

Mitchell-Netravali

Lanczos2

Lanczos3
+ + +License +------- + +Copyright (c) 2012 Jan Schlicht +Resize is released under a MIT style license. diff --git a/vendor/src/github.com/nfnt/resize/converter.go b/vendor/src/github.com/nfnt/resize/converter.go new file mode 100644 index 00000000..f9c520d0 --- /dev/null +++ b/vendor/src/github.com/nfnt/resize/converter.go @@ -0,0 +1,438 @@ +/* +Copyright (c) 2012, Jan Schlicht + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. +*/ + +package resize + +import "image" + +// Keep value in [0,255] range. +func clampUint8(in int32) uint8 { + // casting a negative int to an uint will result in an overflown + // large uint. this behavior will be exploited here and in other functions + // to achieve a higher performance. + if uint32(in) < 256 { + return uint8(in) + } + if in > 255 { + return 255 + } + return 0 +} + +// Keep value in [0,65535] range. +func clampUint16(in int64) uint16 { + if uint64(in) < 65536 { + return uint16(in) + } + if in > 65535 { + return 65535 + } + return 0 +} + +func resizeGeneric(in image.Image, out *image.RGBA64, scale float64, coeffs []int32, offset []int, filterLength int) { + newBounds := out.Bounds() + maxX := in.Bounds().Dx() - 1 + + for x := newBounds.Min.X; x < newBounds.Max.X; x++ { + for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ { + var rgba [4]int64 + var sum int64 + start := offset[y] + ci := y * filterLength + for i := 0; i < filterLength; i++ { + coeff := coeffs[ci+i] + if coeff != 0 { + xi := start + i + switch { + case xi < 0: + xi = 0 + case xi >= maxX: + xi = maxX + } + + r, g, b, a := in.At(xi+in.Bounds().Min.X, x+in.Bounds().Min.Y).RGBA() + + rgba[0] += int64(coeff) * int64(r) + rgba[1] += int64(coeff) * int64(g) + rgba[2] += int64(coeff) * int64(b) + rgba[3] += int64(coeff) * int64(a) + sum += int64(coeff) + } + } + + offset := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*8 + + value := clampUint16(rgba[0] / sum) + out.Pix[offset+0] = uint8(value >> 8) + out.Pix[offset+1] = uint8(value) + value = clampUint16(rgba[1] / sum) + out.Pix[offset+2] = uint8(value >> 8) + out.Pix[offset+3] = uint8(value) + value = clampUint16(rgba[2] / sum) + out.Pix[offset+4] = uint8(value >> 8) + out.Pix[offset+5] = uint8(value) + value = clampUint16(rgba[3] / sum) + out.Pix[offset+6] = uint8(value >> 8) + out.Pix[offset+7] = uint8(value) + } + } +} + +func resizeRGBA(in *image.RGBA, out *image.RGBA, scale float64, coeffs []int16, offset []int, filterLength int) { + newBounds := out.Bounds() + maxX := in.Bounds().Dx() - 1 + + for x := newBounds.Min.X; x < newBounds.Max.X; x++ { + row := in.Pix[x*in.Stride:] + for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ { + var rgba [4]int32 + var sum int32 + start := offset[y] + ci := y * filterLength + for i := 0; i < filterLength; i++ { + coeff := coeffs[ci+i] + if coeff != 0 { + xi := start + i + switch { + case uint(xi) < uint(maxX): + xi *= 4 + case xi >= maxX: + xi = 4 * maxX + default: + xi = 0 + } + + rgba[0] += int32(coeff) * int32(row[xi+0]) + rgba[1] += int32(coeff) * int32(row[xi+1]) + rgba[2] += int32(coeff) * int32(row[xi+2]) + rgba[3] += int32(coeff) * int32(row[xi+3]) + sum += int32(coeff) + } + } + + xo := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*4 + + out.Pix[xo+0] = clampUint8(rgba[0] / sum) + out.Pix[xo+1] = clampUint8(rgba[1] / sum) + out.Pix[xo+2] = clampUint8(rgba[2] / sum) + out.Pix[xo+3] = clampUint8(rgba[3] / sum) + } + } +} + +func resizeNRGBA(in *image.NRGBA, out *image.RGBA, scale float64, coeffs []int16, offset []int, filterLength int) { + newBounds := out.Bounds() + maxX := in.Bounds().Dx() - 1 + + for x := newBounds.Min.X; x < newBounds.Max.X; x++ { + row := in.Pix[x*in.Stride:] + for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ { + var rgba [4]int32 + var sum int32 + start := offset[y] + ci := y * filterLength + for i := 0; i < filterLength; i++ { + coeff := coeffs[ci+i] + if coeff != 0 { + xi := start + i + switch { + case uint(xi) < uint(maxX): + xi *= 4 + case xi >= maxX: + xi = 4 * maxX + default: + xi = 0 + } + + // Forward alpha-premultiplication + a := int32(row[xi+3]) + r := int32(row[xi+0]) * a + r /= 0xff + g := int32(row[xi+1]) * a + g /= 0xff + b := int32(row[xi+2]) * a + b /= 0xff + + rgba[0] += int32(coeff) * r + rgba[1] += int32(coeff) * g + rgba[2] += int32(coeff) * b + rgba[3] += int32(coeff) * a + sum += int32(coeff) + } + } + + xo := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*4 + + out.Pix[xo+0] = clampUint8(rgba[0] / sum) + out.Pix[xo+1] = clampUint8(rgba[1] / sum) + out.Pix[xo+2] = clampUint8(rgba[2] / sum) + out.Pix[xo+3] = clampUint8(rgba[3] / sum) + } + } +} + +func resizeRGBA64(in *image.RGBA64, out *image.RGBA64, scale float64, coeffs []int32, offset []int, filterLength int) { + newBounds := out.Bounds() + maxX := in.Bounds().Dx() - 1 + + for x := newBounds.Min.X; x < newBounds.Max.X; x++ { + row := in.Pix[x*in.Stride:] + for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ { + var rgba [4]int64 + var sum int64 + start := offset[y] + ci := y * filterLength + for i := 0; i < filterLength; i++ { + coeff := coeffs[ci+i] + if coeff != 0 { + xi := start + i + switch { + case uint(xi) < uint(maxX): + xi *= 8 + case xi >= maxX: + xi = 8 * maxX + default: + xi = 0 + } + + rgba[0] += int64(coeff) * (int64(row[xi+0])<<8 | int64(row[xi+1])) + rgba[1] += int64(coeff) * (int64(row[xi+2])<<8 | int64(row[xi+3])) + rgba[2] += int64(coeff) * (int64(row[xi+4])<<8 | int64(row[xi+5])) + rgba[3] += int64(coeff) * (int64(row[xi+6])<<8 | int64(row[xi+7])) + sum += int64(coeff) + } + } + + xo := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*8 + + value := clampUint16(rgba[0] / sum) + out.Pix[xo+0] = uint8(value >> 8) + out.Pix[xo+1] = uint8(value) + value = clampUint16(rgba[1] / sum) + out.Pix[xo+2] = uint8(value >> 8) + out.Pix[xo+3] = uint8(value) + value = clampUint16(rgba[2] / sum) + out.Pix[xo+4] = uint8(value >> 8) + out.Pix[xo+5] = uint8(value) + value = clampUint16(rgba[3] / sum) + out.Pix[xo+6] = uint8(value >> 8) + out.Pix[xo+7] = uint8(value) + } + } +} + +func resizeNRGBA64(in *image.NRGBA64, out *image.RGBA64, scale float64, coeffs []int32, offset []int, filterLength int) { + newBounds := out.Bounds() + maxX := in.Bounds().Dx() - 1 + + for x := newBounds.Min.X; x < newBounds.Max.X; x++ { + row := in.Pix[x*in.Stride:] + for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ { + var rgba [4]int64 + var sum int64 + start := offset[y] + ci := y * filterLength + for i := 0; i < filterLength; i++ { + coeff := coeffs[ci+i] + if coeff != 0 { + xi := start + i + switch { + case uint(xi) < uint(maxX): + xi *= 8 + case xi >= maxX: + xi = 8 * maxX + default: + xi = 0 + } + + // Forward alpha-premultiplication + a := int64(uint16(row[xi+6])<<8 | uint16(row[xi+7])) + r := int64(uint16(row[xi+0])<<8|uint16(row[xi+1])) * a + r /= 0xffff + g := int64(uint16(row[xi+2])<<8|uint16(row[xi+3])) * a + g /= 0xffff + b := int64(uint16(row[xi+4])<<8|uint16(row[xi+5])) * a + b /= 0xffff + + rgba[0] += int64(coeff) * r + rgba[1] += int64(coeff) * g + rgba[2] += int64(coeff) * b + rgba[3] += int64(coeff) * a + sum += int64(coeff) + } + } + + xo := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*8 + + value := clampUint16(rgba[0] / sum) + out.Pix[xo+0] = uint8(value >> 8) + out.Pix[xo+1] = uint8(value) + value = clampUint16(rgba[1] / sum) + out.Pix[xo+2] = uint8(value >> 8) + out.Pix[xo+3] = uint8(value) + value = clampUint16(rgba[2] / sum) + out.Pix[xo+4] = uint8(value >> 8) + out.Pix[xo+5] = uint8(value) + value = clampUint16(rgba[3] / sum) + out.Pix[xo+6] = uint8(value >> 8) + out.Pix[xo+7] = uint8(value) + } + } +} + +func resizeGray(in *image.Gray, out *image.Gray, scale float64, coeffs []int16, offset []int, filterLength int) { + newBounds := out.Bounds() + maxX := in.Bounds().Dx() - 1 + + for x := newBounds.Min.X; x < newBounds.Max.X; x++ { + row := in.Pix[(x-newBounds.Min.X)*in.Stride:] + for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ { + var gray int32 + var sum int32 + start := offset[y] + ci := y * filterLength + for i := 0; i < filterLength; i++ { + coeff := coeffs[ci+i] + if coeff != 0 { + xi := start + i + switch { + case xi < 0: + xi = 0 + case xi >= maxX: + xi = maxX + } + gray += int32(coeff) * int32(row[xi]) + sum += int32(coeff) + } + } + + offset := (y-newBounds.Min.Y)*out.Stride + (x - newBounds.Min.X) + out.Pix[offset] = clampUint8(gray / sum) + } + } +} + +func resizeGray16(in *image.Gray16, out *image.Gray16, scale float64, coeffs []int32, offset []int, filterLength int) { + newBounds := out.Bounds() + maxX := in.Bounds().Dx() - 1 + + for x := newBounds.Min.X; x < newBounds.Max.X; x++ { + row := in.Pix[x*in.Stride:] + for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ { + var gray int64 + var sum int64 + start := offset[y] + ci := y * filterLength + for i := 0; i < filterLength; i++ { + coeff := coeffs[ci+i] + if coeff != 0 { + xi := start + i + switch { + case uint(xi) < uint(maxX): + xi *= 2 + case xi >= maxX: + xi = 2 * maxX + default: + xi = 0 + } + gray += int64(coeff) * int64(uint16(row[xi+0])<<8|uint16(row[xi+1])) + sum += int64(coeff) + } + } + + offset := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*2 + value := clampUint16(gray / sum) + out.Pix[offset+0] = uint8(value >> 8) + out.Pix[offset+1] = uint8(value) + } + } +} + +func resizeYCbCr(in *ycc, out *ycc, scale float64, coeffs []int16, offset []int, filterLength int) { + newBounds := out.Bounds() + maxX := in.Bounds().Dx() - 1 + + for x := newBounds.Min.X; x < newBounds.Max.X; x++ { + row := in.Pix[x*in.Stride:] + for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ { + var p [3]int32 + var sum int32 + start := offset[y] + ci := y * filterLength + for i := 0; i < filterLength; i++ { + coeff := coeffs[ci+i] + if coeff != 0 { + xi := start + i + switch { + case uint(xi) < uint(maxX): + xi *= 3 + case xi >= maxX: + xi = 3 * maxX + default: + xi = 0 + } + p[0] += int32(coeff) * int32(row[xi+0]) + p[1] += int32(coeff) * int32(row[xi+1]) + p[2] += int32(coeff) * int32(row[xi+2]) + sum += int32(coeff) + } + } + + xo := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*3 + out.Pix[xo+0] = clampUint8(p[0] / sum) + out.Pix[xo+1] = clampUint8(p[1] / sum) + out.Pix[xo+2] = clampUint8(p[2] / sum) + } + } +} + +func nearestYCbCr(in *ycc, out *ycc, scale float64, coeffs []bool, offset []int, filterLength int) { + newBounds := out.Bounds() + maxX := in.Bounds().Dx() - 1 + + for x := newBounds.Min.X; x < newBounds.Max.X; x++ { + row := in.Pix[x*in.Stride:] + for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ { + var p [3]float32 + var sum float32 + start := offset[y] + ci := y * filterLength + for i := 0; i < filterLength; i++ { + if coeffs[ci+i] { + xi := start + i + switch { + case uint(xi) < uint(maxX): + xi *= 3 + case xi >= maxX: + xi = 3 * maxX + default: + xi = 0 + } + p[0] += float32(row[xi+0]) + p[1] += float32(row[xi+1]) + p[2] += float32(row[xi+2]) + sum++ + } + } + + xo := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*3 + out.Pix[xo+0] = floatToUint8(p[0] / sum) + out.Pix[xo+1] = floatToUint8(p[1] / sum) + out.Pix[xo+2] = floatToUint8(p[2] / sum) + } + } +} diff --git a/vendor/src/github.com/nfnt/resize/converter_test.go b/vendor/src/github.com/nfnt/resize/converter_test.go new file mode 100644 index 00000000..85639efc --- /dev/null +++ b/vendor/src/github.com/nfnt/resize/converter_test.go @@ -0,0 +1,43 @@ +package resize + +import ( + "testing" +) + +func Test_ClampUint8(t *testing.T) { + var testData = []struct { + in int32 + expected uint8 + }{ + {0, 0}, + {255, 255}, + {128, 128}, + {-2, 0}, + {256, 255}, + } + for _, test := range testData { + actual := clampUint8(test.in) + if actual != test.expected { + t.Fail() + } + } +} + +func Test_ClampUint16(t *testing.T) { + var testData = []struct { + in int64 + expected uint16 + }{ + {0, 0}, + {65535, 65535}, + {128, 128}, + {-2, 0}, + {65536, 65535}, + } + for _, test := range testData { + actual := clampUint16(test.in) + if actual != test.expected { + t.Fail() + } + } +} diff --git a/vendor/src/github.com/nfnt/resize/filters.go b/vendor/src/github.com/nfnt/resize/filters.go new file mode 100644 index 00000000..4ce04e38 --- /dev/null +++ b/vendor/src/github.com/nfnt/resize/filters.go @@ -0,0 +1,143 @@ +/* +Copyright (c) 2012, Jan Schlicht + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. +*/ + +package resize + +import ( + "math" +) + +func nearest(in float64) float64 { + if in >= -0.5 && in < 0.5 { + return 1 + } + return 0 +} + +func linear(in float64) float64 { + in = math.Abs(in) + if in <= 1 { + return 1 - in + } + return 0 +} + +func cubic(in float64) float64 { + in = math.Abs(in) + if in <= 1 { + return in*in*(1.5*in-2.5) + 1.0 + } + if in <= 2 { + return in*(in*(2.5-0.5*in)-4.0) + 2.0 + } + return 0 +} + +func mitchellnetravali(in float64) float64 { + in = math.Abs(in) + if in <= 1 { + return (7.0*in*in*in - 12.0*in*in + 5.33333333333) * 0.16666666666 + } + if in <= 2 { + return (-2.33333333333*in*in*in + 12.0*in*in - 20.0*in + 10.6666666667) * 0.16666666666 + } + return 0 +} + +func sinc(x float64) float64 { + x = math.Abs(x) * math.Pi + if x >= 1.220703e-4 { + return math.Sin(x) / x + } + return 1 +} + +func lanczos2(in float64) float64 { + if in > -2 && in < 2 { + return sinc(in) * sinc(in*0.5) + } + return 0 +} + +func lanczos3(in float64) float64 { + if in > -3 && in < 3 { + return sinc(in) * sinc(in*0.3333333333333333) + } + return 0 +} + +// range [-256,256] +func createWeights8(dy, filterLength int, blur, scale float64, kernel func(float64) float64) ([]int16, []int, int) { + filterLength = filterLength * int(math.Max(math.Ceil(blur*scale), 1)) + filterFactor := math.Min(1./(blur*scale), 1) + + coeffs := make([]int16, dy*filterLength) + start := make([]int, dy) + for y := 0; y < dy; y++ { + interpX := scale*(float64(y)+0.5) - 0.5 + start[y] = int(interpX) - filterLength/2 + 1 + interpX -= float64(start[y]) + for i := 0; i < filterLength; i++ { + in := (interpX - float64(i)) * filterFactor + coeffs[y*filterLength+i] = int16(kernel(in) * 256) + } + } + + return coeffs, start, filterLength +} + +// range [-65536,65536] +func createWeights16(dy, filterLength int, blur, scale float64, kernel func(float64) float64) ([]int32, []int, int) { + filterLength = filterLength * int(math.Max(math.Ceil(blur*scale), 1)) + filterFactor := math.Min(1./(blur*scale), 1) + + coeffs := make([]int32, dy*filterLength) + start := make([]int, dy) + for y := 0; y < dy; y++ { + interpX := scale*(float64(y)+0.5) - 0.5 + start[y] = int(interpX) - filterLength/2 + 1 + interpX -= float64(start[y]) + for i := 0; i < filterLength; i++ { + in := (interpX - float64(i)) * filterFactor + coeffs[y*filterLength+i] = int32(kernel(in) * 65536) + } + } + + return coeffs, start, filterLength +} + +func createWeightsNearest(dy, filterLength int, blur, scale float64) ([]bool, []int, int) { + filterLength = filterLength * int(math.Max(math.Ceil(blur*scale), 1)) + filterFactor := math.Min(1./(blur*scale), 1) + + coeffs := make([]bool, dy*filterLength) + start := make([]int, dy) + for y := 0; y < dy; y++ { + interpX := scale*(float64(y)+0.5) - 0.5 + start[y] = int(interpX) - filterLength/2 + 1 + interpX -= float64(start[y]) + for i := 0; i < filterLength; i++ { + in := (interpX - float64(i)) * filterFactor + if in >= -0.5 && in < 0.5 { + coeffs[y*filterLength+i] = true + } else { + coeffs[y*filterLength+i] = false + } + } + } + + return coeffs, start, filterLength +} diff --git a/vendor/src/github.com/nfnt/resize/nearest.go b/vendor/src/github.com/nfnt/resize/nearest.go new file mode 100644 index 00000000..888039d8 --- /dev/null +++ b/vendor/src/github.com/nfnt/resize/nearest.go @@ -0,0 +1,318 @@ +/* +Copyright (c) 2014, Charlie Vieth + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. +*/ + +package resize + +import "image" + +func floatToUint8(x float32) uint8 { + // Nearest-neighbor values are always + // positive no need to check lower-bound. + if x > 0xfe { + return 0xff + } + return uint8(x) +} + +func floatToUint16(x float32) uint16 { + if x > 0xfffe { + return 0xffff + } + return uint16(x) +} + +func nearestGeneric(in image.Image, out *image.RGBA64, scale float64, coeffs []bool, offset []int, filterLength int) { + newBounds := out.Bounds() + maxX := in.Bounds().Dx() - 1 + + for x := newBounds.Min.X; x < newBounds.Max.X; x++ { + for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ { + var rgba [4]float32 + var sum float32 + start := offset[y] + ci := y * filterLength + for i := 0; i < filterLength; i++ { + if coeffs[ci+i] { + xi := start + i + switch { + case xi < 0: + xi = 0 + case xi >= maxX: + xi = maxX + } + r, g, b, a := in.At(xi+in.Bounds().Min.X, x+in.Bounds().Min.Y).RGBA() + rgba[0] += float32(r) + rgba[1] += float32(g) + rgba[2] += float32(b) + rgba[3] += float32(a) + sum++ + } + } + + offset := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*8 + value := floatToUint16(rgba[0] / sum) + out.Pix[offset+0] = uint8(value >> 8) + out.Pix[offset+1] = uint8(value) + value = floatToUint16(rgba[1] / sum) + out.Pix[offset+2] = uint8(value >> 8) + out.Pix[offset+3] = uint8(value) + value = floatToUint16(rgba[2] / sum) + out.Pix[offset+4] = uint8(value >> 8) + out.Pix[offset+5] = uint8(value) + value = floatToUint16(rgba[3] / sum) + out.Pix[offset+6] = uint8(value >> 8) + out.Pix[offset+7] = uint8(value) + } + } +} + +func nearestRGBA(in *image.RGBA, out *image.RGBA, scale float64, coeffs []bool, offset []int, filterLength int) { + newBounds := out.Bounds() + maxX := in.Bounds().Dx() - 1 + + for x := newBounds.Min.X; x < newBounds.Max.X; x++ { + row := in.Pix[x*in.Stride:] + for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ { + var rgba [4]float32 + var sum float32 + start := offset[y] + ci := y * filterLength + for i := 0; i < filterLength; i++ { + if coeffs[ci+i] { + xi := start + i + switch { + case uint(xi) < uint(maxX): + xi *= 4 + case xi >= maxX: + xi = 4 * maxX + default: + xi = 0 + } + rgba[0] += float32(row[xi+0]) + rgba[1] += float32(row[xi+1]) + rgba[2] += float32(row[xi+2]) + rgba[3] += float32(row[xi+3]) + sum++ + } + } + + xo := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*4 + out.Pix[xo+0] = floatToUint8(rgba[0] / sum) + out.Pix[xo+1] = floatToUint8(rgba[1] / sum) + out.Pix[xo+2] = floatToUint8(rgba[2] / sum) + out.Pix[xo+3] = floatToUint8(rgba[3] / sum) + } + } +} + +func nearestNRGBA(in *image.NRGBA, out *image.NRGBA, scale float64, coeffs []bool, offset []int, filterLength int) { + newBounds := out.Bounds() + maxX := in.Bounds().Dx() - 1 + + for x := newBounds.Min.X; x < newBounds.Max.X; x++ { + row := in.Pix[x*in.Stride:] + for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ { + var rgba [4]float32 + var sum float32 + start := offset[y] + ci := y * filterLength + for i := 0; i < filterLength; i++ { + if coeffs[ci+i] { + xi := start + i + switch { + case uint(xi) < uint(maxX): + xi *= 4 + case xi >= maxX: + xi = 4 * maxX + default: + xi = 0 + } + rgba[0] += float32(row[xi+0]) + rgba[1] += float32(row[xi+1]) + rgba[2] += float32(row[xi+2]) + rgba[3] += float32(row[xi+3]) + sum++ + } + } + + xo := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*4 + out.Pix[xo+0] = floatToUint8(rgba[0] / sum) + out.Pix[xo+1] = floatToUint8(rgba[1] / sum) + out.Pix[xo+2] = floatToUint8(rgba[2] / sum) + out.Pix[xo+3] = floatToUint8(rgba[3] / sum) + } + } +} + +func nearestRGBA64(in *image.RGBA64, out *image.RGBA64, scale float64, coeffs []bool, offset []int, filterLength int) { + newBounds := out.Bounds() + maxX := in.Bounds().Dx() - 1 + + for x := newBounds.Min.X; x < newBounds.Max.X; x++ { + row := in.Pix[x*in.Stride:] + for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ { + var rgba [4]float32 + var sum float32 + start := offset[y] + ci := y * filterLength + for i := 0; i < filterLength; i++ { + if coeffs[ci+i] { + xi := start + i + switch { + case uint(xi) < uint(maxX): + xi *= 8 + case xi >= maxX: + xi = 8 * maxX + default: + xi = 0 + } + rgba[0] += float32(uint16(row[xi+0])<<8 | uint16(row[xi+1])) + rgba[1] += float32(uint16(row[xi+2])<<8 | uint16(row[xi+3])) + rgba[2] += float32(uint16(row[xi+4])<<8 | uint16(row[xi+5])) + rgba[3] += float32(uint16(row[xi+6])<<8 | uint16(row[xi+7])) + sum++ + } + } + + xo := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*8 + value := floatToUint16(rgba[0] / sum) + out.Pix[xo+0] = uint8(value >> 8) + out.Pix[xo+1] = uint8(value) + value = floatToUint16(rgba[1] / sum) + out.Pix[xo+2] = uint8(value >> 8) + out.Pix[xo+3] = uint8(value) + value = floatToUint16(rgba[2] / sum) + out.Pix[xo+4] = uint8(value >> 8) + out.Pix[xo+5] = uint8(value) + value = floatToUint16(rgba[3] / sum) + out.Pix[xo+6] = uint8(value >> 8) + out.Pix[xo+7] = uint8(value) + } + } +} + +func nearestNRGBA64(in *image.NRGBA64, out *image.NRGBA64, scale float64, coeffs []bool, offset []int, filterLength int) { + newBounds := out.Bounds() + maxX := in.Bounds().Dx() - 1 + + for x := newBounds.Min.X; x < newBounds.Max.X; x++ { + row := in.Pix[x*in.Stride:] + for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ { + var rgba [4]float32 + var sum float32 + start := offset[y] + ci := y * filterLength + for i := 0; i < filterLength; i++ { + if coeffs[ci+i] { + xi := start + i + switch { + case uint(xi) < uint(maxX): + xi *= 8 + case xi >= maxX: + xi = 8 * maxX + default: + xi = 0 + } + rgba[0] += float32(uint16(row[xi+0])<<8 | uint16(row[xi+1])) + rgba[1] += float32(uint16(row[xi+2])<<8 | uint16(row[xi+3])) + rgba[2] += float32(uint16(row[xi+4])<<8 | uint16(row[xi+5])) + rgba[3] += float32(uint16(row[xi+6])<<8 | uint16(row[xi+7])) + sum++ + } + } + + xo := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*8 + value := floatToUint16(rgba[0] / sum) + out.Pix[xo+0] = uint8(value >> 8) + out.Pix[xo+1] = uint8(value) + value = floatToUint16(rgba[1] / sum) + out.Pix[xo+2] = uint8(value >> 8) + out.Pix[xo+3] = uint8(value) + value = floatToUint16(rgba[2] / sum) + out.Pix[xo+4] = uint8(value >> 8) + out.Pix[xo+5] = uint8(value) + value = floatToUint16(rgba[3] / sum) + out.Pix[xo+6] = uint8(value >> 8) + out.Pix[xo+7] = uint8(value) + } + } +} + +func nearestGray(in *image.Gray, out *image.Gray, scale float64, coeffs []bool, offset []int, filterLength int) { + newBounds := out.Bounds() + maxX := in.Bounds().Dx() - 1 + + for x := newBounds.Min.X; x < newBounds.Max.X; x++ { + row := in.Pix[x*in.Stride:] + for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ { + var gray float32 + var sum float32 + start := offset[y] + ci := y * filterLength + for i := 0; i < filterLength; i++ { + if coeffs[ci+i] { + xi := start + i + switch { + case xi < 0: + xi = 0 + case xi >= maxX: + xi = maxX + } + gray += float32(row[xi]) + sum++ + } + } + + offset := (y-newBounds.Min.Y)*out.Stride + (x - newBounds.Min.X) + out.Pix[offset] = floatToUint8(gray / sum) + } + } +} + +func nearestGray16(in *image.Gray16, out *image.Gray16, scale float64, coeffs []bool, offset []int, filterLength int) { + newBounds := out.Bounds() + maxX := in.Bounds().Dx() - 1 + + for x := newBounds.Min.X; x < newBounds.Max.X; x++ { + row := in.Pix[x*in.Stride:] + for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ { + var gray float32 + var sum float32 + start := offset[y] + ci := y * filterLength + for i := 0; i < filterLength; i++ { + if coeffs[ci+i] { + xi := start + i + switch { + case uint(xi) < uint(maxX): + xi *= 2 + case xi >= maxX: + xi = 2 * maxX + default: + xi = 0 + } + gray += float32(uint16(row[xi+0])<<8 | uint16(row[xi+1])) + sum++ + } + } + + offset := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*2 + value := floatToUint16(gray / sum) + out.Pix[offset+0] = uint8(value >> 8) + out.Pix[offset+1] = uint8(value) + } + } +} diff --git a/vendor/src/github.com/nfnt/resize/nearest_test.go b/vendor/src/github.com/nfnt/resize/nearest_test.go new file mode 100644 index 00000000..d4a76dda --- /dev/null +++ b/vendor/src/github.com/nfnt/resize/nearest_test.go @@ -0,0 +1,57 @@ +/* +Copyright (c) 2014, Charlie Vieth + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. +*/ + +package resize + +import "testing" + +func Test_FloatToUint8(t *testing.T) { + var testData = []struct { + in float32 + expected uint8 + }{ + {0, 0}, + {255, 255}, + {128, 128}, + {1, 1}, + {256, 255}, + } + for _, test := range testData { + actual := floatToUint8(test.in) + if actual != test.expected { + t.Fail() + } + } +} + +func Test_FloatToUint16(t *testing.T) { + var testData = []struct { + in float32 + expected uint16 + }{ + {0, 0}, + {65535, 65535}, + {128, 128}, + {1, 1}, + {65536, 65535}, + } + for _, test := range testData { + actual := floatToUint16(test.in) + if actual != test.expected { + t.Fail() + } + } +} diff --git a/vendor/src/github.com/nfnt/resize/resize.go b/vendor/src/github.com/nfnt/resize/resize.go new file mode 100644 index 00000000..57bd1fcd --- /dev/null +++ b/vendor/src/github.com/nfnt/resize/resize.go @@ -0,0 +1,614 @@ +/* +Copyright (c) 2012, Jan Schlicht + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. +*/ + +// Package resize implements various image resizing methods. +// +// The package works with the Image interface described in the image package. +// Various interpolation methods are provided and multiple processors may be +// utilized in the computations. +// +// Example: +// imgResized := resize.Resize(1000, 0, imgOld, resize.MitchellNetravali) +package resize + +import ( + "image" + "runtime" + "sync" +) + +// An InterpolationFunction provides the parameters that describe an +// interpolation kernel. It returns the number of samples to take +// and the kernel function to use for sampling. +type InterpolationFunction int + +// InterpolationFunction constants +const ( + // Nearest-neighbor interpolation + NearestNeighbor InterpolationFunction = iota + // Bilinear interpolation + Bilinear + // Bicubic interpolation (with cubic hermite spline) + Bicubic + // Mitchell-Netravali interpolation + MitchellNetravali + // Lanczos interpolation (a=2) + Lanczos2 + // Lanczos interpolation (a=3) + Lanczos3 +) + +// kernal, returns an InterpolationFunctions taps and kernel. +func (i InterpolationFunction) kernel() (int, func(float64) float64) { + switch i { + case Bilinear: + return 2, linear + case Bicubic: + return 4, cubic + case MitchellNetravali: + return 4, mitchellnetravali + case Lanczos2: + return 4, lanczos2 + case Lanczos3: + return 6, lanczos3 + default: + // Default to NearestNeighbor. + return 2, nearest + } +} + +// values <1 will sharpen the image +var blur = 1.0 + +// Resize scales an image to new width and height using the interpolation function interp. +// A new image with the given dimensions will be returned. +// If one of the parameters width or height is set to 0, its size will be calculated so that +// the aspect ratio is that of the originating image. +// The resizing algorithm uses channels for parallel computation. +func Resize(width, height uint, img image.Image, interp InterpolationFunction) image.Image { + scaleX, scaleY := calcFactors(width, height, float64(img.Bounds().Dx()), float64(img.Bounds().Dy())) + if width == 0 { + width = uint(0.7 + float64(img.Bounds().Dx())/scaleX) + } + if height == 0 { + height = uint(0.7 + float64(img.Bounds().Dy())/scaleY) + } + + // Trivial case: return input image + if int(width) == img.Bounds().Dx() && int(height) == img.Bounds().Dy() { + return img + } + + if interp == NearestNeighbor { + return resizeNearest(width, height, scaleX, scaleY, img, interp) + } + + taps, kernel := interp.kernel() + cpus := runtime.GOMAXPROCS(0) + wg := sync.WaitGroup{} + + // Generic access to image.Image is slow in tight loops. + // The optimal access has to be determined from the concrete image type. + switch input := img.(type) { + case *image.RGBA: + // 8-bit precision + temp := image.NewRGBA(image.Rect(0, 0, input.Bounds().Dy(), int(width))) + result := image.NewRGBA(image.Rect(0, 0, int(width), int(height))) + + // horizontal filter, results in transposed temporary image + coeffs, offset, filterLength := createWeights8(temp.Bounds().Dy(), taps, blur, scaleX, kernel) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(temp, i, cpus).(*image.RGBA) + go func() { + defer wg.Done() + resizeRGBA(input, slice, scaleX, coeffs, offset, filterLength) + }() + } + wg.Wait() + + // horizontal filter on transposed image, result is not transposed + coeffs, offset, filterLength = createWeights8(result.Bounds().Dy(), taps, blur, scaleY, kernel) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(result, i, cpus).(*image.RGBA) + go func() { + defer wg.Done() + resizeRGBA(temp, slice, scaleY, coeffs, offset, filterLength) + }() + } + wg.Wait() + return result + case *image.NRGBA: + // 8-bit precision + temp := image.NewRGBA(image.Rect(0, 0, input.Bounds().Dy(), int(width))) + result := image.NewRGBA(image.Rect(0, 0, int(width), int(height))) + + // horizontal filter, results in transposed temporary image + coeffs, offset, filterLength := createWeights8(temp.Bounds().Dy(), taps, blur, scaleX, kernel) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(temp, i, cpus).(*image.RGBA) + go func() { + defer wg.Done() + resizeNRGBA(input, slice, scaleX, coeffs, offset, filterLength) + }() + } + wg.Wait() + + // horizontal filter on transposed image, result is not transposed + coeffs, offset, filterLength = createWeights8(result.Bounds().Dy(), taps, blur, scaleY, kernel) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(result, i, cpus).(*image.RGBA) + go func() { + defer wg.Done() + resizeRGBA(temp, slice, scaleY, coeffs, offset, filterLength) + }() + } + wg.Wait() + return result + + case *image.YCbCr: + // 8-bit precision + // accessing the YCbCr arrays in a tight loop is slow. + // converting the image to ycc increases performance by 2x. + temp := newYCC(image.Rect(0, 0, input.Bounds().Dy(), int(width)), input.SubsampleRatio) + result := newYCC(image.Rect(0, 0, int(width), int(height)), image.YCbCrSubsampleRatio444) + + coeffs, offset, filterLength := createWeights8(temp.Bounds().Dy(), taps, blur, scaleX, kernel) + in := imageYCbCrToYCC(input) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(temp, i, cpus).(*ycc) + go func() { + defer wg.Done() + resizeYCbCr(in, slice, scaleX, coeffs, offset, filterLength) + }() + } + wg.Wait() + + coeffs, offset, filterLength = createWeights8(result.Bounds().Dy(), taps, blur, scaleY, kernel) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(result, i, cpus).(*ycc) + go func() { + defer wg.Done() + resizeYCbCr(temp, slice, scaleY, coeffs, offset, filterLength) + }() + } + wg.Wait() + return result.YCbCr() + case *image.RGBA64: + // 16-bit precision + temp := image.NewRGBA64(image.Rect(0, 0, input.Bounds().Dy(), int(width))) + result := image.NewRGBA64(image.Rect(0, 0, int(width), int(height))) + + // horizontal filter, results in transposed temporary image + coeffs, offset, filterLength := createWeights16(temp.Bounds().Dy(), taps, blur, scaleX, kernel) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(temp, i, cpus).(*image.RGBA64) + go func() { + defer wg.Done() + resizeRGBA64(input, slice, scaleX, coeffs, offset, filterLength) + }() + } + wg.Wait() + + // horizontal filter on transposed image, result is not transposed + coeffs, offset, filterLength = createWeights16(result.Bounds().Dy(), taps, blur, scaleY, kernel) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(result, i, cpus).(*image.RGBA64) + go func() { + defer wg.Done() + resizeRGBA64(temp, slice, scaleY, coeffs, offset, filterLength) + }() + } + wg.Wait() + return result + case *image.NRGBA64: + // 16-bit precision + temp := image.NewRGBA64(image.Rect(0, 0, input.Bounds().Dy(), int(width))) + result := image.NewRGBA64(image.Rect(0, 0, int(width), int(height))) + + // horizontal filter, results in transposed temporary image + coeffs, offset, filterLength := createWeights16(temp.Bounds().Dy(), taps, blur, scaleX, kernel) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(temp, i, cpus).(*image.RGBA64) + go func() { + defer wg.Done() + resizeNRGBA64(input, slice, scaleX, coeffs, offset, filterLength) + }() + } + wg.Wait() + + // horizontal filter on transposed image, result is not transposed + coeffs, offset, filterLength = createWeights16(result.Bounds().Dy(), taps, blur, scaleY, kernel) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(result, i, cpus).(*image.RGBA64) + go func() { + defer wg.Done() + resizeRGBA64(temp, slice, scaleY, coeffs, offset, filterLength) + }() + } + wg.Wait() + return result + case *image.Gray: + // 8-bit precision + temp := image.NewGray(image.Rect(0, 0, input.Bounds().Dy(), int(width))) + result := image.NewGray(image.Rect(0, 0, int(width), int(height))) + + // horizontal filter, results in transposed temporary image + coeffs, offset, filterLength := createWeights8(temp.Bounds().Dy(), taps, blur, scaleX, kernel) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(temp, i, cpus).(*image.Gray) + go func() { + defer wg.Done() + resizeGray(input, slice, scaleX, coeffs, offset, filterLength) + }() + } + wg.Wait() + + // horizontal filter on transposed image, result is not transposed + coeffs, offset, filterLength = createWeights8(result.Bounds().Dy(), taps, blur, scaleY, kernel) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(result, i, cpus).(*image.Gray) + go func() { + defer wg.Done() + resizeGray(temp, slice, scaleY, coeffs, offset, filterLength) + }() + } + wg.Wait() + return result + case *image.Gray16: + // 16-bit precision + temp := image.NewGray16(image.Rect(0, 0, input.Bounds().Dy(), int(width))) + result := image.NewGray16(image.Rect(0, 0, int(width), int(height))) + + // horizontal filter, results in transposed temporary image + coeffs, offset, filterLength := createWeights16(temp.Bounds().Dy(), taps, blur, scaleX, kernel) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(temp, i, cpus).(*image.Gray16) + go func() { + defer wg.Done() + resizeGray16(input, slice, scaleX, coeffs, offset, filterLength) + }() + } + wg.Wait() + + // horizontal filter on transposed image, result is not transposed + coeffs, offset, filterLength = createWeights16(result.Bounds().Dy(), taps, blur, scaleY, kernel) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(result, i, cpus).(*image.Gray16) + go func() { + defer wg.Done() + resizeGray16(temp, slice, scaleY, coeffs, offset, filterLength) + }() + } + wg.Wait() + return result + default: + // 16-bit precision + temp := image.NewRGBA64(image.Rect(0, 0, img.Bounds().Dy(), int(width))) + result := image.NewRGBA64(image.Rect(0, 0, int(width), int(height))) + + // horizontal filter, results in transposed temporary image + coeffs, offset, filterLength := createWeights16(temp.Bounds().Dy(), taps, blur, scaleX, kernel) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(temp, i, cpus).(*image.RGBA64) + go func() { + defer wg.Done() + resizeGeneric(img, slice, scaleX, coeffs, offset, filterLength) + }() + } + wg.Wait() + + // horizontal filter on transposed image, result is not transposed + coeffs, offset, filterLength = createWeights16(result.Bounds().Dy(), taps, blur, scaleY, kernel) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(result, i, cpus).(*image.RGBA64) + go func() { + defer wg.Done() + resizeRGBA64(temp, slice, scaleY, coeffs, offset, filterLength) + }() + } + wg.Wait() + return result + } +} + +func resizeNearest(width, height uint, scaleX, scaleY float64, img image.Image, interp InterpolationFunction) image.Image { + taps, _ := interp.kernel() + cpus := runtime.GOMAXPROCS(0) + wg := sync.WaitGroup{} + + switch input := img.(type) { + case *image.RGBA: + // 8-bit precision + temp := image.NewRGBA(image.Rect(0, 0, input.Bounds().Dy(), int(width))) + result := image.NewRGBA(image.Rect(0, 0, int(width), int(height))) + + // horizontal filter, results in transposed temporary image + coeffs, offset, filterLength := createWeightsNearest(temp.Bounds().Dy(), taps, blur, scaleX) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(temp, i, cpus).(*image.RGBA) + go func() { + defer wg.Done() + nearestRGBA(input, slice, scaleX, coeffs, offset, filterLength) + }() + } + wg.Wait() + + // horizontal filter on transposed image, result is not transposed + coeffs, offset, filterLength = createWeightsNearest(result.Bounds().Dy(), taps, blur, scaleY) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(result, i, cpus).(*image.RGBA) + go func() { + defer wg.Done() + nearestRGBA(temp, slice, scaleY, coeffs, offset, filterLength) + }() + } + wg.Wait() + return result + case *image.NRGBA: + // 8-bit precision + temp := image.NewNRGBA(image.Rect(0, 0, input.Bounds().Dy(), int(width))) + result := image.NewNRGBA(image.Rect(0, 0, int(width), int(height))) + + // horizontal filter, results in transposed temporary image + coeffs, offset, filterLength := createWeightsNearest(temp.Bounds().Dy(), taps, blur, scaleX) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(temp, i, cpus).(*image.NRGBA) + go func() { + defer wg.Done() + nearestNRGBA(input, slice, scaleX, coeffs, offset, filterLength) + }() + } + wg.Wait() + + // horizontal filter on transposed image, result is not transposed + coeffs, offset, filterLength = createWeightsNearest(result.Bounds().Dy(), taps, blur, scaleY) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(result, i, cpus).(*image.NRGBA) + go func() { + defer wg.Done() + nearestNRGBA(temp, slice, scaleY, coeffs, offset, filterLength) + }() + } + wg.Wait() + return result + case *image.YCbCr: + // 8-bit precision + // accessing the YCbCr arrays in a tight loop is slow. + // converting the image to ycc increases performance by 2x. + temp := newYCC(image.Rect(0, 0, input.Bounds().Dy(), int(width)), input.SubsampleRatio) + result := newYCC(image.Rect(0, 0, int(width), int(height)), image.YCbCrSubsampleRatio444) + + coeffs, offset, filterLength := createWeightsNearest(temp.Bounds().Dy(), taps, blur, scaleX) + in := imageYCbCrToYCC(input) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(temp, i, cpus).(*ycc) + go func() { + defer wg.Done() + nearestYCbCr(in, slice, scaleX, coeffs, offset, filterLength) + }() + } + wg.Wait() + + coeffs, offset, filterLength = createWeightsNearest(result.Bounds().Dy(), taps, blur, scaleY) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(result, i, cpus).(*ycc) + go func() { + defer wg.Done() + nearestYCbCr(temp, slice, scaleY, coeffs, offset, filterLength) + }() + } + wg.Wait() + return result.YCbCr() + case *image.RGBA64: + // 16-bit precision + temp := image.NewRGBA64(image.Rect(0, 0, input.Bounds().Dy(), int(width))) + result := image.NewRGBA64(image.Rect(0, 0, int(width), int(height))) + + // horizontal filter, results in transposed temporary image + coeffs, offset, filterLength := createWeightsNearest(temp.Bounds().Dy(), taps, blur, scaleX) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(temp, i, cpus).(*image.RGBA64) + go func() { + defer wg.Done() + nearestRGBA64(input, slice, scaleX, coeffs, offset, filterLength) + }() + } + wg.Wait() + + // horizontal filter on transposed image, result is not transposed + coeffs, offset, filterLength = createWeightsNearest(result.Bounds().Dy(), taps, blur, scaleY) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(result, i, cpus).(*image.RGBA64) + go func() { + defer wg.Done() + nearestRGBA64(temp, slice, scaleY, coeffs, offset, filterLength) + }() + } + wg.Wait() + return result + case *image.NRGBA64: + // 16-bit precision + temp := image.NewNRGBA64(image.Rect(0, 0, input.Bounds().Dy(), int(width))) + result := image.NewNRGBA64(image.Rect(0, 0, int(width), int(height))) + + // horizontal filter, results in transposed temporary image + coeffs, offset, filterLength := createWeightsNearest(temp.Bounds().Dy(), taps, blur, scaleX) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(temp, i, cpus).(*image.NRGBA64) + go func() { + defer wg.Done() + nearestNRGBA64(input, slice, scaleX, coeffs, offset, filterLength) + }() + } + wg.Wait() + + // horizontal filter on transposed image, result is not transposed + coeffs, offset, filterLength = createWeightsNearest(result.Bounds().Dy(), taps, blur, scaleY) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(result, i, cpus).(*image.NRGBA64) + go func() { + defer wg.Done() + nearestNRGBA64(temp, slice, scaleY, coeffs, offset, filterLength) + }() + } + wg.Wait() + return result + case *image.Gray: + // 8-bit precision + temp := image.NewGray(image.Rect(0, 0, input.Bounds().Dy(), int(width))) + result := image.NewGray(image.Rect(0, 0, int(width), int(height))) + + // horizontal filter, results in transposed temporary image + coeffs, offset, filterLength := createWeightsNearest(temp.Bounds().Dy(), taps, blur, scaleX) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(temp, i, cpus).(*image.Gray) + go func() { + defer wg.Done() + nearestGray(input, slice, scaleX, coeffs, offset, filterLength) + }() + } + wg.Wait() + + // horizontal filter on transposed image, result is not transposed + coeffs, offset, filterLength = createWeightsNearest(result.Bounds().Dy(), taps, blur, scaleY) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(result, i, cpus).(*image.Gray) + go func() { + defer wg.Done() + nearestGray(temp, slice, scaleY, coeffs, offset, filterLength) + }() + } + wg.Wait() + return result + case *image.Gray16: + // 16-bit precision + temp := image.NewGray16(image.Rect(0, 0, input.Bounds().Dy(), int(width))) + result := image.NewGray16(image.Rect(0, 0, int(width), int(height))) + + // horizontal filter, results in transposed temporary image + coeffs, offset, filterLength := createWeightsNearest(temp.Bounds().Dy(), taps, blur, scaleX) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(temp, i, cpus).(*image.Gray16) + go func() { + defer wg.Done() + nearestGray16(input, slice, scaleX, coeffs, offset, filterLength) + }() + } + wg.Wait() + + // horizontal filter on transposed image, result is not transposed + coeffs, offset, filterLength = createWeightsNearest(result.Bounds().Dy(), taps, blur, scaleY) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(result, i, cpus).(*image.Gray16) + go func() { + defer wg.Done() + nearestGray16(temp, slice, scaleY, coeffs, offset, filterLength) + }() + } + wg.Wait() + return result + default: + // 16-bit precision + temp := image.NewRGBA64(image.Rect(0, 0, img.Bounds().Dy(), int(width))) + result := image.NewRGBA64(image.Rect(0, 0, int(width), int(height))) + + // horizontal filter, results in transposed temporary image + coeffs, offset, filterLength := createWeightsNearest(temp.Bounds().Dy(), taps, blur, scaleX) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(temp, i, cpus).(*image.RGBA64) + go func() { + defer wg.Done() + nearestGeneric(img, slice, scaleX, coeffs, offset, filterLength) + }() + } + wg.Wait() + + // horizontal filter on transposed image, result is not transposed + coeffs, offset, filterLength = createWeightsNearest(result.Bounds().Dy(), taps, blur, scaleY) + wg.Add(cpus) + for i := 0; i < cpus; i++ { + slice := makeSlice(result, i, cpus).(*image.RGBA64) + go func() { + defer wg.Done() + nearestRGBA64(temp, slice, scaleY, coeffs, offset, filterLength) + }() + } + wg.Wait() + return result + } + +} + +// Calculates scaling factors using old and new image dimensions. +func calcFactors(width, height uint, oldWidth, oldHeight float64) (scaleX, scaleY float64) { + if width == 0 { + if height == 0 { + scaleX = 1.0 + scaleY = 1.0 + } else { + scaleY = oldHeight / float64(height) + scaleX = scaleY + } + } else { + scaleX = oldWidth / float64(width) + if height == 0 { + scaleY = scaleX + } else { + scaleY = oldHeight / float64(height) + } + } + return +} + +type imageWithSubImage interface { + image.Image + SubImage(image.Rectangle) image.Image +} + +func makeSlice(img imageWithSubImage, i, n int) image.Image { + return img.SubImage(image.Rect(img.Bounds().Min.X, img.Bounds().Min.Y+i*img.Bounds().Dy()/n, img.Bounds().Max.X, img.Bounds().Min.Y+(i+1)*img.Bounds().Dy()/n)) +} diff --git a/vendor/src/github.com/nfnt/resize/resize_test.go b/vendor/src/github.com/nfnt/resize/resize_test.go new file mode 100644 index 00000000..d4b80bee --- /dev/null +++ b/vendor/src/github.com/nfnt/resize/resize_test.go @@ -0,0 +1,330 @@ +package resize + +import ( + "image" + "image/color" + "runtime" + "testing" +) + +var img = image.NewGray16(image.Rect(0, 0, 3, 3)) + +func init() { + runtime.GOMAXPROCS(runtime.NumCPU()) + img.Set(1, 1, color.White) +} + +func Test_Param1(t *testing.T) { + m := Resize(0, 0, img, NearestNeighbor) + if m.Bounds() != img.Bounds() { + t.Fail() + } +} + +func Test_Param2(t *testing.T) { + m := Resize(100, 0, img, NearestNeighbor) + if m.Bounds() != image.Rect(0, 0, 100, 100) { + t.Fail() + } +} + +func Test_ZeroImg(t *testing.T) { + zeroImg := image.NewGray16(image.Rect(0, 0, 0, 0)) + + m := Resize(0, 0, zeroImg, NearestNeighbor) + if m.Bounds() != zeroImg.Bounds() { + t.Fail() + } +} + +func Test_CorrectResize(t *testing.T) { + zeroImg := image.NewGray16(image.Rect(0, 0, 256, 256)) + + m := Resize(60, 0, zeroImg, NearestNeighbor) + if m.Bounds() != image.Rect(0, 0, 60, 60) { + t.Fail() + } +} + +func Test_SameColorWithRGBA(t *testing.T) { + img := image.NewRGBA(image.Rect(0, 0, 20, 20)) + for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { + for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ { + img.SetRGBA(x, y, color.RGBA{0x80, 0x80, 0x80, 0xFF}) + } + } + out := Resize(10, 10, img, Lanczos3) + for y := out.Bounds().Min.Y; y < out.Bounds().Max.Y; y++ { + for x := out.Bounds().Min.X; x < out.Bounds().Max.X; x++ { + color := out.At(x, y).(color.RGBA) + if color.R != 0x80 || color.G != 0x80 || color.B != 0x80 || color.A != 0xFF { + t.Errorf("%+v", color) + } + } + } +} + +func Test_SameColorWithNRGBA(t *testing.T) { + img := image.NewNRGBA(image.Rect(0, 0, 20, 20)) + for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { + for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ { + img.SetNRGBA(x, y, color.NRGBA{0x80, 0x80, 0x80, 0xFF}) + } + } + out := Resize(10, 10, img, Lanczos3) + for y := out.Bounds().Min.Y; y < out.Bounds().Max.Y; y++ { + for x := out.Bounds().Min.X; x < out.Bounds().Max.X; x++ { + color := out.At(x, y).(color.RGBA) + if color.R != 0x80 || color.G != 0x80 || color.B != 0x80 || color.A != 0xFF { + t.Errorf("%+v", color) + } + } + } +} + +func Test_SameColorWithRGBA64(t *testing.T) { + img := image.NewRGBA64(image.Rect(0, 0, 20, 20)) + for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { + for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ { + img.SetRGBA64(x, y, color.RGBA64{0x8000, 0x8000, 0x8000, 0xFFFF}) + } + } + out := Resize(10, 10, img, Lanczos3) + for y := out.Bounds().Min.Y; y < out.Bounds().Max.Y; y++ { + for x := out.Bounds().Min.X; x < out.Bounds().Max.X; x++ { + color := out.At(x, y).(color.RGBA64) + if color.R != 0x8000 || color.G != 0x8000 || color.B != 0x8000 || color.A != 0xFFFF { + t.Errorf("%+v", color) + } + } + } +} + +func Test_SameColorWithNRGBA64(t *testing.T) { + img := image.NewNRGBA64(image.Rect(0, 0, 20, 20)) + for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { + for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ { + img.SetNRGBA64(x, y, color.NRGBA64{0x8000, 0x8000, 0x8000, 0xFFFF}) + } + } + out := Resize(10, 10, img, Lanczos3) + for y := out.Bounds().Min.Y; y < out.Bounds().Max.Y; y++ { + for x := out.Bounds().Min.X; x < out.Bounds().Max.X; x++ { + color := out.At(x, y).(color.RGBA64) + if color.R != 0x8000 || color.G != 0x8000 || color.B != 0x8000 || color.A != 0xFFFF { + t.Errorf("%+v", color) + } + } + } +} + +func Test_SameColorWithGray(t *testing.T) { + img := image.NewGray(image.Rect(0, 0, 20, 20)) + for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { + for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ { + img.SetGray(x, y, color.Gray{0x80}) + } + } + out := Resize(10, 10, img, Lanczos3) + for y := out.Bounds().Min.Y; y < out.Bounds().Max.Y; y++ { + for x := out.Bounds().Min.X; x < out.Bounds().Max.X; x++ { + color := out.At(x, y).(color.Gray) + if color.Y != 0x80 { + t.Errorf("%+v", color) + } + } + } +} + +func Test_SameColorWithGray16(t *testing.T) { + img := image.NewGray16(image.Rect(0, 0, 20, 20)) + for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { + for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ { + img.SetGray16(x, y, color.Gray16{0x8000}) + } + } + out := Resize(10, 10, img, Lanczos3) + for y := out.Bounds().Min.Y; y < out.Bounds().Max.Y; y++ { + for x := out.Bounds().Min.X; x < out.Bounds().Max.X; x++ { + color := out.At(x, y).(color.Gray16) + if color.Y != 0x8000 { + t.Errorf("%+v", color) + } + } + } +} + +func Test_Bounds(t *testing.T) { + img := image.NewRGBA(image.Rect(20, 10, 200, 99)) + out := Resize(80, 80, img, Lanczos2) + out.At(0, 0) +} + +func Test_SameSizeReturnsOriginal(t *testing.T) { + img := image.NewRGBA(image.Rect(0, 0, 10, 10)) + out := Resize(0, 0, img, Lanczos2) + + if img != out { + t.Fail() + } + + out = Resize(10, 10, img, Lanczos2) + + if img != out { + t.Fail() + } +} + +func Test_PixelCoordinates(t *testing.T) { + checkers := image.NewGray(image.Rect(0, 0, 4, 4)) + checkers.Pix = []uint8{ + 255, 0, 255, 0, + 0, 255, 0, 255, + 255, 0, 255, 0, + 0, 255, 0, 255, + } + + resized := Resize(12, 12, checkers, NearestNeighbor).(*image.Gray) + + if resized.Pix[0] != 255 || resized.Pix[1] != 255 || resized.Pix[2] != 255 { + t.Fail() + } + + if resized.Pix[3] != 0 || resized.Pix[4] != 0 || resized.Pix[5] != 0 { + t.Fail() + } +} + +func Test_ResizeWithPremultipliedAlpha(t *testing.T) { + img := image.NewRGBA(image.Rect(0, 0, 1, 4)) + for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { + // 0x80 = 0.5 * 0xFF. + img.SetRGBA(0, y, color.RGBA{0x80, 0x80, 0x80, 0x80}) + } + + out := Resize(1, 2, img, MitchellNetravali) + + outputColor := out.At(0, 0).(color.RGBA) + if outputColor.R != 0x80 { + t.Fail() + } +} + +func Test_ResizeWithTranslucentColor(t *testing.T) { + img := image.NewNRGBA(image.Rect(0, 0, 1, 2)) + + // Set the pixel colors to an "invisible green" and white. + // After resizing, the green shouldn't be visible. + img.SetNRGBA(0, 0, color.NRGBA{0x00, 0xFF, 0x00, 0x00}) + img.SetNRGBA(0, 1, color.NRGBA{0x00, 0x00, 0x00, 0xFF}) + + out := Resize(1, 1, img, Bilinear) + + _, g, _, _ := out.At(0, 0).RGBA() + if g != 0x00 { + t.Errorf("%+v", g) + } +} + +const ( + // Use a small image size for benchmarks. We don't want memory performance + // to affect the benchmark results. + benchMaxX = 250 + benchMaxY = 250 + + // Resize values near the original size require increase the amount of time + // resize spends converting the image. + benchWidth = 200 + benchHeight = 200 +) + +func benchRGBA(b *testing.B, interp InterpolationFunction) { + m := image.NewRGBA(image.Rect(0, 0, benchMaxX, benchMaxY)) + // Initialize m's pixels to create a non-uniform image. + for y := m.Rect.Min.Y; y < m.Rect.Max.Y; y++ { + for x := m.Rect.Min.X; x < m.Rect.Max.X; x++ { + i := m.PixOffset(x, y) + m.Pix[i+0] = uint8(y + 4*x) + m.Pix[i+1] = uint8(y + 4*x) + m.Pix[i+2] = uint8(y + 4*x) + m.Pix[i+3] = uint8(4*y + x) + } + } + + var out image.Image + b.ResetTimer() + for i := 0; i < b.N; i++ { + out = Resize(benchWidth, benchHeight, m, interp) + } + out.At(0, 0) +} + +// The names of some interpolation functions are truncated so that the columns +// of 'go test -bench' line up. +func Benchmark_Nearest_RGBA(b *testing.B) { + benchRGBA(b, NearestNeighbor) +} + +func Benchmark_Bilinear_RGBA(b *testing.B) { + benchRGBA(b, Bilinear) +} + +func Benchmark_Bicubic_RGBA(b *testing.B) { + benchRGBA(b, Bicubic) +} + +func Benchmark_Mitchell_RGBA(b *testing.B) { + benchRGBA(b, MitchellNetravali) +} + +func Benchmark_Lanczos2_RGBA(b *testing.B) { + benchRGBA(b, Lanczos2) +} + +func Benchmark_Lanczos3_RGBA(b *testing.B) { + benchRGBA(b, Lanczos3) +} + +func benchYCbCr(b *testing.B, interp InterpolationFunction) { + m := image.NewYCbCr(image.Rect(0, 0, benchMaxX, benchMaxY), image.YCbCrSubsampleRatio422) + // Initialize m's pixels to create a non-uniform image. + for y := m.Rect.Min.Y; y < m.Rect.Max.Y; y++ { + for x := m.Rect.Min.X; x < m.Rect.Max.X; x++ { + yi := m.YOffset(x, y) + ci := m.COffset(x, y) + m.Y[yi] = uint8(16*y + x) + m.Cb[ci] = uint8(y + 16*x) + m.Cr[ci] = uint8(y + 16*x) + } + } + var out image.Image + b.ResetTimer() + for i := 0; i < b.N; i++ { + out = Resize(benchWidth, benchHeight, m, interp) + } + out.At(0, 0) +} + +func Benchmark_Nearest_YCC(b *testing.B) { + benchYCbCr(b, NearestNeighbor) +} + +func Benchmark_Bilinear_YCC(b *testing.B) { + benchYCbCr(b, Bilinear) +} + +func Benchmark_Bicubic_YCC(b *testing.B) { + benchYCbCr(b, Bicubic) +} + +func Benchmark_Mitchell_YCC(b *testing.B) { + benchYCbCr(b, MitchellNetravali) +} + +func Benchmark_Lanczos2_YCC(b *testing.B) { + benchYCbCr(b, Lanczos2) +} + +func Benchmark_Lanczos3_YCC(b *testing.B) { + benchYCbCr(b, Lanczos3) +} diff --git a/vendor/src/github.com/nfnt/resize/thumbnail.go b/vendor/src/github.com/nfnt/resize/thumbnail.go new file mode 100644 index 00000000..9efc246b --- /dev/null +++ b/vendor/src/github.com/nfnt/resize/thumbnail.go @@ -0,0 +1,55 @@ +/* +Copyright (c) 2012, Jan Schlicht + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. +*/ + +package resize + +import ( + "image" +) + +// Thumbnail will downscale provided image to max width and height preserving +// original aspect ratio and using the interpolation function interp. +// It will return original image, without processing it, if original sizes +// are already smaller than provided constraints. +func Thumbnail(maxWidth, maxHeight uint, img image.Image, interp InterpolationFunction) image.Image { + origBounds := img.Bounds() + origWidth := uint(origBounds.Dx()) + origHeight := uint(origBounds.Dy()) + newWidth, newHeight := origWidth, origHeight + + // Return original image if it have same or smaller size as constraints + if maxWidth >= origWidth && maxHeight >= origHeight { + return img + } + + // Preserve aspect ratio + if origWidth > maxWidth { + newHeight = uint(origHeight * maxWidth / origWidth) + if newHeight < 1 { + newHeight = 1 + } + newWidth = maxWidth + } + + if newHeight > maxHeight { + newWidth = uint(newWidth * maxHeight / newHeight) + if newWidth < 1 { + newWidth = 1 + } + newHeight = maxHeight + } + return Resize(newWidth, newHeight, img, interp) +} diff --git a/vendor/src/github.com/nfnt/resize/thumbnail_test.go b/vendor/src/github.com/nfnt/resize/thumbnail_test.go new file mode 100644 index 00000000..bd9875b2 --- /dev/null +++ b/vendor/src/github.com/nfnt/resize/thumbnail_test.go @@ -0,0 +1,47 @@ +package resize + +import ( + "image" + "runtime" + "testing" +) + +func init() { + runtime.GOMAXPROCS(runtime.NumCPU()) +} + +var thumbnailTests = []struct { + origWidth int + origHeight int + maxWidth uint + maxHeight uint + expectedWidth uint + expectedHeight uint +}{ + {5, 5, 10, 10, 5, 5}, + {10, 10, 5, 5, 5, 5}, + {10, 50, 10, 10, 2, 10}, + {50, 10, 10, 10, 10, 2}, + {50, 100, 60, 90, 45, 90}, + {120, 100, 60, 90, 60, 50}, + {200, 250, 200, 150, 120, 150}, +} + +func TestThumbnail(t *testing.T) { + for i, tt := range thumbnailTests { + img := image.NewGray16(image.Rect(0, 0, tt.origWidth, tt.origHeight)) + + outImg := Thumbnail(tt.maxWidth, tt.maxHeight, img, NearestNeighbor) + + newWidth := uint(outImg.Bounds().Dx()) + newHeight := uint(outImg.Bounds().Dy()) + if newWidth != tt.expectedWidth || + newHeight != tt.expectedHeight { + t.Errorf("%d. Thumbnail(%v, %v, img, NearestNeighbor) => "+ + "width: %v, height: %v, want width: %v, height: %v", + i, tt.maxWidth, tt.maxHeight, + newWidth, newHeight, tt.expectedWidth, tt.expectedHeight, + ) + } + } +} diff --git a/vendor/src/github.com/nfnt/resize/ycc.go b/vendor/src/github.com/nfnt/resize/ycc.go new file mode 100644 index 00000000..10415995 --- /dev/null +++ b/vendor/src/github.com/nfnt/resize/ycc.go @@ -0,0 +1,227 @@ +/* +Copyright (c) 2014, Charlie Vieth + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. +*/ + +package resize + +import ( + "image" + "image/color" +) + +// ycc is an in memory YCbCr image. The Y, Cb and Cr samples are held in a +// single slice to increase resizing performance. +type ycc struct { + // Pix holds the image's pixels, in Y, Cb, Cr order. The pixel at + // (x, y) starts at Pix[(y-Rect.Min.Y)*Stride + (x-Rect.Min.X)*3]. + Pix []uint8 + // Stride is the Pix stride (in bytes) between vertically adjacent pixels. + Stride int + // Rect is the image's bounds. + Rect image.Rectangle + // SubsampleRatio is the subsample ratio of the original YCbCr image. + SubsampleRatio image.YCbCrSubsampleRatio +} + +// PixOffset returns the index of the first element of Pix that corresponds to +// the pixel at (x, y). +func (p *ycc) PixOffset(x, y int) int { + return (y-p.Rect.Min.Y)*p.Stride + (x-p.Rect.Min.X)*3 +} + +func (p *ycc) Bounds() image.Rectangle { + return p.Rect +} + +func (p *ycc) ColorModel() color.Model { + return color.YCbCrModel +} + +func (p *ycc) At(x, y int) color.Color { + if !(image.Point{x, y}.In(p.Rect)) { + return color.YCbCr{} + } + i := p.PixOffset(x, y) + return color.YCbCr{ + p.Pix[i+0], + p.Pix[i+1], + p.Pix[i+2], + } +} + +func (p *ycc) Opaque() bool { + return true +} + +// SubImage returns an image representing the portion of the image p visible +// through r. The returned value shares pixels with the original image. +func (p *ycc) SubImage(r image.Rectangle) image.Image { + r = r.Intersect(p.Rect) + if r.Empty() { + return &ycc{SubsampleRatio: p.SubsampleRatio} + } + i := p.PixOffset(r.Min.X, r.Min.Y) + return &ycc{ + Pix: p.Pix[i:], + Stride: p.Stride, + Rect: r, + SubsampleRatio: p.SubsampleRatio, + } +} + +// newYCC returns a new ycc with the given bounds and subsample ratio. +func newYCC(r image.Rectangle, s image.YCbCrSubsampleRatio) *ycc { + w, h := r.Dx(), r.Dy() + buf := make([]uint8, 3*w*h) + return &ycc{Pix: buf, Stride: 3 * w, Rect: r, SubsampleRatio: s} +} + +// YCbCr converts ycc to a YCbCr image with the same subsample ratio +// as the YCbCr image that ycc was generated from. +func (p *ycc) YCbCr() *image.YCbCr { + ycbcr := image.NewYCbCr(p.Rect, p.SubsampleRatio) + var off int + + switch ycbcr.SubsampleRatio { + case image.YCbCrSubsampleRatio422: + for y := ycbcr.Rect.Min.Y; y < ycbcr.Rect.Max.Y; y++ { + yy := (y - ycbcr.Rect.Min.Y) * ycbcr.YStride + cy := (y - ycbcr.Rect.Min.Y) * ycbcr.CStride + for x := ycbcr.Rect.Min.X; x < ycbcr.Rect.Max.X; x++ { + xx := (x - ycbcr.Rect.Min.X) + yi := yy + xx + ci := cy + xx/2 + ycbcr.Y[yi] = p.Pix[off+0] + ycbcr.Cb[ci] = p.Pix[off+1] + ycbcr.Cr[ci] = p.Pix[off+2] + off += 3 + } + } + case image.YCbCrSubsampleRatio420: + for y := ycbcr.Rect.Min.Y; y < ycbcr.Rect.Max.Y; y++ { + yy := (y - ycbcr.Rect.Min.Y) * ycbcr.YStride + cy := (y/2 - ycbcr.Rect.Min.Y/2) * ycbcr.CStride + for x := ycbcr.Rect.Min.X; x < ycbcr.Rect.Max.X; x++ { + xx := (x - ycbcr.Rect.Min.X) + yi := yy + xx + ci := cy + xx/2 + ycbcr.Y[yi] = p.Pix[off+0] + ycbcr.Cb[ci] = p.Pix[off+1] + ycbcr.Cr[ci] = p.Pix[off+2] + off += 3 + } + } + case image.YCbCrSubsampleRatio440: + for y := ycbcr.Rect.Min.Y; y < ycbcr.Rect.Max.Y; y++ { + yy := (y - ycbcr.Rect.Min.Y) * ycbcr.YStride + cy := (y/2 - ycbcr.Rect.Min.Y/2) * ycbcr.CStride + for x := ycbcr.Rect.Min.X; x < ycbcr.Rect.Max.X; x++ { + xx := (x - ycbcr.Rect.Min.X) + yi := yy + xx + ci := cy + xx + ycbcr.Y[yi] = p.Pix[off+0] + ycbcr.Cb[ci] = p.Pix[off+1] + ycbcr.Cr[ci] = p.Pix[off+2] + off += 3 + } + } + default: + // Default to 4:4:4 subsampling. + for y := ycbcr.Rect.Min.Y; y < ycbcr.Rect.Max.Y; y++ { + yy := (y - ycbcr.Rect.Min.Y) * ycbcr.YStride + cy := (y - ycbcr.Rect.Min.Y) * ycbcr.CStride + for x := ycbcr.Rect.Min.X; x < ycbcr.Rect.Max.X; x++ { + xx := (x - ycbcr.Rect.Min.X) + yi := yy + xx + ci := cy + xx + ycbcr.Y[yi] = p.Pix[off+0] + ycbcr.Cb[ci] = p.Pix[off+1] + ycbcr.Cr[ci] = p.Pix[off+2] + off += 3 + } + } + } + return ycbcr +} + +// imageYCbCrToYCC converts a YCbCr image to a ycc image for resizing. +func imageYCbCrToYCC(in *image.YCbCr) *ycc { + w, h := in.Rect.Dx(), in.Rect.Dy() + r := image.Rect(0, 0, w, h) + buf := make([]uint8, 3*w*h) + p := ycc{Pix: buf, Stride: 3 * w, Rect: r, SubsampleRatio: in.SubsampleRatio} + var off int + + switch in.SubsampleRatio { + case image.YCbCrSubsampleRatio422: + for y := in.Rect.Min.Y; y < in.Rect.Max.Y; y++ { + yy := (y - in.Rect.Min.Y) * in.YStride + cy := (y - in.Rect.Min.Y) * in.CStride + for x := in.Rect.Min.X; x < in.Rect.Max.X; x++ { + xx := (x - in.Rect.Min.X) + yi := yy + xx + ci := cy + xx/2 + p.Pix[off+0] = in.Y[yi] + p.Pix[off+1] = in.Cb[ci] + p.Pix[off+2] = in.Cr[ci] + off += 3 + } + } + case image.YCbCrSubsampleRatio420: + for y := in.Rect.Min.Y; y < in.Rect.Max.Y; y++ { + yy := (y - in.Rect.Min.Y) * in.YStride + cy := (y/2 - in.Rect.Min.Y/2) * in.CStride + for x := in.Rect.Min.X; x < in.Rect.Max.X; x++ { + xx := (x - in.Rect.Min.X) + yi := yy + xx + ci := cy + xx/2 + p.Pix[off+0] = in.Y[yi] + p.Pix[off+1] = in.Cb[ci] + p.Pix[off+2] = in.Cr[ci] + off += 3 + } + } + case image.YCbCrSubsampleRatio440: + for y := in.Rect.Min.Y; y < in.Rect.Max.Y; y++ { + yy := (y - in.Rect.Min.Y) * in.YStride + cy := (y/2 - in.Rect.Min.Y/2) * in.CStride + for x := in.Rect.Min.X; x < in.Rect.Max.X; x++ { + xx := (x - in.Rect.Min.X) + yi := yy + xx + ci := cy + xx + p.Pix[off+0] = in.Y[yi] + p.Pix[off+1] = in.Cb[ci] + p.Pix[off+2] = in.Cr[ci] + off += 3 + } + } + default: + // Default to 4:4:4 subsampling. + for y := in.Rect.Min.Y; y < in.Rect.Max.Y; y++ { + yy := (y - in.Rect.Min.Y) * in.YStride + cy := (y - in.Rect.Min.Y) * in.CStride + for x := in.Rect.Min.X; x < in.Rect.Max.X; x++ { + xx := (x - in.Rect.Min.X) + yi := yy + xx + ci := cy + xx + p.Pix[off+0] = in.Y[yi] + p.Pix[off+1] = in.Cb[ci] + p.Pix[off+2] = in.Cr[ci] + off += 3 + } + } + } + return &p +} diff --git a/vendor/src/github.com/nfnt/resize/ycc_test.go b/vendor/src/github.com/nfnt/resize/ycc_test.go new file mode 100644 index 00000000..54d53d15 --- /dev/null +++ b/vendor/src/github.com/nfnt/resize/ycc_test.go @@ -0,0 +1,214 @@ +/* +Copyright (c) 2014, Charlie Vieth + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. +*/ + +package resize + +import ( + "image" + "image/color" + "testing" +) + +type Image interface { + image.Image + SubImage(image.Rectangle) image.Image +} + +func TestImage(t *testing.T) { + testImage := []Image{ + newYCC(image.Rect(0, 0, 10, 10), image.YCbCrSubsampleRatio420), + newYCC(image.Rect(0, 0, 10, 10), image.YCbCrSubsampleRatio422), + newYCC(image.Rect(0, 0, 10, 10), image.YCbCrSubsampleRatio440), + newYCC(image.Rect(0, 0, 10, 10), image.YCbCrSubsampleRatio444), + } + for _, m := range testImage { + if !image.Rect(0, 0, 10, 10).Eq(m.Bounds()) { + t.Errorf("%T: want bounds %v, got %v", + m, image.Rect(0, 0, 10, 10), m.Bounds()) + continue + } + m = m.SubImage(image.Rect(3, 2, 9, 8)).(Image) + if !image.Rect(3, 2, 9, 8).Eq(m.Bounds()) { + t.Errorf("%T: sub-image want bounds %v, got %v", + m, image.Rect(3, 2, 9, 8), m.Bounds()) + continue + } + // Test that taking an empty sub-image starting at a corner does not panic. + m.SubImage(image.Rect(0, 0, 0, 0)) + m.SubImage(image.Rect(10, 0, 10, 0)) + m.SubImage(image.Rect(0, 10, 0, 10)) + m.SubImage(image.Rect(10, 10, 10, 10)) + } +} + +func TestConvertYCbCr(t *testing.T) { + testImage := []Image{ + image.NewYCbCr(image.Rect(0, 0, 50, 50), image.YCbCrSubsampleRatio420), + image.NewYCbCr(image.Rect(0, 0, 50, 50), image.YCbCrSubsampleRatio422), + image.NewYCbCr(image.Rect(0, 0, 50, 50), image.YCbCrSubsampleRatio440), + image.NewYCbCr(image.Rect(0, 0, 50, 50), image.YCbCrSubsampleRatio444), + } + + for _, img := range testImage { + m := img.(*image.YCbCr) + for y := m.Rect.Min.Y; y < m.Rect.Max.Y; y++ { + for x := m.Rect.Min.X; x < m.Rect.Max.X; x++ { + yi := m.YOffset(x, y) + ci := m.COffset(x, y) + m.Y[yi] = uint8(16*y + x) + m.Cb[ci] = uint8(y + 16*x) + m.Cr[ci] = uint8(y + 16*x) + } + } + + // test conversion from YCbCr to ycc + yc := imageYCbCrToYCC(m) + for y := m.Rect.Min.Y; y < m.Rect.Max.Y; y++ { + for x := m.Rect.Min.X; x < m.Rect.Max.X; x++ { + ystride := 3 * (m.Rect.Max.X - m.Rect.Min.X) + xstride := 3 + yi := m.YOffset(x, y) + ci := m.COffset(x, y) + si := (y * ystride) + (x * xstride) + if m.Y[yi] != yc.Pix[si] { + t.Errorf("Err Y - found: %d expected: %d x: %d y: %d yi: %d si: %d", + m.Y[yi], yc.Pix[si], x, y, yi, si) + } + if m.Cb[ci] != yc.Pix[si+1] { + t.Errorf("Err Cb - found: %d expected: %d x: %d y: %d ci: %d si: %d", + m.Cb[ci], yc.Pix[si+1], x, y, ci, si+1) + } + if m.Cr[ci] != yc.Pix[si+2] { + t.Errorf("Err Cr - found: %d expected: %d x: %d y: %d ci: %d si: %d", + m.Cr[ci], yc.Pix[si+2], x, y, ci, si+2) + } + } + } + + // test conversion from ycc back to YCbCr + ym := yc.YCbCr() + for y := m.Rect.Min.Y; y < m.Rect.Max.Y; y++ { + for x := m.Rect.Min.X; x < m.Rect.Max.X; x++ { + yi := m.YOffset(x, y) + ci := m.COffset(x, y) + if m.Y[yi] != ym.Y[yi] { + t.Errorf("Err Y - found: %d expected: %d x: %d y: %d yi: %d", + m.Y[yi], ym.Y[yi], x, y, yi) + } + if m.Cb[ci] != ym.Cb[ci] { + t.Errorf("Err Cb - found: %d expected: %d x: %d y: %d ci: %d", + m.Cb[ci], ym.Cb[ci], x, y, ci) + } + if m.Cr[ci] != ym.Cr[ci] { + t.Errorf("Err Cr - found: %d expected: %d x: %d y: %d ci: %d", + m.Cr[ci], ym.Cr[ci], x, y, ci) + } + } + } + } +} + +func TestYCbCr(t *testing.T) { + rects := []image.Rectangle{ + image.Rect(0, 0, 16, 16), + image.Rect(1, 0, 16, 16), + image.Rect(0, 1, 16, 16), + image.Rect(1, 1, 16, 16), + image.Rect(1, 1, 15, 16), + image.Rect(1, 1, 16, 15), + image.Rect(1, 1, 15, 15), + image.Rect(2, 3, 14, 15), + image.Rect(7, 0, 7, 16), + image.Rect(0, 8, 16, 8), + image.Rect(0, 0, 10, 11), + image.Rect(5, 6, 16, 16), + image.Rect(7, 7, 8, 8), + image.Rect(7, 8, 8, 9), + image.Rect(8, 7, 9, 8), + image.Rect(8, 8, 9, 9), + image.Rect(7, 7, 17, 17), + image.Rect(8, 8, 17, 17), + image.Rect(9, 9, 17, 17), + image.Rect(10, 10, 17, 17), + } + subsampleRatios := []image.YCbCrSubsampleRatio{ + image.YCbCrSubsampleRatio444, + image.YCbCrSubsampleRatio422, + image.YCbCrSubsampleRatio420, + image.YCbCrSubsampleRatio440, + } + deltas := []image.Point{ + image.Pt(0, 0), + image.Pt(1000, 1001), + image.Pt(5001, -400), + image.Pt(-701, -801), + } + for _, r := range rects { + for _, subsampleRatio := range subsampleRatios { + for _, delta := range deltas { + testYCbCr(t, r, subsampleRatio, delta) + } + } + if testing.Short() { + break + } + } +} + +func testYCbCr(t *testing.T, r image.Rectangle, subsampleRatio image.YCbCrSubsampleRatio, delta image.Point) { + // Create a YCbCr image m, whose bounds are r translated by (delta.X, delta.Y). + r1 := r.Add(delta) + img := image.NewYCbCr(r1, subsampleRatio) + + // Initialize img's pixels. For 422 and 420 subsampling, some of the Cb and Cr elements + // will be set multiple times. That's OK. We just want to avoid a uniform image. + for y := r1.Min.Y; y < r1.Max.Y; y++ { + for x := r1.Min.X; x < r1.Max.X; x++ { + yi := img.YOffset(x, y) + ci := img.COffset(x, y) + img.Y[yi] = uint8(16*y + x) + img.Cb[ci] = uint8(y + 16*x) + img.Cr[ci] = uint8(y + 16*x) + } + } + + m := imageYCbCrToYCC(img) + + // Make various sub-images of m. + for y0 := delta.Y + 3; y0 < delta.Y+7; y0++ { + for y1 := delta.Y + 8; y1 < delta.Y+13; y1++ { + for x0 := delta.X + 3; x0 < delta.X+7; x0++ { + for x1 := delta.X + 8; x1 < delta.X+13; x1++ { + subRect := image.Rect(x0, y0, x1, y1) + sub := m.SubImage(subRect).(*ycc) + + // For each point in the sub-image's bounds, check that m.At(x, y) equals sub.At(x, y). + for y := sub.Rect.Min.Y; y < sub.Rect.Max.Y; y++ { + for x := sub.Rect.Min.X; x < sub.Rect.Max.X; x++ { + color0 := m.At(x, y).(color.YCbCr) + color1 := sub.At(x, y).(color.YCbCr) + if color0 != color1 { + t.Errorf("r=%v, subsampleRatio=%v, delta=%v, x=%d, y=%d, color0=%v, color1=%v", + r, subsampleRatio, delta, x, y, color0, color1) + return + } + } + } + } + } + } + } +} diff --git a/vendor/src/github.com/tj/go-debug/History.md b/vendor/src/github.com/tj/go-debug/History.md new file mode 100644 index 00000000..318ceb4d --- /dev/null +++ b/vendor/src/github.com/tj/go-debug/History.md @@ -0,0 +1,21 @@ + +v2.0.0 / 2014-10-22 +================== + + * remove live toggling feature. Closes #10 + +1.1.1 / 2014-07-07 +================== + + * fix: dispose socket. Closes #1 + +1.1.0 / 2014-06-29 +================== + + * add unix domain socket live debugging support + * add support for enabling/disabling at runtime + +0.1.0 / 2014-05-24 +================== + + * add global and debug relative deltas diff --git a/vendor/src/github.com/tj/go-debug/Makefile b/vendor/src/github.com/tj/go-debug/Makefile new file mode 100644 index 00000000..16bc6d36 --- /dev/null +++ b/vendor/src/github.com/tj/go-debug/Makefile @@ -0,0 +1,8 @@ + +test: + @go test + +bench: + @go test -bench=. + +.PHONY: bench test \ No newline at end of file diff --git a/vendor/src/github.com/tj/go-debug/Readme.md b/vendor/src/github.com/tj/go-debug/Readme.md new file mode 100644 index 00000000..6560af8a --- /dev/null +++ b/vendor/src/github.com/tj/go-debug/Readme.md @@ -0,0 +1,75 @@ + +# go-debug + + Conditional debug logging for Go libraries. + + View the [docs](http://godoc.org/github.com/tj/go-debug). + +## Installation + +``` +$ go get github.com/tj/go-debug +``` + +## Example + +```go +package main + +import . "github.com/tj/go-debug" +import "time" + +var debug = Debug("single") + +func main() { + for { + debug("sending mail") + debug("send email to %s", "tobi@segment.io") + debug("send email to %s", "loki@segment.io") + debug("send email to %s", "jane@segment.io") + time.Sleep(500 * time.Millisecond) + } +} +``` + +If you run the program with the `DEBUG=*` environment variable you will see: + +``` +15:58:15.115 34us 33us single - sending mail +15:58:15.116 3us 3us single - send email to tobi@segment.io +15:58:15.116 1us 1us single - send email to loki@segment.io +15:58:15.116 1us 1us single - send email to jane@segment.io +15:58:15.620 504ms 504ms single - sending mail +15:58:15.620 6us 6us single - send email to tobi@segment.io +15:58:15.620 4us 4us single - send email to loki@segment.io +15:58:15.620 4us 4us single - send email to jane@segment.io +15:58:16.123 503ms 503ms single - sending mail +15:58:16.123 7us 7us single - send email to tobi@segment.io +15:58:16.123 4us 4us single - send email to loki@segment.io +15:58:16.123 4us 4us single - send email to jane@segment.io +15:58:16.625 501ms 501ms single - sending mail +15:58:16.625 4us 4us single - send email to tobi@segment.io +15:58:16.625 4us 4us single - send email to loki@segment.io +15:58:16.625 5us 5us single - send email to jane@segment.io +``` + +A timestamp and two deltas are displayed. The timestamp consists of hour, minute, second and microseconds. The left-most delta is relative to the previous debug call of any name, followed by a delta specific to that debug function. These may be useful to identify timing issues and potential bottlenecks. + +## The DEBUG environment variable + + Executables often support `--verbose` flags for conditional logging, however + libraries typically either require altering your code to enable logging, + or simply omit logging all together. go-debug allows conditional logging + to be enabled via the __DEBUG__ environment variable, where one or more + patterns may be specified. + + For example suppose your application has several models and you want + to output logs for users only, you might use `DEBUG=models:user`. In contrast + if you wanted to see what all database activity was you might use `DEBUG=models:*`, + or if you're love being swamped with logs: `DEBUG=*`. You may also specify a list of names delimited by a comma, for example `DEBUG=mongo,redis:*`. + + The name given _should_ be the package name, however you can use whatever you like. + +# License + +MIT \ No newline at end of file diff --git a/vendor/src/github.com/tj/go-debug/debug.go b/vendor/src/github.com/tj/go-debug/debug.go new file mode 100644 index 00000000..016ca469 --- /dev/null +++ b/vendor/src/github.com/tj/go-debug/debug.go @@ -0,0 +1,128 @@ +package debug + +import ( + "fmt" + "io" + "math/rand" + "os" + "regexp" + "strconv" + "strings" + "sync" + "time" +) + +var ( + writer io.Writer = os.Stderr + reg *regexp.Regexp + m sync.Mutex + enabled = false +) + +// Debugger function. +type DebugFunction func(string, ...interface{}) + +// Terminal colors used at random. +var colors []string = []string{ + "31", + "32", + "33", + "34", + "35", + "36", +} + +// Initialize with DEBUG environment variable. +func init() { + env := os.Getenv("DEBUG") + + if "" != env { + Enable(env) + } +} + +// SetWriter replaces the default of os.Stderr with `w`. +func SetWriter(w io.Writer) { + m.Lock() + defer m.Unlock() + writer = w +} + +// Disable all pattern matching. This function is thread-safe. +func Disable() { + m.Lock() + defer m.Unlock() + enabled = false +} + +// Enable the given debug `pattern`. Patterns take a glob-like form, +// for example if you wanted to enable everything, just use "*", or +// if you had a library named mongodb you could use "mongodb:connection", +// or "mongodb:*". Multiple matches can be made with a comma, for +// example "mongo*,redis*". +// +// This function is thread-safe. +func Enable(pattern string) { + m.Lock() + defer m.Unlock() + pattern = regexp.QuoteMeta(pattern) + pattern = strings.Replace(pattern, "\\*", ".*?", -1) + pattern = strings.Replace(pattern, ",", "|", -1) + pattern = "^(" + pattern + ")$" + reg = regexp.MustCompile(pattern) + enabled = true +} + +// Debug creates a debug function for `name` which you call +// with printf-style arguments in your application or library. +func Debug(name string) DebugFunction { + prevGlobal := time.Now() + color := colors[rand.Intn(len(colors))] + prev := time.Now() + + return func(format string, args ...interface{}) { + if !enabled { + return + } + + if !reg.MatchString(name) { + return + } + + d := deltas(prevGlobal, prev, color) + fmt.Fprintf(writer, d+" \033["+color+"m"+name+"\033[0m - "+format+"\n", args...) + prevGlobal = time.Now() + prev = time.Now() + } +} + +// Return formatting for deltas. +func deltas(prevGlobal, prev time.Time, color string) string { + now := time.Now() + global := now.Sub(prevGlobal).Nanoseconds() + delta := now.Sub(prev).Nanoseconds() + ts := now.UTC().Format("15:04:05.000") + deltas := fmt.Sprintf("%s %-6s \033["+color+"m%-6s", ts, humanizeNano(global), humanizeNano(delta)) + return deltas +} + +// Humanize nanoseconds to a string. +func humanizeNano(n int64) string { + var suffix string + + switch { + case n > 1e9: + n /= 1e9 + suffix = "s" + case n > 1e6: + n /= 1e6 + suffix = "ms" + case n > 1e3: + n /= 1e3 + suffix = "us" + default: + suffix = "ns" + } + + return strconv.Itoa(int(n)) + suffix +} diff --git a/vendor/src/github.com/tj/go-debug/debug_test.go b/vendor/src/github.com/tj/go-debug/debug_test.go new file mode 100644 index 00000000..7ce2764c --- /dev/null +++ b/vendor/src/github.com/tj/go-debug/debug_test.go @@ -0,0 +1,152 @@ +package debug + +import "testing" +import "strings" +import "bytes" +import "time" + +func assertContains(t *testing.T, str, substr string) { + if !strings.Contains(str, substr) { + t.Fatalf("expected %q to contain %q", str, substr) + } +} + +func assertNotContains(t *testing.T, str, substr string) { + if strings.Contains(str, substr) { + t.Fatalf("expected %q to not contain %q", str, substr) + } +} + +func TestDefault(t *testing.T) { + var b []byte + buf := bytes.NewBuffer(b) + SetWriter(buf) + + debug := Debug("foo") + debug("something") + debug("here") + debug("whoop") + + if buf.Len() != 0 { + t.Fatalf("buffer should be empty") + } +} + +func TestEnable(t *testing.T) { + var b []byte + buf := bytes.NewBuffer(b) + SetWriter(buf) + + Enable("foo") + + debug := Debug("foo") + debug("something") + debug("here") + debug("whoop") + + if buf.Len() == 0 { + t.Fatalf("buffer should have output") + } + + str := string(buf.Bytes()) + assertContains(t, str, "something") + assertContains(t, str, "here") + assertContains(t, str, "whoop") +} + +func TestMultipleOneEnabled(t *testing.T) { + var b []byte + buf := bytes.NewBuffer(b) + SetWriter(buf) + + Enable("foo") + + foo := Debug("foo") + foo("foo") + + bar := Debug("bar") + bar("bar") + + if buf.Len() == 0 { + t.Fatalf("buffer should have output") + } + + str := string(buf.Bytes()) + assertContains(t, str, "foo") + assertNotContains(t, str, "bar") +} + +func TestMultipleEnabled(t *testing.T) { + var b []byte + buf := bytes.NewBuffer(b) + SetWriter(buf) + + Enable("foo,bar") + + foo := Debug("foo") + foo("foo") + + bar := Debug("bar") + bar("bar") + + if buf.Len() == 0 { + t.Fatalf("buffer should have output") + } + + str := string(buf.Bytes()) + assertContains(t, str, "foo") + assertContains(t, str, "bar") +} + +func TestEnableDisable(t *testing.T) { + var b []byte + buf := bytes.NewBuffer(b) + SetWriter(buf) + + Enable("foo,bar") + Disable() + + foo := Debug("foo") + foo("foo") + + bar := Debug("bar") + bar("bar") + + if buf.Len() != 0 { + t.Fatalf("buffer should not have output") + } +} + +func ExampleEnable() { + Enable("mongo:connection") + Enable("mongo:*") + Enable("foo,bar,baz") + Enable("*") +} + +func ExampleDebug() { + var debug = Debug("single") + + for { + debug("sending mail") + debug("send email to %s", "tobi@segment.io") + debug("send email to %s", "loki@segment.io") + debug("send email to %s", "jane@segment.io") + time.Sleep(500 * time.Millisecond) + } +} + +func BenchmarkDisabled(b *testing.B) { + debug := Debug("something") + for i := 0; i < b.N; i++ { + debug("stuff") + } +} + +func BenchmarkNonMatch(b *testing.B) { + debug := Debug("something") + Enable("nonmatch") + for i := 0; i < b.N; i++ { + debug("stuff") + } +} diff --git a/vendor/src/github.com/tj/go-debug/example/multiple.go b/vendor/src/github.com/tj/go-debug/example/multiple.go new file mode 100644 index 00000000..81c33080 --- /dev/null +++ b/vendor/src/github.com/tj/go-debug/example/multiple.go @@ -0,0 +1,25 @@ +package main + +import . "github.com/visionmedia/go-debug" +import "time" + +var a = Debug("multiple:a") +var b = Debug("multiple:b") +var c = Debug("multiple:c") + +func work(debug DebugFunction, delay time.Duration) { + for { + debug("doing stuff") + time.Sleep(delay) + } +} + +func main() { + q := make(chan bool) + + go work(a, 1000*time.Millisecond) + go work(b, 250*time.Millisecond) + go work(c, 100*time.Millisecond) + + <-q +} diff --git a/vendor/src/github.com/tj/go-debug/example/single.go b/vendor/src/github.com/tj/go-debug/example/single.go new file mode 100644 index 00000000..fccfe33f --- /dev/null +++ b/vendor/src/github.com/tj/go-debug/example/single.go @@ -0,0 +1,16 @@ +package main + +import . "github.com/visionmedia/go-debug" +import "time" + +var debug = Debug("single") + +func main() { + for { + debug("sending mail") + debug("send email to %s", "tobi@segment.io") + debug("send email to %s", "loki@segment.io") + debug("send email to %s", "jane@segment.io") + time.Sleep(500 * time.Millisecond) + } +} diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/History.md b/vendor/src/gopkg.in/h2non/bimg.v1/History.md new file mode 100644 index 00000000..57cdfc54 --- /dev/null +++ b/vendor/src/gopkg.in/h2non/bimg.v1/History.md @@ -0,0 +1,85 @@ + +## v1.0.9 / 2017-05-25 + + * Merge pull request #156 from Dynom/SmartCropToGravity + * Adding a test, verifying both ways of enabling SmartCrop work + * Merge pull request #149 from waldophotos/master + * Replacing SmartCrop with a Gravity option + * refactor(docs): v8.4 + * Change for older LIBVIPS versions. `vips_bandjoin_const1` is added in libvips 8.2. + * Second try, watermarking memory issue fix + +## v1.0.8 / 2017-05-18 + + * Merge pull request #145 from greut/smartcrop + * Merge pull request #155 from greut/libvips8.5.5 + * Update libvips to 8.5.5. + * Adding basic smartcrop support. + * Merge pull request #153 from abracadaber/master + * Added Linux Mint 17.3+ distro names + * feat(docs): add new maintainer notice (thanks to @kirillDanshin) + * Merge pull request #152 from greut/libvips85 + * Download latest version of libvips from github. + * Merge pull request #147 from h2non/revert-143-master + * Revert "Fix for memory issue when watermarking images" + * Merge pull request #146 from greut/minor-major + * Merge pull request #143 from waldophotos/master + * Merge pull request #144 from greut/go18 + * Fix tests where minor/major were mixed up + * Enabled go 1.8 builds. + * Fix the unref of images, when image isn't transparent + * Fix for memory issue when watermarking images + * feat(docs): add maintainers sections + * Merge pull request #132 from jaume-pinyol/WATERMARK_SUPPORT + * Add support for image watermarks + * Merge pull request #131 from greut/versions + * Running tests on more specific versions. + * refactor(preinstall.sh): remove deprecation notice + * Update preinstall.sh + * fix(requirements): required libvips 7.42 + * fix(History): typo + * chore(History): add breaking change note + +## v1.0.7 / 13-01-2017 + +- fix(#128): crop image calculation for missing width or height axis. +- feat: add TIFF save output format (**note**: this introduces a minor interface breaking change in `bimg.IsImageTypeSupportedByVips` auxiliary function). + +## v1.0.6 / 12-11-2016 + +- feat(#118): handle 16-bit PNGs. +- feat(#119): adds JPEG2000 file for the type tests. +- feat(#121): test bimg against multiple libvips versions. + +## v1.0.5 / 01-10-2016 + +- feat(#92): support Extend param with optional background. +- fix(#106): allow image area extraction without explicit x/y axis. +- feat(api): add Extend type with `libvips` enum alias. + +## v1.0.4 / 29-09-2016 + +- fix(#111): safe check of magick image type support. + +## v1.0.3 / 28-09-2016 + +- fix(#95): better image type inference and support check. +- fix(background): pass proper background RGB color for PNG image conversion. +- feat(types): validate supported image types by current `libvips` compilation. +- feat(types): consistent SVG image checking. +- feat(api): add public functions `VipsIsTypeSupported()`, `IsImageTypeSupportedByVips()` and `IsSVGImage()`. + +## v1.0.2 / 27-09-2016 + +- feat(#95): support GIF, SVG and PDF formats. +- fix(#108): auto-width and height calculations now round instead of floor. + +## v1.0.1 / 22-06-2016 + +- fix(#90): Do not not dereference the original image a second time. + +## v1.0.0 / 21-04-2016 + +- refactor(api): breaking changes: normalize public members to follow Go naming idioms. +- feat(version): bump to major version. API contract won't be compromised in `v1`. +- feat(docs): add missing inline godoc documentation. diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/LICENSE b/vendor/src/gopkg.in/h2non/bimg.v1/LICENSE new file mode 100644 index 00000000..b28d546c --- /dev/null +++ b/vendor/src/gopkg.in/h2non/bimg.v1/LICENSE @@ -0,0 +1,24 @@ +The MIT License + +Copyright (c) Tomas Aparicio and contributors + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/README.md b/vendor/src/gopkg.in/h2non/bimg.v1/README.md new file mode 100644 index 00000000..022f6540 --- /dev/null +++ b/vendor/src/gopkg.in/h2non/bimg.v1/README.md @@ -0,0 +1,347 @@ +# 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) + +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). + +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. + +bimg is able to output images as JPEG, PNG and WEBP formats, including transparent conversion across them. + +bimg uses internally libvips, a powerful library written in C for image processing which requires a [low memory footprint](http://www.vips.ecs.soton.ac.uk/index.php?title=Speed_and_Memory_Use) +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. + +If you're looking for an HTTP based image processing solution, see [imaginary](https://github.com/h2non/imaginary). + +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. + +**v1 notice**: `bimg` introduces some minor breaking changes in `v1` release. +If you're using `gopkg.in`, you can still rely in the `v0` without worrying about API breaking changes. + +`bimg` is currently maintained by [Kirill Danshin](https://github.com/kirillDanshin). + +## Contents + +- [Supported image operations](#supported-image-operations) +- [Prerequisites](#prerequisites) +- [Installation](#installation) +- [Performance](#performance) +- [Benchmark](#benchmark) +- [Examples](#examples) +- [Debugging](#debugging) +- [API](#api) +- [Authors](#authors) +- [Credits](#credits) + +## Supported image operations + +- Resize +- Enlarge +- Crop (including smart crop support) +- Rotate (with auto-rotate based on EXIF orientation) +- Flip (with auto-flip based on EXIF metadata) +- Flop +- Zoom +- Thumbnail +- Extract area +- Watermark (text only) +- Gaussian blur effect +- Custom output color space (RGB, grayscale...) +- Format conversion (with additional quality/compression settings) +- EXIF metadata (size, alpha channel, profile, orientation...) + +## Prerequisites + +- [libvips](https://github.com/jcupitt/libvips) 7.42+ or 8+ (8.4+ recommended) +- C compatible compiler such as gcc 4.6+ or clang 3.0+ +- Go 1.3+ + +**Note**: `libvips` v8.3+ is required for GIF, PDF and SVG support. + +## Installation + +```bash +go get -u gopkg.in/h2non/bimg.v1 +``` + +### libvips + +Run the following script as `sudo` (supports OSX, Debian/Ubuntu, Redhat, Fedora, Amazon Linux): +```bash +curl -s https://raw.githubusercontent.com/h2non/bimg/master/preinstall.sh | sudo bash - +``` + +If you wanna take the advantage of [OpenSlide](http://openslide.org/), simply add `--with-openslide` to enable it: +```bash +curl -s https://raw.githubusercontent.com/h2non/bimg/master/preinstall.sh | sudo bash -s --with-openslide +``` + +The [install script](https://github.com/h2non/bimg/blob/master/preinstall.sh) requires `curl` and `pkg-config`. + +## Performance + +libvips is probably the faster open source solution for image processing. +Here you can see some performance test comparisons for multiple scenarios: + +- [libvips speed and memory usage](http://www.vips.ecs.soton.ac.uk/index.php?title=Speed_and_Memory_Use) + +## Benchmark + +Tested using Go 1.5.1 and libvips-7.42.3 in OSX i7 2.7Ghz +``` +BenchmarkRotateJpeg-8 20 64686945 ns/op +BenchmarkResizeLargeJpeg-8 20 63390416 ns/op +BenchmarkResizePng-8 100 18147294 ns/op +BenchmarkResizeWebP-8 100 20836741 ns/op +BenchmarkConvertToJpeg-8 100 12831812 ns/op +BenchmarkConvertToPng-8 10 128901422 ns/op +BenchmarkConvertToWebp-8 10 204027990 ns/op +BenchmarkCropJpeg-8 30 59068572 ns/op +BenchmarkCropPng-8 10 117303259 ns/op +BenchmarkCropWebP-8 10 107060659 ns/op +BenchmarkExtractJpeg-8 50 30708919 ns/op +BenchmarkExtractPng-8 3000 595546 ns/op +BenchmarkExtractWebp-8 3000 386379 ns/op +BenchmarkZoomJpeg-8 10 160005424 ns/op +BenchmarkZoomPng-8 30 44561047 ns/op +BenchmarkZoomWebp-8 10 126732678 ns/op +BenchmarkWatermarkJpeg-8 20 79006133 ns/op +BenchmarkWatermarPng-8 200 8197291 ns/op +BenchmarkWatermarWebp-8 30 49360369 ns/op +``` + +## Examples + +```go +import ( + "fmt" + "os" + "gopkg.in/h2non/bimg.v1" +) +``` + +#### Resize + +```go +buffer, err := bimg.Read("image.jpg") +if err != nil { + fmt.Fprintln(os.Stderr, err) +} + +newImage, err := bimg.NewImage(buffer).Resize(800, 600) +if err != nil { + fmt.Fprintln(os.Stderr, err) +} + +size, err := bimg.NewImage(newImage).Size() +if size.Width == 400 && size.Height == 300 { + fmt.Println("The image size is valid") +} + +bimg.Write("new.jpg", newImage) +``` + +#### Rotate + +```go +buffer, err := bimg.Read("image.jpg") +if err != nil { + fmt.Fprintln(os.Stderr, err) +} + +newImage, err := bimg.NewImage(buffer).Rotate(90) +if err != nil { + fmt.Fprintln(os.Stderr, err) +} + +bimg.Write("new.jpg", newImage) +``` + +#### Convert + +```go +buffer, err := bimg.Read("image.jpg") +if err != nil { + fmt.Fprintln(os.Stderr, err) +} + +newImage, err := bimg.NewImage(buffer).Convert(bimg.PNG) +if err != nil { + fmt.Fprintln(os.Stderr, err) +} + +if bimg.NewImage(newImage).Type() == "png" { + fmt.Fprintln(os.Stderr, "The image was converted into png") +} +``` + +#### Force resize + +Force resize operation without perserving the aspect ratio: + +```go +buffer, err := bimg.Read("image.jpg") +if err != nil { + fmt.Fprintln(os.Stderr, err) +} + +newImage, err := bimg.NewImage(buffer).ForceResize(1000, 500) +if err != nil { + fmt.Fprintln(os.Stderr, err) +} + +size := bimg.Size(newImage) +if size.Width != 1000 || size.Height != 500 { + fmt.Fprintln(os.Stderr, "Incorrect image size") +} +``` + +#### Custom colour space (black & white) + +```go +buffer, err := bimg.Read("image.jpg") +if err != nil { + fmt.Fprintln(os.Stderr, err) +} + +newImage, err := bimg.NewImage(buffer).Colourspace(bimg.INTERPRETATION_B_W) +if err != nil { + fmt.Fprintln(os.Stderr, err) +} + +colourSpace, _ := bimg.ImageInterpretation(newImage) +if colourSpace != bimg.INTERPRETATION_B_W { + fmt.Fprintln(os.Stderr, "Invalid colour space") +} +``` + +#### Custom options + +See [Options](https://godoc.org/github.com/h2non/bimg#Options) struct to discover all the available fields + +```go +options := bimg.Options{ + Width: 800, + Height: 600, + Crop: true, + Quality: 95, + Rotate: 180, + Interlace: true, +} + +buffer, err := bimg.Read("image.jpg") +if err != nil { + fmt.Fprintln(os.Stderr, err) +} + +newImage, err := bimg.NewImage(buffer).Process(options) +if err != nil { + fmt.Fprintln(os.Stderr, err) +} + +bimg.Write("new.jpg", newImage) +``` + +#### Watermark + +```go +buffer, err := bimg.Read("image.jpg") +if err != nil { + fmt.Fprintln(os.Stderr, err) +} + +watermark := bimg.Watermark{ + Text: "Chuck Norris (c) 2315", + Opacity: 0.25, + Width: 200, + DPI: 100, + Margin: 150, + Font: "sans bold 12", + Background: bimg.Color{255, 255, 255}, +} + +newImage, err := bimg.NewImage(buffer).Watermark(watermark) +if err != nil { + fmt.Fprintln(os.Stderr, err) +} + +bimg.Write("new.jpg", newImage) +``` + +#### Fluent interface + +```go +buffer, err := bimg.Read("image.jpg") +if err != nil { + fmt.Fprintln(os.Stderr, err) +} + +image := bimg.NewImage(buffer) + +// first crop image +_, err := image.CropByWidth(300) +if err != nil { + fmt.Fprintln(os.Stderr, err) +} + +// then flip it +newImage, err := image.Flip() +if err != nil { + fmt.Fprintln(os.Stderr, err) +} + +// save the cropped and flipped image +bimg.Write("new.jpg", newImage) +``` + +## Debugging + +Run the process passing the `DEBUG` environment variable +``` +DEBUG=bimg ./app +``` + +Enable libvips traces (note that a lot of data will be written in stdout): +``` +VIPS_TRACE=1 ./app +``` + +You can also dump a core on failure, as [John Cuppit](https://github.com/jcupitt) said: +```c +g_log_set_always_fatal( + G_LOG_FLAG_RECURSION | + G_LOG_FLAG_FATAL | + G_LOG_LEVEL_ERROR | + G_LOG_LEVEL_CRITICAL | + G_LOG_LEVEL_WARNING ); +``` + +Or set the G_DEBUG environment variable: +``` +export G_DEBUG=fatal-warnings,fatal-criticals +``` + +## API + +See [godoc reference](https://godoc.org/github.com/h2non/bimg) for detailed API documentation. + +## Authors + +- [Tomás Aparicio](https://github.com/h2non) - Original author and architect. +- [Kirill Danshin](https://github.com/kirillDanshin) - Maintainer since April 2017. + +## Credits + +People who recurrently contributed to improve `bimg` in some way. + +- [John Cupitt](https://github.com/jcupitt) +- [Yoan Blanc](https://github.com/greut) +- [Christophe Eblé](https://github.com/chreble) +- [Brant Fitzsimmons](https://github.com/bfitzsimmons) +- [Thomas Meson](https://github.com/zllak) + +Thank you! + +## License + +MIT - Tomas Aparicio + +[![views](https://sourcegraph.com/api/repos/github.com/h2non/bimg/.counters/views.svg)](https://sourcegraph.com/github.com/h2non/bimg) diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/file.go b/vendor/src/gopkg.in/h2non/bimg.v1/file.go new file mode 100644 index 00000000..0cbf82aa --- /dev/null +++ b/vendor/src/gopkg.in/h2non/bimg.v1/file.go @@ -0,0 +1,15 @@ +package bimg + +import "io/ioutil" + +// Read reads all the content of the given file path +// and returns it as byte buffer. +func Read(path string) ([]byte, error) { + return ioutil.ReadFile(path) +} + +// Write writes the given byte buffer into disk +// to the given file path. +func Write(path string, buf []byte) error { + return ioutil.WriteFile(path, buf, 0644) +} diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/file_test.go b/vendor/src/gopkg.in/h2non/bimg.v1/file_test.go new file mode 100644 index 00000000..2144669a --- /dev/null +++ b/vendor/src/gopkg.in/h2non/bimg.v1/file_test.go @@ -0,0 +1,38 @@ +package bimg + +import ( + "testing" +) + +func TestRead(t *testing.T) { + buf, err := Read("fixtures/test.jpg") + + if err != nil { + t.Errorf("Cannot read the image: %#v", err) + } + + if len(buf) == 0 { + t.Fatal("Empty buffer") + } + + if DetermineImageType(buf) != JPEG { + t.Fatal("Image is not jpeg") + } +} + +func TestWrite(t *testing.T) { + buf, err := Read("fixtures/test.jpg") + + if err != nil { + t.Errorf("Cannot read the image: %#v", err) + } + + if len(buf) == 0 { + t.Fatal("Empty buffer") + } + + err = Write("fixtures/test_write_out.jpg", buf) + if err != nil { + t.Fatalf("Cannot write the file: %#v", err) + } +} diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/corrupt.jpg b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/corrupt.jpg new file mode 100644 index 00000000..e5992220 Binary files /dev/null and b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/corrupt.jpg differ diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/northern_cardinal_bird.jpg b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/northern_cardinal_bird.jpg new file mode 100644 index 00000000..1bf53853 Binary files /dev/null and b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/northern_cardinal_bird.jpg differ diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test.gif b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test.gif new file mode 100644 index 00000000..7bf290ac Binary files /dev/null and b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test.gif differ diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test.jp2 b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test.jp2 new file mode 100644 index 00000000..940778ff Binary files /dev/null and b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test.jp2 differ diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test.jpg b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test.jpg new file mode 100644 index 00000000..f17d2f18 Binary files /dev/null and b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test.jpg differ diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test.pdf b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test.pdf new file mode 100644 index 00000000..c14cc561 Binary files /dev/null and b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test.pdf differ diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test.png b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test.png new file mode 100644 index 00000000..d2f05912 Binary files /dev/null and b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test.png differ diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test.svg b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test.svg new file mode 100644 index 00000000..679edec2 --- /dev/null +++ b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test.svg @@ -0,0 +1,725 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test.webp b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test.webp new file mode 100644 index 00000000..122741b6 Binary files /dev/null and b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test.webp differ diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test_gif.jpg b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test_gif.jpg new file mode 100644 index 00000000..e69de29b diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test_icc_prophoto.jpg b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test_icc_prophoto.jpg new file mode 100644 index 00000000..ebf7f02b Binary files /dev/null and b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test_icc_prophoto.jpg differ diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test_issue.jpg b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test_issue.jpg new file mode 100644 index 00000000..8348e387 Binary files /dev/null and b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test_issue.jpg differ diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test_pdf.jpg b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test_pdf.jpg new file mode 100644 index 00000000..e69de29b diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test_square.jpg b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test_square.jpg new file mode 100644 index 00000000..c69aab42 Binary files /dev/null and b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test_square.jpg differ diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test_svg.jpg b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/test_svg.jpg new file mode 100644 index 00000000..e69de29b diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/transparent.png b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/transparent.png new file mode 100644 index 00000000..c82d0151 Binary files /dev/null and b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/transparent.png differ diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/vertical.jpg b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/vertical.jpg new file mode 100644 index 00000000..3e12e65e Binary files /dev/null and b/vendor/src/gopkg.in/h2non/bimg.v1/fixtures/vertical.jpg differ diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/image.go b/vendor/src/gopkg.in/h2non/bimg.v1/image.go new file mode 100644 index 00000000..efaffcb7 --- /dev/null +++ b/vendor/src/gopkg.in/h2non/bimg.v1/image.go @@ -0,0 +1,223 @@ +package bimg + +// Image provides a simple method DSL to transform a given image as byte buffer. +type Image struct { + buffer []byte +} + +// NewImage creates a new Image struct with method DSL. +func NewImage(buf []byte) *Image { + return &Image{buf} +} + +// Resize resizes the image to fixed width and height. +func (i *Image) Resize(width, height int) ([]byte, error) { + options := Options{ + Width: width, + Height: height, + Embed: true, + } + return i.Process(options) +} + +// ForceResize resizes with custom size (aspect ratio won't be maintained). +func (i *Image) ForceResize(width, height int) ([]byte, error) { + options := Options{ + Width: width, + Height: height, + Force: true, + } + return i.Process(options) +} + +// ResizeAndCrop resizes the image to fixed width and height with additional crop transformation. +func (i *Image) ResizeAndCrop(width, height int) ([]byte, error) { + options := Options{ + Width: width, + Height: height, + Embed: true, + Crop: true, + } + return i.Process(options) +} + +// SmartCrop produces a thumbnail aiming at focus on the interesting part. +func (i *Image) SmartCrop(width, height int) ([]byte, error) { + options := Options{ + Width: width, + Height: height, + Crop: true, + Gravity: GravitySmart, + } + return i.Process(options) +} + +// Extract area from the by X/Y axis in the current image. +func (i *Image) Extract(top, left, width, height int) ([]byte, error) { + options := Options{ + Top: top, + Left: left, + AreaWidth: width, + AreaHeight: height, + } + + if top == 0 && left == 0 { + options.Top = -1 + } + + return i.Process(options) +} + +// Enlarge enlarges the image by width and height. Aspect ratio is maintained. +func (i *Image) Enlarge(width, height int) ([]byte, error) { + options := Options{ + Width: width, + Height: height, + Enlarge: true, + } + return i.Process(options) +} + +// EnlargeAndCrop enlarges the image by width and height with additional crop transformation. +func (i *Image) EnlargeAndCrop(width, height int) ([]byte, error) { + options := Options{ + Width: width, + Height: height, + Enlarge: true, + Crop: true, + } + return i.Process(options) +} + +// Crop crops the image to the exact size specified. +func (i *Image) Crop(width, height int, gravity Gravity) ([]byte, error) { + options := Options{ + Width: width, + Height: height, + Gravity: gravity, + Crop: true, + } + return i.Process(options) +} + +// CropByWidth crops an image by width only param (auto height). +func (i *Image) CropByWidth(width int) ([]byte, error) { + options := Options{ + Width: width, + Crop: true, + } + return i.Process(options) +} + +// CropByHeight crops an image by height (auto width). +func (i *Image) CropByHeight(height int) ([]byte, error) { + options := Options{ + Height: height, + Crop: true, + } + return i.Process(options) +} + +// Thumbnail creates a thumbnail of the image by the a given width by aspect ratio 4:4. +func (i *Image) Thumbnail(pixels int) ([]byte, error) { + options := Options{ + Width: pixels, + Height: pixels, + Crop: true, + Quality: 95, + } + return i.Process(options) +} + +// Watermark adds text as watermark on the given image. +func (i *Image) Watermark(w Watermark) ([]byte, error) { + options := Options{Watermark: w} + return i.Process(options) +} + +// WatermarkImage adds image as watermark on the given image. +func (i *Image) WatermarkImage(w WatermarkImage) ([]byte, error) { + options := Options{WatermarkImage: w} + return i.Process(options) +} + +// Zoom zooms the image by the given factor. +// You should probably call Extract() before. +func (i *Image) Zoom(factor int) ([]byte, error) { + options := Options{Zoom: factor} + return i.Process(options) +} + +// Rotate rotates the image by given angle degrees (0, 90, 180 or 270). +func (i *Image) Rotate(a Angle) ([]byte, error) { + options := Options{Rotate: a} + return i.Process(options) +} + +// Flip flips the image about the vertical Y axis. +func (i *Image) Flip() ([]byte, error) { + options := Options{Flip: true} + return i.Process(options) +} + +// Flop flops the image about the horizontal X axis. +func (i *Image) Flop() ([]byte, error) { + options := Options{Flop: true} + return i.Process(options) +} + +// Convert converts image to another format. +func (i *Image) Convert(t ImageType) ([]byte, error) { + options := Options{Type: t} + return i.Process(options) +} + +// Colourspace performs a color space conversion bsaed on the given interpretation. +func (i *Image) Colourspace(c Interpretation) ([]byte, error) { + options := Options{Interpretation: c} + return i.Process(options) +} + +// Process processes the image based on the given transformation options, +// talking with libvips bindings accordingly and returning the resultant +// image buffer. +func (i *Image) Process(o Options) ([]byte, error) { + image, err := Resize(i.buffer, o) + if err != nil { + return nil, err + } + i.buffer = image + return image, nil +} + +// Metadata returns the image metadata (size, alpha channel, profile, EXIF rotation). +func (i *Image) Metadata() (ImageMetadata, error) { + return Metadata(i.buffer) +} + +// Interpretation gets the image interpretation type. +// See: http://www.vips.ecs.soton.ac.uk/supported/current/doc/html/libvips/VipsImage.html#VipsInterpretation +func (i *Image) Interpretation() (Interpretation, error) { + return ImageInterpretation(i.buffer) +} + +// ColourspaceIsSupported checks if the current image +// color space is supported. +func (i *Image) ColourspaceIsSupported() (bool, error) { + return ColourspaceIsSupported(i.buffer) +} + +// Type returns the image type format (jpeg, png, webp, tiff). +func (i *Image) Type() string { + return DetermineImageTypeName(i.buffer) +} + +// Size returns the image size as form of width and height pixels. +func (i *Image) Size() (ImageSize, error) { + return Size(i.buffer) +} + +// Image returns the current resultant image image buffer. +func (i *Image) Image() []byte { + return i.buffer +} diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/image_test.go b/vendor/src/gopkg.in/h2non/bimg.v1/image_test.go new file mode 100644 index 00000000..96fc4ad9 --- /dev/null +++ b/vendor/src/gopkg.in/h2non/bimg.v1/image_test.go @@ -0,0 +1,496 @@ +package bimg + +import ( + "fmt" + "path" + "testing" +) + +func TestImageResize(t *testing.T) { + buf, err := initImage("test.jpg").Resize(300, 240) + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + + err = assertSize(buf, 300, 240) + if err != nil { + t.Error(err) + } + + Write("fixtures/test_resize_out.jpg", buf) +} + +func TestImageGifResize(t *testing.T) { + _, err := initImage("test.gif").Resize(300, 240) + if err == nil { + t.Errorf("GIF shouldn't be saved within VIPS") + } +} + +func TestImagePdfResize(t *testing.T) { + _, err := initImage("test.pdf").Resize(300, 240) + if err == nil { + t.Errorf("PDF cannot be saved within VIPS") + } +} + +func TestImageSvgResize(t *testing.T) { + _, err := initImage("test.svg").Resize(300, 240) + if err == nil { + t.Errorf("SVG cannot be saved within VIPS") + } +} + +func TestImageGifToJpeg(t *testing.T) { + if VipsMajorVersion >= 8 && VipsMinorVersion > 2 { + i := initImage("test.gif") + options := Options{ + Type: JPEG, + } + buf, err := i.Process(options) + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + + Write("fixtures/test_gif.jpg", buf) + } +} + +func TestImagePdfToJpeg(t *testing.T) { + if VipsMajorVersion >= 8 && VipsMinorVersion > 2 { + i := initImage("test.pdf") + options := Options{ + Type: JPEG, + } + buf, err := i.Process(options) + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + + Write("fixtures/test_pdf.jpg", buf) + } +} + +func TestImageSvgToJpeg(t *testing.T) { + if VipsMajorVersion >= 8 && VipsMinorVersion > 2 { + i := initImage("test.svg") + options := Options{ + Type: JPEG, + } + buf, err := i.Process(options) + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + + Write("fixtures/test_svg.jpg", buf) + } +} + +func TestImageResizeAndCrop(t *testing.T) { + buf, err := initImage("test.jpg").ResizeAndCrop(300, 200) + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + + err = assertSize(buf, 300, 200) + if err != nil { + t.Error(err) + } + + Write("fixtures/test_resize_crop_out.jpg", buf) +} + +func TestImageExtract(t *testing.T) { + buf, err := initImage("test.jpg").Extract(100, 100, 300, 200) + if err != nil { + t.Errorf("Cannot process the image: %s", err) + } + + err = assertSize(buf, 300, 200) + if err != nil { + t.Error(err) + } + + Write("fixtures/test_extract_out.jpg", buf) +} + +func TestImageExtractZero(t *testing.T) { + buf, err := initImage("test.jpg").Extract(0, 0, 300, 200) + if err != nil { + t.Errorf("Cannot process the image: %s", err) + } + + err = assertSize(buf, 300, 200) + if err != nil { + t.Error(err) + } + + Write("fixtures/test_extract_zero_out.jpg", buf) +} + +func TestImageEnlarge(t *testing.T) { + buf, err := initImage("test.png").Enlarge(500, 375) + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + + err = assertSize(buf, 500, 375) + if err != nil { + t.Error(err) + } + + Write("fixtures/test_enlarge_out.jpg", buf) +} + +func TestImageEnlargeAndCrop(t *testing.T) { + buf, err := initImage("test.png").EnlargeAndCrop(800, 480) + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + + err = assertSize(buf, 800, 480) + if err != nil { + t.Error(err) + } + + Write("fixtures/test_enlarge_crop_out.jpg", buf) +} + +func TestImageCrop(t *testing.T) { + buf, err := initImage("test.jpg").Crop(800, 600, GravityNorth) + if err != nil { + t.Errorf("Cannot process the image: %s", err) + } + + err = assertSize(buf, 800, 600) + if err != nil { + t.Error(err) + } + + Write("fixtures/test_crop_out.jpg", buf) +} + +func TestImageCropByWidth(t *testing.T) { + buf, err := initImage("test.jpg").CropByWidth(600) + if err != nil { + t.Errorf("Cannot process the image: %s", err) + } + + err = assertSize(buf, 600, 1050) + if err != nil { + t.Error(err) + } + + Write("fixtures/test_crop_width_out.jpg", buf) +} + +func TestImageCropByHeight(t *testing.T) { + buf, err := initImage("test.jpg").CropByHeight(300) + if err != nil { + t.Errorf("Cannot process the image: %s", err) + } + + err = assertSize(buf, 1680, 300) + if err != nil { + t.Error(err) + } + + Write("fixtures/test_crop_height_out.jpg", buf) +} + +func TestImageThumbnail(t *testing.T) { + buf, err := initImage("test.jpg").Thumbnail(100) + if err != nil { + t.Errorf("Cannot process the image: %s", err) + } + + err = assertSize(buf, 100, 100) + if err != nil { + t.Error(err) + } + + Write("fixtures/test_thumbnail_out.jpg", buf) +} + +func TestImageWatermark(t *testing.T) { + image := initImage("test.jpg") + _, err := image.Crop(800, 600, GravityNorth) + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + + buf, err := image.Watermark(Watermark{ + Text: "Copy me if you can", + Opacity: 0.5, + Width: 200, + DPI: 100, + Background: Color{255, 255, 255}, + }) + if err != nil { + t.Error(err) + } + + err = assertSize(buf, 800, 600) + if err != nil { + t.Error(err) + } + + if DetermineImageType(buf) != JPEG { + t.Fatal("Image is not jpeg") + } + + Write("fixtures/test_watermark_text_out.jpg", buf) +} + +func TestImageWatermarkWithImage(t *testing.T) { + image := initImage("test.jpg") + watermark, _ := imageBuf("transparent.png") + + _, err := image.Crop(800, 600, GravityNorth) + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + + buf, err := image.WatermarkImage(WatermarkImage{Left: 100, Top: 100, Buf: watermark}) + + if err != nil { + t.Error(err) + } + + err = assertSize(buf, 800, 600) + if err != nil { + t.Error(err) + } + + if DetermineImageType(buf) != JPEG { + t.Fatal("Image is not jpeg") + } + + Write("fixtures/test_watermark_image_out.jpg", buf) +} + +func TestImageWatermarkNoReplicate(t *testing.T) { + image := initImage("test.jpg") + _, err := image.Crop(800, 600, GravityNorth) + if err != nil { + t.Errorf("Cannot process the image: %s", err) + } + + buf, err := image.Watermark(Watermark{ + Text: "Copy me if you can", + Opacity: 0.5, + Width: 200, + DPI: 100, + NoReplicate: true, + Background: Color{255, 255, 255}, + }) + if err != nil { + t.Error(err) + } + + err = assertSize(buf, 800, 600) + if err != nil { + t.Error(err) + } + + if DetermineImageType(buf) != JPEG { + t.Fatal("Image is not jpeg") + } + + Write("fixtures/test_watermark_replicate_out.jpg", buf) +} + +func TestImageZoom(t *testing.T) { + image := initImage("test.jpg") + + _, err := image.Extract(100, 100, 400, 300) + if err != nil { + t.Errorf("Cannot extract the image: %s", err) + } + + buf, err := image.Zoom(1) + if err != nil { + t.Errorf("Cannot process the image: %s", err) + } + + err = assertSize(buf, 800, 600) + if err != nil { + t.Error(err) + } + + Write("fixtures/test_zoom_out.jpg", buf) +} + +func TestImageFlip(t *testing.T) { + buf, err := initImage("test.jpg").Flip() + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + Write("fixtures/test_flip_out.jpg", buf) +} + +func TestImageFlop(t *testing.T) { + buf, err := initImage("test.jpg").Flop() + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + Write("fixtures/test_flop_out.jpg", buf) +} + +func TestImageRotate(t *testing.T) { + buf, err := initImage("test_flip_out.jpg").Rotate(90) + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + Write("fixtures/test_image_rotate_out.jpg", buf) +} + +func TestImageConvert(t *testing.T) { + buf, err := initImage("test.jpg").Convert(PNG) + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + Write("fixtures/test_image_convert_out.png", buf) +} + +func TestTransparentImageConvert(t *testing.T) { + image := initImage("transparent.png") + options := Options{ + Type: JPEG, + Background: Color{255, 255, 255}, + } + buf, err := image.Process(options) + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + Write("fixtures/test_transparent_image_convert_out.jpg", buf) +} + +func TestImageMetadata(t *testing.T) { + data, err := initImage("test.png").Metadata() + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + if data.Alpha != true { + t.Fatal("Invalid alpha channel") + } + if data.Size.Width != 400 { + t.Fatal("Invalid width size") + } + if data.Type != "png" { + t.Fatal("Invalid image type") + } +} + +func TestInterpretation(t *testing.T) { + interpretation, err := initImage("test.jpg").Interpretation() + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + if interpretation != InterpretationSRGB { + t.Errorf("Invalid interpretation: %d", interpretation) + } +} + +func TestImageColourspace(t *testing.T) { + tests := []struct { + file string + interpretation Interpretation + }{ + {"test.jpg", InterpretationSRGB}, + {"test.jpg", InterpretationBW}, + } + + for _, test := range tests { + buf, err := initImage(test.file).Colourspace(test.interpretation) + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + + interpretation, err := ImageInterpretation(buf) + if interpretation != test.interpretation { + t.Errorf("Invalid colourspace") + } + } +} + +func TestImageColourspaceIsSupported(t *testing.T) { + supported, err := initImage("test.jpg").ColourspaceIsSupported() + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + if supported != true { + t.Errorf("Non-supported colourspace") + } +} + +func TestFluentInterface(t *testing.T) { + image := initImage("test.jpg") + _, err := image.CropByWidth(300) + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + + _, err = image.Flip() + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + + _, err = image.Convert(PNG) + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + + data, _ := image.Metadata() + if data.Alpha != false { + t.Fatal("Invalid alpha channel") + } + if data.Size.Width != 300 { + t.Fatal("Invalid width size") + } + if data.Type != "png" { + t.Fatal("Invalid image type") + } + + Write("fixtures/test_image_fluent_out.png", image.Image()) +} + +func TestImageSmartCrop(t *testing.T) { + + if !(VipsMajorVersion >= 8 && VipsMinorVersion > 4) { + t.Skipf("Skipping this test, libvips doesn't meet version requirement %s > 8.4", VipsVersion) + } + + i := initImage("northern_cardinal_bird.jpg") + buf, err := i.SmartCrop(300, 300) + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + + err = assertSize(buf, 300, 300) + if err != nil { + t.Error(err) + } + + Write("fixtures/test_smart_crop.jpg", buf) +} + +func initImage(file string) *Image { + buf, _ := imageBuf(file) + return NewImage(buf) +} + +func imageBuf(file string) ([]byte, error) { + return Read(path.Join("fixtures", file)) +} + +func assertSize(buf []byte, width, height int) error { + size, err := NewImage(buf).Size() + if err != nil { + return err + } + if size.Width != width || size.Height != height { + return fmt.Errorf("Invalid image size: %dx%d", size.Width, size.Height) + } + return nil +} diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/metadata.go b/vendor/src/gopkg.in/h2non/bimg.v1/metadata.go new file mode 100644 index 00000000..77eac8cd --- /dev/null +++ b/vendor/src/gopkg.in/h2non/bimg.v1/metadata.go @@ -0,0 +1,77 @@ +package bimg + +/* +#cgo pkg-config: vips +#include "vips/vips.h" +*/ +import "C" + +// ImageSize represents the image width and height values +type ImageSize struct { + Width int + Height int +} + +// ImageMetadata represents the basic metadata fields +type ImageMetadata struct { + Orientation int + Channels int + Alpha bool + Profile bool + Type string + Space string + Colourspace string + Size ImageSize +} + +// Size returns the image size by width and height pixels. +func Size(buf []byte) (ImageSize, error) { + metadata, err := Metadata(buf) + if err != nil { + return ImageSize{}, err + } + + return ImageSize{ + Width: int(metadata.Size.Width), + Height: int(metadata.Size.Height), + }, nil +} + +// ColourspaceIsSupported checks if the image colourspace is supported by libvips. +func ColourspaceIsSupported(buf []byte) (bool, error) { + return vipsColourspaceIsSupportedBuffer(buf) +} + +// ImageInterpretation returns the image interpretation type. +// See: http://www.vips.ecs.soton.ac.uk/supported/current/doc/html/libvips/VipsImage.html#VipsInterpretation +func ImageInterpretation(buf []byte) (Interpretation, error) { + return vipsInterpretationBuffer(buf) +} + +// Metadata returns the image metadata (size, type, alpha channel, profile, EXIF orientation...). +func Metadata(buf []byte) (ImageMetadata, error) { + defer C.vips_thread_shutdown() + + image, imageType, err := vipsRead(buf) + if err != nil { + return ImageMetadata{}, err + } + defer C.g_object_unref(C.gpointer(image)) + + size := ImageSize{ + Width: int(image.Xsize), + Height: int(image.Ysize), + } + + metadata := ImageMetadata{ + Size: size, + Channels: int(image.Bands), + Orientation: vipsExifOrientation(image), + Alpha: vipsHasAlpha(image), + Profile: vipsHasProfile(image), + Space: vipsSpace(image), + Type: ImageTypeName(imageType), + } + + return metadata, nil +} diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/metadata_test.go b/vendor/src/gopkg.in/h2non/bimg.v1/metadata_test.go new file mode 100644 index 00000000..663ec564 --- /dev/null +++ b/vendor/src/gopkg.in/h2non/bimg.v1/metadata_test.go @@ -0,0 +1,124 @@ +package bimg + +import ( + "io/ioutil" + "os" + "path" + "testing" +) + +func TestSize(t *testing.T) { + files := []struct { + name string + width int + height int + }{ + {"test.jpg", 1680, 1050}, + {"test.png", 400, 300}, + {"test.webp", 550, 368}, + } + for _, file := range files { + size, err := Size(readFile(file.name)) + if err != nil { + t.Fatalf("Cannot read the image: %#v", err) + } + + if size.Width != file.width || size.Height != file.height { + t.Fatalf("Unexpected image size: %dx%d", size.Width, size.Height) + } + } +} + +func TestMetadata(t *testing.T) { + files := []struct { + name string + format string + orientation int + alpha bool + profile bool + space string + }{ + {"test.jpg", "jpeg", 0, false, false, "srgb"}, + {"test_icc_prophoto.jpg", "jpeg", 0, false, true, "srgb"}, + {"test.png", "png", 0, true, false, "srgb"}, + {"test.webp", "webp", 0, false, false, "srgb"}, + } + + for _, file := range files { + metadata, err := Metadata(readFile(file.name)) + if err != nil { + t.Fatalf("Cannot read the image: %s -> %s", file.name, err) + } + + if metadata.Type != file.format { + t.Fatalf("Unexpected image format: %s", file.format) + } + if metadata.Orientation != file.orientation { + t.Fatalf("Unexpected image orientation: %d != %d", metadata.Orientation, file.orientation) + } + if metadata.Alpha != file.alpha { + t.Fatalf("Unexpected image alpha: %t != %t", metadata.Alpha, file.alpha) + } + if metadata.Profile != file.profile { + t.Fatalf("Unexpected image profile: %t != %t", metadata.Profile, file.profile) + } + if metadata.Space != file.space { + t.Fatalf("Unexpected image profile: %t != %t", metadata.Profile, file.profile) + } + } +} + +func TestImageInterpretation(t *testing.T) { + files := []struct { + name string + interpretation Interpretation + }{ + {"test.jpg", InterpretationSRGB}, + {"test.png", InterpretationSRGB}, + {"test.webp", InterpretationSRGB}, + } + + for _, file := range files { + interpretation, err := ImageInterpretation(readFile(file.name)) + if err != nil { + t.Fatalf("Cannot read the image: %s -> %s", file.name, err) + } + if interpretation != file.interpretation { + t.Fatalf("Unexpected image interpretation") + } + } +} + +func TestColourspaceIsSupported(t *testing.T) { + files := []struct { + name string + }{ + {"test.jpg"}, + {"test.png"}, + {"test.webp"}, + } + + for _, file := range files { + supported, err := ColourspaceIsSupported(readFile(file.name)) + if err != nil { + t.Fatalf("Cannot read the image: %s -> %s", file.name, err) + } + if supported != true { + t.Fatalf("Unsupported image colourspace") + } + } + + supported, err := initImage("test.jpg").ColourspaceIsSupported() + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + if supported != true { + t.Errorf("Non-supported colourspace") + } +} + +func readFile(file string) []byte { + data, _ := os.Open(path.Join("fixtures", file)) + buf, _ := ioutil.ReadAll(data) + return buf +} diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/options.go b/vendor/src/gopkg.in/h2non/bimg.v1/options.go new file mode 100644 index 00000000..f6ebec2c --- /dev/null +++ b/vendor/src/gopkg.in/h2non/bimg.v1/options.go @@ -0,0 +1,218 @@ +package bimg + +/* +#cgo pkg-config: vips +#include "vips/vips.h" +*/ +import "C" + +const ( + // Quality defines the default JPEG quality to be used. + Quality = 80 + // MaxSize defines the maximum pixels width or height supported. + MaxSize = 16383 +) + +// Gravity represents the image gravity value. +type Gravity int + +const ( + // GravityCentre represents the centre value used for image gravity orientation. + GravityCentre Gravity = iota + // GravityNorth represents the north value used for image gravity orientation. + GravityNorth + // GravityEast represents the east value used for image gravity orientation. + GravityEast + // GravitySouth represents the south value used for image gravity orientation. + GravitySouth + // GravityWest represents the west value used for image gravity orientation. + GravityWest + // GravitySmart enables libvips Smart Crop algorithm for image gravity orientation. + GravitySmart +) + +// Interpolator represents the image interpolation value. +type Interpolator int + +const ( + // Bicubic interpolation value. + Bicubic Interpolator = iota + // Bilinear interpolation value. + Bilinear + // Nohalo interpolation value. + Nohalo +) + +var interpolations = map[Interpolator]string{ + Bicubic: "bicubic", + Bilinear: "bilinear", + Nohalo: "nohalo", +} + +func (i Interpolator) String() string { + return interpolations[i] +} + +// Angle represents the image rotation angle value. +type Angle int + +const ( + // D0 represents the rotation angle 0 degrees. + D0 Angle = 0 + // D45 represents the rotation angle 90 degrees. + D45 Angle = 45 + // D90 represents the rotation angle 90 degrees. + D90 Angle = 90 + // D135 represents the rotation angle 90 degrees. + D135 Angle = 135 + // D180 represents the rotation angle 180 degrees. + D180 Angle = 180 + // D235 represents the rotation angle 235 degrees. + D235 Angle = 235 + // D270 represents the rotation angle 270 degrees. + D270 Angle = 270 + // D315 represents the rotation angle 180 degrees. + D315 Angle = 315 +) + +// Direction represents the image direction value. +type Direction int + +const ( + // Horizontal represents the orizontal image direction value. + Horizontal Direction = C.VIPS_DIRECTION_HORIZONTAL + // Vertical represents the vertical image direction value. + Vertical Direction = C.VIPS_DIRECTION_VERTICAL +) + +// Interpretation represents the image interpretation type. +// See: http://www.vips.ecs.soton.ac.uk/supported/current/doc/html/libvips/VipsImage.html#VipsInterpretation +type Interpretation int + +const ( + // InterpretationError points to the libvips interpretation error type. + InterpretationError Interpretation = C.VIPS_INTERPRETATION_ERROR + // InterpretationMultiband points to its libvips interpretation equivalent type. + InterpretationMultiband Interpretation = C.VIPS_INTERPRETATION_MULTIBAND + // InterpretationBW points to its libvips interpretation equivalent type. + InterpretationBW Interpretation = C.VIPS_INTERPRETATION_B_W + // InterpretationCMYK points to its libvips interpretation equivalent type. + InterpretationCMYK Interpretation = C.VIPS_INTERPRETATION_CMYK + // InterpretationRGB points to its libvips interpretation equivalent type. + InterpretationRGB Interpretation = C.VIPS_INTERPRETATION_RGB + // InterpretationSRGB points to its libvips interpretation equivalent type. + InterpretationSRGB Interpretation = C.VIPS_INTERPRETATION_sRGB + // InterpretationRGB16 points to its libvips interpretation equivalent type. + InterpretationRGB16 Interpretation = C.VIPS_INTERPRETATION_RGB16 + // InterpretationGREY16 points to its libvips interpretation equivalent type. + InterpretationGREY16 Interpretation = C.VIPS_INTERPRETATION_GREY16 + // InterpretationScRGB points to its libvips interpretation equivalent type. + InterpretationScRGB Interpretation = C.VIPS_INTERPRETATION_scRGB + // InterpretationLAB points to its libvips interpretation equivalent type. + InterpretationLAB Interpretation = C.VIPS_INTERPRETATION_LAB + // InterpretationXYZ points to its libvips interpretation equivalent type. + InterpretationXYZ Interpretation = C.VIPS_INTERPRETATION_XYZ +) + +// Extend represents the image extend mode, used when the edges +// of an image are extended, you can specify how you want the extension done. +// See: http://www.vips.ecs.soton.ac.uk/supported/8.4/doc/html/libvips/libvips-conversion.html#VIPS-EXTEND-BACKGROUND:CAPS +type Extend int + +const ( + // ExtendBlack extend with black (all 0) pixels mode. + ExtendBlack Extend = C.VIPS_EXTEND_BLACK + // ExtendCopy copy the image edges. + ExtendCopy Extend = C.VIPS_EXTEND_COPY + // ExtendRepeat repeat the whole image. + ExtendRepeat Extend = C.VIPS_EXTEND_REPEAT + // ExtendMirror mirror the whole image. + ExtendMirror Extend = C.VIPS_EXTEND_MIRROR + // ExtendWhite extend with white (all bits set) pixels. + ExtendWhite Extend = C.VIPS_EXTEND_WHITE + // ExtendBackground with colour from the background property. + ExtendBackground Extend = C.VIPS_EXTEND_BACKGROUND + // ExtendLast extend with last pixel. + ExtendLast Extend = C.VIPS_EXTEND_LAST +) + +// WatermarkFont defines the default watermark font to be used. +var WatermarkFont = "sans 10" + +// Color represents a traditional RGB color scheme. +type Color struct { + R, G, B uint8 +} + +// ColorBlack is a shortcut to black RGB color representation. +var ColorBlack = Color{0, 0, 0} + +// Watermark represents the text-based watermark supported options. +type Watermark struct { + Width int + DPI int + Margin int + Opacity float32 + NoReplicate bool + Text string + Font string + Background Color +} + +// WatermarkImage represents the image-based watermark supported options. +type WatermarkImage struct { + Left int + Top int + Buf []byte + Opacity float32 +} + +// GaussianBlur represents the gaussian image transformation values. +type GaussianBlur struct { + Sigma float64 + MinAmpl float64 +} + +// Sharpen represents the image sharp transformation options. +type Sharpen struct { + Radius int + X1 float64 + Y2 float64 + Y3 float64 + M1 float64 + M2 float64 +} + +// Options represents the supported image transformation options. +type Options struct { + Height int + Width int + AreaHeight int + AreaWidth int + Top int + Left int + Quality int + Compression int + Zoom int + Crop bool + SmartCrop bool // Deprecated + Enlarge bool + Embed bool + Flip bool + Flop bool + Force bool + NoAutoRotate bool + NoProfile bool + Interlace bool + Extend Extend + Rotate Angle + Background Color + Gravity Gravity + Watermark Watermark + WatermarkImage WatermarkImage + Type ImageType + Interpolator Interpolator + Interpretation Interpretation + GaussianBlur GaussianBlur + Sharpen Sharpen +} diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/preinstall.sh b/vendor/src/gopkg.in/h2non/bimg.v1/preinstall.sh new file mode 100644 index 00000000..47fa24c5 --- /dev/null +++ b/vendor/src/gopkg.in/h2non/bimg.v1/preinstall.sh @@ -0,0 +1,302 @@ +#!/bin/bash + +vips_version_minimum=8.4.2 +vips_version_latest_major_minor=8.4 +vips_version_latest_patch=2 + +openslide_version_minimum=3.4.0 +openslide_version_latest_major_minor=3.4 +openslide_version_latest_patch=1 + +install_libvips_from_source() { + echo "Compiling libvips $vips_version_latest_major_minor.$vips_version_latest_patch from source" + curl -O http://www.vips.ecs.soton.ac.uk/supported/$vips_version_latest_major_minor/vips-$vips_version_latest_major_minor.$vips_version_latest_patch.tar.gz + tar zvxf vips-$vips_version_latest_major_minor.$vips_version_latest_patch.tar.gz + cd vips-$vips_version_latest_major_minor.$vips_version_latest_patch + 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 + make + make install + cd .. + rm -rf vips-$vips_version_latest_major_minor.$vips_version_latest_patch + rm vips-$vips_version_latest_major_minor.$vips_version_latest_patch.tar.gz + ldconfig + echo "Installed libvips $(PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig pkg-config --modversion vips)" +} + +install_libopenslide_from_source() { + echo "Compiling openslide $openslide_version_latest_major_minor.$openslide_version_latest_patch from source" + 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 + tar xzvf openslide-$openslide_version_latest_major_minor.$openslide_version_latest_patch.tar.gz + cd openslide-$openslide_version_latest_major_minor.$openslide_version_latest_patch + PKG_CONFIG_PATH=$pkg_config_path ./configure $1 + make + make install + cd .. + rm -rf openslide-$openslide_version_latest_major_minor.$openslide_version_latest_patch + rm openslide-$openslide_version_latest_major_minor.$openslide_version_latest_patch.tar.gz + ldconfig + echo "Installed libopenslide $openslide_version_latest_major_minor.$openslide_version_latest_patch" +} + +sorry() { + echo "Sorry, I don't yet know how to install lib$1 on $2" + exit 1 +} + +pkg_config_path="$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig" + +check_if_library_exists() { + PKG_CONFIG_PATH=$pkg_config_path pkg-config --exists $1 + if [ $? -eq 0 ]; then + version_found=$(PKG_CONFIG_PATH=$pkg_config_path pkg-config --modversion $1) + PKG_CONFIG_PATH=$pkg_config_path pkg-config --atleast-version=$2 $1 + if [ $? -eq 0 ]; then + # Found suitable version of libvips + echo "Found lib$1 $version_found" + return 1 + fi + echo "Found lib$1 $version_found but require $2" + else + echo "Could not find lib$1 using a PKG_CONFIG_PATH of '$pkg_config_path'" + fi + return 0 +} + +enable_openslide=0 +# Is libvips already installed, and is it at least the minimum required version? +if [ $# -eq 1 ]; then + if [ "$1" = "--with-openslide" ]; then + echo "Installing vips with openslide support" + enable_openslide=1 + else + echo "Sorry, $1 is not supported. Did you mean --with-openslide?" + exit 1 + fi +fi + +if ! type pkg-config >/dev/null; then + sorry "vips" "a system without pkg-config" +fi + +openslide_exists=0 +if [ $enable_openslide -eq 1 ]; then + check_if_library_exists "openslide" "$openslide_version_minimum" + openslide_exists=$? +fi + +check_if_library_exists "vips" "$vips_version_minimum" +vips_exists=$? +if [ $vips_exists -eq 1 ] && [ $enable_openslide -eq 1 ]; then + if [ $openslide_exists -eq 1 ]; then + # Check if vips compiled with openslide support + vips_with_openslide=`vips list classes | grep -i opensli` + if [ -z $vips_with_openslide ]; then + echo "Vips compiled without openslide support." + else + exit 0 + fi + fi +elif [ $vips_exists -eq 1 ] && [ $enable_openslide -eq 0 ]; then + exit 0 +fi + +# Verify root/sudo access +if [ "$(id -u)" -ne "0" ]; then + echo "Sorry, I need root/sudo access to continue" + exit 1 +fi + +# Deprecation warning +if [ "$(arch)" == "x86_64" ]; then + echo "This script is no longer required on most 64-bit Linux systems when using sharp v0.12.0+" +fi + +# OS-specific installations of libopenslide follows +# Either openslide does not exist, or vips is installed without openslide support +if [ $enable_openslide -eq 1 ] && [ -z $vips_with_openslide ] && [ $openslide_exists -eq 0 ]; then + if [ -f /etc/debian_version ]; then + # Debian Linux + DISTRO=$(lsb_release -c -s) + echo "Detected Debian Linux '$DISTRO'" + case "$DISTRO" in + jessie|vivid|wily|xenial) + # Debian 8, Ubuntu 15 + echo "Installing libopenslide via apt-get" + apt-get install -y libopenslide-dev + ;; + trusty|utopic|qiana|rebecca|rafaela|freya|rosa|sarah|serena) + # Ubuntu 14, Mint 17+ + echo "Installing libopenslide dependencies via apt-get" + 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 + install_libopenslide_from_source + ;; + precise|wheezy|maya) + # Debian 7, Ubuntu 12.04, Mint 13 + echo "Installing libopenslide dependencies via apt-get" + 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 + install_libopenslide_from_source + ;; + *) + # Unsupported Debian-based OS + sorry "openslide" "Debian-based $DISTRO" + ;; + esac + elif [ -f /etc/redhat-release ]; then + # Red Hat Linux + RELEASE=$(cat /etc/redhat-release) + echo "Detected Red Hat Linux '$RELEASE'" + case $RELEASE in + "Red Hat Enterprise Linux release 7."*|"CentOS Linux release 7."*|"Scientific Linux release 7."*) + # RHEL/CentOS 7 + echo "Installing libopenslide dependencies via yum" + yum groupinstall -y "Development Tools" + 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 + install_libopenslide_from_source "--prefix=/usr" + ;; + "Red Hat Enterprise Linux release 6."*|"CentOS release 6."*|"Scientific Linux release 6."*) + # RHEL/CentOS 6 + echo "Installing libopenslide dependencies via yum" + yum groupinstall -y "Development Tools" + 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 + install_libopenslide_from_source "--prefix=/usr" + ;; + "Fedora release 21 "*|"Fedora release 22 "*) + # Fedora 21, 22 + echo "Installing libopenslide via yum" + yum install -y openslide-devel + ;; + *) + # Unsupported RHEL-based OS + sorry "openslide" "$RELEASE" + ;; + esac + elif [ -f /etc/os-release ]; then + RELEASE=$(cat /etc/os-release | grep VERSION) + echo "Detected OpenSuse Linux '$RELEASE'" + case $RELEASE in + *"13.2"*) + echo "Installing libopenslide via zypper" + zypper --gpg-auto-import-keys install -y libopenslide-devel + ;; + esac + elif [ -f /etc/SuSE-brand ]; then + RELEASE=$(cat /etc/SuSE-brand | grep VERSION) + echo "Detected OpenSuse Linux '$RELEASE'" + case $RELEASE in + *"13.1") + echo "Installing libopenslide dependencies via zypper" + zypper --gpg-auto-import-keys install -y --type pattern devel_basis + 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 + install_libopenslide_from_source + ;; + esac + else + # Unsupported OS + sorry "openslide" "$(uname -a)" + fi +fi + +# OS-specific installations of libvips follows + +if [ -f /etc/debian_version ]; then + # Debian Linux + DISTRO=$(lsb_release -c -s) + echo "Detected Debian Linux '$DISTRO'" + case "$DISTRO" in + jessie|trusty|utopic|vivid|wily|xenial|qiana|rebecca|rafaela|freya|rosa|sarah|serena) + # Debian 8, Ubuntu 14.04+, Mint 17+ + echo "Installing libvips dependencies via apt-get" + 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 + install_libvips_from_source + ;; + precise|wheezy|maya) + # Debian 7, Ubuntu 12.04, Mint 13 + echo "Installing libvips dependencies via apt-get" + add-apt-repository -y ppa:lyrasis/precise-backports + apt-get update + 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 + install_libvips_from_source + ;; + *) + # Unsupported Debian-based OS + sorry "vips" "Debian-based $DISTRO" + ;; + esac +elif [ -f /etc/redhat-release ]; then + # Red Hat Linux + RELEASE=$(cat /etc/redhat-release) + echo "Detected Red Hat Linux '$RELEASE'" + case $RELEASE in + "Red Hat Enterprise Linux release 7."*|"CentOS Linux release 7."*|"Scientific Linux release 7."*) + # RHEL/CentOS 7 + echo "Installing libvips dependencies via yum" + yum groupinstall -y "Development Tools" + 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 + install_libvips_from_source "--prefix=/usr" + ;; + "Red Hat Enterprise Linux release 6."*|"CentOS release 6."*|"Scientific Linux release 6."*) + # RHEL/CentOS 6 + echo "Installing libvips dependencies via yum" + yum groupinstall -y "Development Tools" + yum install -y tar curl gtk-doc libxml2-devel libjpeg-turbo-devel libpng-devel libtiff-devel libexif-devel libgsf-devel lcms-devel ImageMagick-devel + yum install -y http://li.nux.ro/download/nux/dextop/el6/x86_64/nux-dextop-release-0-2.el6.nux.noarch.rpm + yum install -y --enablerepo=nux-dextop gobject-introspection-devel + yum install -y http://rpms.famillecollet.com/enterprise/remi-release-6.rpm + yum install -y --enablerepo=remi libwebp-devel + install_libvips_from_source "--prefix=/usr" + ;; + "Fedora"*) + # Fedora 21, 22, 23 + echo "Installing libvips dependencies via yum" + yum groupinstall -y "Development Tools" + 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 + install_libvips_from_source "--prefix=/usr" + ;; + *) + # Unsupported RHEL-based OS + sorry "vips" "$RELEASE" + ;; + esac +elif [ -f /etc/system-release ]; then + # Probably Amazon Linux + RELEASE=$(cat /etc/system-release) + case $RELEASE in + "Amazon Linux AMI release 2015.03"|"Amazon Linux AMI release 2015.09") + # Amazon Linux + echo "Detected '$RELEASE'" + echo "Installing libvips dependencies via yum" + yum groupinstall -y "Development Tools" + 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 + install_libvips_from_source "--prefix=/usr" + ;; + *) + # Unsupported Amazon Linux version + sorry "vips" "$RELEASE" + ;; + esac +elif [ -f /etc/os-release ]; then + RELEASE=$(cat /etc/os-release | grep VERSION) + echo "Detected OpenSuse Linux '$RELEASE'" + case $RELEASE in + *"13.2"*) + echo "Installing libvips dependencies via zypper" + zypper --gpg-auto-import-keys install -y --type pattern devel_basis + 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 + install_libvips_from_source + ;; + esac +elif [ -f /etc/SuSE-brand ]; then + RELEASE=$(cat /etc/SuSE-brand | grep VERSION) + echo "Detected OpenSuse Linux '$RELEASE'" + case $RELEASE in + *"13.1") + echo "Installing libvips dependencies via zypper" + zypper --gpg-auto-import-keys install -y --type pattern devel_basis + 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 + install_libvips_from_source + ;; + esac +else + # Unsupported OS + sorry "vips" "$(uname -a)" +fi diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/resize.go b/vendor/src/gopkg.in/h2non/bimg.v1/resize.go new file mode 100644 index 00000000..93d62480 --- /dev/null +++ b/vendor/src/gopkg.in/h2non/bimg.v1/resize.go @@ -0,0 +1,561 @@ +package bimg + +/* +#cgo pkg-config: vips +#include "vips/vips.h" +*/ +import "C" + +import ( + "errors" + "math" +) + +// Resize is used to transform a given image as byte buffer +// with the passed options. +func Resize(buf []byte, o Options) ([]byte, error) { + defer C.vips_thread_shutdown() + + image, imageType, err := loadImage(buf) + + if err != nil { + return nil, err + } + + // Clone and define default options + o = applyDefaults(o, imageType) + + if !IsTypeSupported(o.Type) { + return nil, errors.New("Unsupported image output type") + } + + debug("Options: %#v", o) + + // Auto rotate image based on EXIF orientation header + image, rotated, err := rotateAndFlipImage(image, o) + if err != nil { + return nil, err + } + + // If JPEG image, retrieve the buffer + if rotated && imageType == JPEG && !o.NoAutoRotate { + buf, err = getImageBuffer(image) + if err != nil { + return nil, err + } + } + + inWidth := int(image.Xsize) + inHeight := int(image.Ysize) + + // Infer the required operation based on the in/out image sizes for a coherent transformation + normalizeOperation(&o, inWidth, inHeight) + + // image calculations + factor := imageCalculations(&o, inWidth, inHeight) + shrink := calculateShrink(factor, o.Interpolator) + residual := calculateResidual(factor, shrink) + + // Do not enlarge the output if the input width or height + // are already less than the required dimensions + if !o.Enlarge && !o.Force { + if inWidth < o.Width && inHeight < o.Height { + factor = 1.0 + shrink = 1 + residual = 0 + o.Width = inWidth + o.Height = inHeight + } + } + + // Try to use libjpeg shrink-on-load + if imageType == JPEG && shrink >= 2 { + tmpImage, factor, err := shrinkJpegImage(buf, image, factor, shrink) + if err != nil { + return nil, err + } + + image = tmpImage + factor = math.Max(factor, 1.0) + shrink = int(math.Floor(factor)) + residual = float64(shrink) / factor + } + + // Zoom image, if necessary + image, err = zoomImage(image, o.Zoom) + if err != nil { + return nil, err + } + + // Transform image, if necessary + if shouldTransformImage(o, inWidth, inHeight) { + image, err = transformImage(image, o, shrink, residual) + if err != nil { + return nil, err + } + } + + // Apply effects, if necessary + if shouldApplyEffects(o) { + image, err = applyEffects(image, o) + if err != nil { + return nil, err + } + } + + // Add watermark, if necessary + image, err = watermarkImageWithText(image, o.Watermark) + if err != nil { + return nil, err + } + + // Add watermark, if necessary + image, err = watermarkImageWithAnotherImage(image, o.WatermarkImage) + if err != nil { + return nil, err + } + + // Flatten image on a background, if necessary + image, err = imageFlatten(image, imageType, o) + if err != nil { + return nil, err + } + + return saveImage(image, o) +} + +func loadImage(buf []byte) (*C.VipsImage, ImageType, error) { + if len(buf) == 0 { + return nil, JPEG, errors.New("Image buffer is empty") + } + + image, imageType, err := vipsRead(buf) + if err != nil { + return nil, JPEG, err + } + + return image, imageType, nil +} + +func applyDefaults(o Options, imageType ImageType) Options { + if o.Quality == 0 { + o.Quality = Quality + } + if o.Compression == 0 { + o.Compression = 6 + } + if o.Type == 0 { + o.Type = imageType + } + if o.Interpretation == 0 { + o.Interpretation = InterpretationSRGB + } + return o +} + +func saveImage(image *C.VipsImage, o Options) ([]byte, error) { + saveOptions := vipsSaveOptions{ + Quality: o.Quality, + Type: o.Type, + Compression: o.Compression, + Interlace: o.Interlace, + NoProfile: o.NoProfile, + Interpretation: o.Interpretation, + } + // Finally get the resultant buffer + return vipsSave(image, saveOptions) +} + +func normalizeOperation(o *Options, inWidth, inHeight int) { + if !o.Force && !o.Crop && !o.Embed && !o.Enlarge && o.Rotate == 0 && (o.Width > 0 || o.Height > 0) { + o.Force = true + } +} + +func shouldTransformImage(o Options, inWidth, inHeight int) bool { + return o.Force || (o.Width > 0 && o.Width != inWidth) || + (o.Height > 0 && o.Height != inHeight) || o.AreaWidth > 0 || o.AreaHeight > 0 +} + +func shouldApplyEffects(o Options) bool { + return o.GaussianBlur.Sigma > 0 || o.GaussianBlur.MinAmpl > 0 || o.Sharpen.Radius > 0 && o.Sharpen.Y2 > 0 || o.Sharpen.Y3 > 0 +} + +func transformImage(image *C.VipsImage, o Options, shrink int, residual float64) (*C.VipsImage, error) { + var err error + // Use vips_shrink with the integral reduction + if shrink > 1 { + image, residual, err = shrinkImage(image, o, residual, shrink) + if err != nil { + return nil, err + } + } + + residualx, residualy := residual, residual + if o.Force { + residualx = float64(o.Width) / float64(image.Xsize) + residualy = float64(o.Height) / float64(image.Ysize) + } + + if o.Force || residual != 0 { + image, err = vipsAffine(image, residualx, residualy, o.Interpolator) + if err != nil { + return nil, err + } + } + + if o.Force { + o.Crop = false + o.Embed = false + } + + image, err = extractOrEmbedImage(image, o) + if err != nil { + return nil, err + } + + debug("Transform: shrink=%v, residual=%v, interpolator=%v", + shrink, residual, o.Interpolator.String()) + + return image, nil +} + +func applyEffects(image *C.VipsImage, o Options) (*C.VipsImage, error) { + var err error + + if o.GaussianBlur.Sigma > 0 || o.GaussianBlur.MinAmpl > 0 { + image, err = vipsGaussianBlur(image, o.GaussianBlur) + if err != nil { + return nil, err + } + } + + if o.Sharpen.Radius > 0 && o.Sharpen.Y2 > 0 || o.Sharpen.Y3 > 0 { + image, err = vipsSharpen(image, o.Sharpen) + if err != nil { + return nil, err + } + } + + debug("Effects: gaussSigma=%v, gaussMinAmpl=%v, sharpenRadius=%v", + o.GaussianBlur.Sigma, o.GaussianBlur.MinAmpl, o.Sharpen.Radius) + + return image, nil +} + +func extractOrEmbedImage(image *C.VipsImage, o Options) (*C.VipsImage, error) { + var err error + inWidth := int(image.Xsize) + inHeight := int(image.Ysize) + + switch { + case o.Gravity == GravitySmart, o.SmartCrop: + image, err = vipsSmartCrop(image, o.Width, o.Height) + break + case o.Crop: + width := int(math.Min(float64(inWidth), float64(o.Width))) + height := int(math.Min(float64(inHeight), float64(o.Height))) + left, top := calculateCrop(inWidth, inHeight, o.Width, o.Height, o.Gravity) + left, top = int(math.Max(float64(left), 0)), int(math.Max(float64(top), 0)) + image, err = vipsExtract(image, left, top, width, height) + break + case o.Embed: + left, top := (o.Width-inWidth)/2, (o.Height-inHeight)/2 + image, err = vipsEmbed(image, left, top, o.Width, o.Height, o.Extend, o.Background) + break + + case o.Top != 0 || o.Left != 0 || o.AreaWidth != 0 || o.AreaHeight != 0: + if o.AreaWidth == 0 { + o.AreaHeight = o.Width + } + if o.AreaHeight == 0 { + o.AreaHeight = o.Height + } + if o.AreaWidth == 0 || o.AreaHeight == 0 { + return nil, errors.New("Extract area width/height params are required") + } + image, err = vipsExtract(image, o.Left, o.Top, o.AreaWidth, o.AreaHeight) + break + } + + return image, err +} + +func rotateAndFlipImage(image *C.VipsImage, o Options) (*C.VipsImage, bool, error) { + var err error + var rotated bool + var direction Direction = -1 + + if o.NoAutoRotate == false { + rotation, flip := calculateRotationAndFlip(image, o.Rotate) + if flip { + o.Flip = flip + } + if rotation > 0 && o.Rotate == 0 { + o.Rotate = rotation + } + } + + if o.Rotate > 0 { + rotated = true + image, err = vipsRotate(image, getAngle(o.Rotate)) + } + + if o.Flip { + direction = Horizontal + } else if o.Flop { + direction = Vertical + } + + if direction != -1 { + rotated = true + image, err = vipsFlip(image, direction) + } + + return image, rotated, err +} + +func watermarkImageWithText(image *C.VipsImage, w Watermark) (*C.VipsImage, error) { + if w.Text == "" { + return image, nil + } + + // Defaults + if w.Font == "" { + w.Font = WatermarkFont + } + if w.Width == 0 { + w.Width = int(math.Floor(float64(image.Xsize / 6))) + } + if w.DPI == 0 { + w.DPI = 150 + } + if w.Margin == 0 { + w.Margin = w.Width + } + if w.Opacity == 0 { + w.Opacity = 0.25 + } else if w.Opacity > 1 { + w.Opacity = 1 + } + + image, err := vipsWatermark(image, w) + if err != nil { + return nil, err + } + + return image, nil +} + +func watermarkImageWithAnotherImage(image *C.VipsImage, w WatermarkImage) (*C.VipsImage, error) { + + if len(w.Buf) == 0 { + return image, nil + } + + if w.Opacity == 0.0 { + w.Opacity = 1.0 + } + + image, err := vipsDrawWatermark(image, w) + + if err != nil { + return nil, err + } + + return image, nil +} + +func imageFlatten(image *C.VipsImage, imageType ImageType, o Options) (*C.VipsImage, error) { + // Only PNG images are supported for now + if imageType != PNG || o.Background == ColorBlack { + return image, nil + } + return vipsFlattenBackground(image, o.Background) +} + +func zoomImage(image *C.VipsImage, zoom int) (*C.VipsImage, error) { + if zoom == 0 { + return image, nil + } + return vipsZoom(image, zoom+1) +} + +func shrinkImage(image *C.VipsImage, o Options, residual float64, shrink int) (*C.VipsImage, float64, error) { + // Use vips_shrink with the integral reduction + image, err := vipsShrink(image, shrink) + if err != nil { + return nil, 0, err + } + + // Recalculate residual float based on dimensions of required vs shrunk images + residualx := float64(o.Width) / float64(image.Xsize) + residualy := float64(o.Height) / float64(image.Ysize) + + if o.Crop { + residual = math.Max(residualx, residualy) + } else { + residual = math.Min(residualx, residualy) + } + + return image, residual, nil +} + +func shrinkJpegImage(buf []byte, input *C.VipsImage, factor float64, shrink int) (*C.VipsImage, float64, error) { + var image *C.VipsImage + var err error + shrinkOnLoad := 1 + + // Recalculate integral shrink and double residual + switch { + case shrink >= 8: + factor = factor / 8 + shrinkOnLoad = 8 + case shrink >= 4: + factor = factor / 4 + shrinkOnLoad = 4 + case shrink >= 2: + factor = factor / 2 + shrinkOnLoad = 2 + } + + // Reload input using shrink-on-load + if shrinkOnLoad > 1 { + image, err = vipsShrinkJpeg(buf, input, shrinkOnLoad) + } + + return image, factor, err +} + +func imageCalculations(o *Options, inWidth, inHeight int) float64 { + factor := 1.0 + xfactor := float64(inWidth) / float64(o.Width) + yfactor := float64(inHeight) / float64(o.Height) + + switch { + // Fixed width and height + case o.Width > 0 && o.Height > 0: + if o.Crop { + factor = math.Min(xfactor, yfactor) + } else { + factor = math.Max(xfactor, yfactor) + } + // Fixed width, auto height + case o.Width > 0: + if o.Crop { + o.Height = inHeight + } else { + factor = xfactor + o.Height = roundFloat(float64(inHeight) / factor) + } + // Fixed height, auto width + case o.Height > 0: + if o.Crop { + o.Width = inWidth + } else { + factor = yfactor + o.Width = roundFloat(float64(inWidth) / factor) + } + // Identity transform + default: + o.Width = inWidth + o.Height = inHeight + break + } + + return factor +} + +func roundFloat(f float64) int { + if f < 0 { + return int(math.Ceil(f - 0.5)) + } + return int(math.Floor(f + 0.5)) +} + +func calculateCrop(inWidth, inHeight, outWidth, outHeight int, gravity Gravity) (int, int) { + left, top := 0, 0 + + switch gravity { + case GravityNorth: + left = (inWidth - outWidth + 1) / 2 + case GravityEast: + left = inWidth - outWidth + top = (inHeight - outHeight + 1) / 2 + case GravitySouth: + left = (inWidth - outWidth + 1) / 2 + top = inHeight - outHeight + case GravityWest: + top = (inHeight - outHeight + 1) / 2 + default: + left = (inWidth - outWidth + 1) / 2 + top = (inHeight - outHeight + 1) / 2 + } + + return left, top +} + +func calculateRotationAndFlip(image *C.VipsImage, angle Angle) (Angle, bool) { + rotate := D0 + flip := false + + if angle > 0 { + return rotate, flip + } + + switch vipsExifOrientation(image) { + case 6: + rotate = D90 + break + case 3: + rotate = D180 + break + case 8: + rotate = D270 + break + case 2: + flip = true + break // flip 1 + case 7: + flip = true + rotate = D90 + break // flip 6 + case 4: + flip = true + rotate = D180 + break // flip 3 + case 5: + flip = true + rotate = D270 + break // flip 8 + } + + return rotate, flip +} + +func calculateShrink(factor float64, i Interpolator) int { + var shrink float64 + + // Calculate integral box shrink + windowSize := vipsWindowSize(i.String()) + if factor >= 2 && windowSize > 3 { + // Shrink less, affine more with interpolators that use at least 4x4 pixel window, e.g. bicubic + shrink = float64(math.Floor(factor * 3.0 / windowSize)) + } else { + shrink = math.Floor(factor) + } + + return int(math.Max(shrink, 1)) +} + +func calculateResidual(factor float64, shrink int) float64 { + return float64(shrink) / factor +} + +func getAngle(angle Angle) Angle { + divisor := angle % 90 + if divisor != 0 { + angle = angle - divisor + } + return Angle(math.Min(float64(angle), 270)) +} diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/resize_test.go b/vendor/src/gopkg.in/h2non/bimg.v1/resize_test.go new file mode 100644 index 00000000..28e31095 --- /dev/null +++ b/vendor/src/gopkg.in/h2non/bimg.v1/resize_test.go @@ -0,0 +1,644 @@ +package bimg + +import ( + "bytes" + "crypto/md5" + "image" + "image/jpeg" + "io/ioutil" + "os" + "path" + "strconv" + "testing" +) + +func TestResize(t *testing.T) { + options := Options{Width: 800, Height: 600} + buf, _ := Read("fixtures/test.jpg") + + newImg, err := Resize(buf, options) + if err != nil { + t.Errorf("Resize(imgData, %#v) error: %#v", options, err) + } + + if DetermineImageType(newImg) != JPEG { + t.Fatal("Image is not jpeg") + } + + size, _ := Size(newImg) + if size.Height != options.Height || size.Width != options.Width { + t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) + } + + Write("fixtures/test_out.jpg", newImg) +} + +func TestResizeVerticalImage(t *testing.T) { + tests := []struct { + format ImageType + options Options + }{ + {JPEG, Options{Width: 800, Height: 600}}, + {JPEG, Options{Width: 1000, Height: 1000}}, + {JPEG, Options{Width: 1000, Height: 1500}}, + {JPEG, Options{Width: 1000}}, + {JPEG, Options{Height: 1500}}, + {JPEG, Options{Width: 100, Height: 50}}, + {JPEG, Options{Width: 2000, Height: 2000}}, + {JPEG, Options{Width: 500, Height: 1000}}, + {JPEG, Options{Width: 500}}, + {JPEG, Options{Height: 500}}, + {JPEG, Options{Crop: true, Width: 500, Height: 1000}}, + {JPEG, Options{Crop: true, Enlarge: true, Width: 2000, Height: 1400}}, + {JPEG, Options{Enlarge: true, Force: true, Width: 2000, Height: 2000}}, + {JPEG, Options{Force: true, Width: 2000, Height: 2000}}, + } + + buf, _ := Read("fixtures/vertical.jpg") + for _, test := range tests { + image, err := Resize(buf, test.options) + if err != nil { + t.Errorf("Resize(imgData, %#v) error: %#v", test.options, err) + } + + if DetermineImageType(image) != test.format { + t.Fatalf("Image format is invalid. Expected: %#v", test.format) + } + + size, _ := Size(image) + if test.options.Height > 0 && size.Height != test.options.Height { + t.Fatalf("Invalid height: %d", size.Height) + } + if test.options.Width > 0 && size.Width != test.options.Width { + t.Fatalf("Invalid width: %d", size.Width) + } + + Write("fixtures/test_vertical_"+strconv.Itoa(test.options.Width)+"x"+strconv.Itoa(test.options.Height)+"_out.jpg", image) + } +} + +func TestResizeCustomSizes(t *testing.T) { + tests := []struct { + format ImageType + options Options + }{ + {JPEG, Options{Width: 800, Height: 600}}, + {JPEG, Options{Width: 1000, Height: 1000}}, + {JPEG, Options{Width: 100, Height: 50}}, + {JPEG, Options{Width: 2000, Height: 2000}}, + {JPEG, Options{Width: 500, Height: 1000}}, + {JPEG, Options{Width: 500}}, + {JPEG, Options{Height: 500}}, + {JPEG, Options{Crop: true, Width: 500, Height: 1000}}, + {JPEG, Options{Crop: true, Enlarge: true, Width: 2000, Height: 1400}}, + {JPEG, Options{Enlarge: true, Force: true, Width: 2000, Height: 2000}}, + {JPEG, Options{Force: true, Width: 2000, Height: 2000}}, + } + + buf, _ := Read("fixtures/test.jpg") + for _, test := range tests { + image, err := Resize(buf, test.options) + if err != nil { + t.Errorf("Resize(imgData, %#v) error: %#v", test.options, err) + } + + if DetermineImageType(image) != test.format { + t.Fatalf("Image format is invalid. Expected: %#v", test.format) + } + + size, _ := Size(image) + if test.options.Height > 0 && size.Height != test.options.Height { + t.Fatalf("Invalid height: %d", size.Height) + } + if test.options.Width > 0 && size.Width != test.options.Width { + t.Fatalf("Invalid width: %d", size.Width) + } + } +} + +func TestResizePrecision(t *testing.T) { + // see https://github.com/h2non/bimg/issues/99 + img := image.NewGray16(image.Rect(0, 0, 1920, 1080)) + input := &bytes.Buffer{} + jpeg.Encode(input, img, nil) + + opts := Options{Width: 300} + newImg, err := Resize(input.Bytes(), opts) + if err != nil { + t.Fatalf("Resize(imgData, %#v) error: %#v", opts, err) + } + + size, _ := Size(newImg) + if size.Width != opts.Width { + t.Fatalf("Invalid width: %d", size.Width) + } +} + +func TestRotate(t *testing.T) { + options := Options{Width: 800, Height: 600, Rotate: 270, Crop: true} + buf, _ := Read("fixtures/test.jpg") + + newImg, err := Resize(buf, options) + if err != nil { + t.Errorf("Resize(imgData, %#v) error: %#v", options, err) + } + + if DetermineImageType(newImg) != JPEG { + t.Error("Image is not jpeg") + } + + size, _ := Size(newImg) + if size.Width != options.Width || size.Height != options.Height { + t.Errorf("Invalid image size: %dx%d", size.Width, size.Height) + } + + Write("fixtures/test_rotate_out.jpg", newImg) +} + +func TestInvalidRotateDegrees(t *testing.T) { + options := Options{Width: 800, Height: 600, Rotate: 111, Crop: true} + buf, _ := Read("fixtures/test.jpg") + + newImg, err := Resize(buf, options) + if err != nil { + t.Errorf("Resize(imgData, %#v) error: %#v", options, err) + } + + if DetermineImageType(newImg) != JPEG { + t.Errorf("Image is not jpeg") + } + + size, _ := Size(newImg) + if size.Width != options.Width || size.Height != options.Height { + t.Errorf("Invalid image size: %dx%d", size.Width, size.Height) + } + + Write("fixtures/test_rotate_invalid_out.jpg", newImg) +} + +func TestCorruptedImage(t *testing.T) { + options := Options{Width: 800, Height: 600} + buf, _ := Read("fixtures/corrupt.jpg") + + newImg, err := Resize(buf, options) + if err != nil { + t.Errorf("Resize(imgData, %#v) error: %#v", options, err) + } + + if DetermineImageType(newImg) != JPEG { + t.Fatal("Image is not jpeg") + } + + size, _ := Size(newImg) + if size.Height != options.Height || size.Width != options.Width { + t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) + } + + Write("fixtures/test_corrupt_out.jpg", newImg) +} + +func TestNoColorProfile(t *testing.T) { + options := Options{Width: 800, Height: 600, NoProfile: true} + buf, _ := Read("fixtures/test.jpg") + + newImg, err := Resize(buf, options) + if err != nil { + t.Errorf("Resize(imgData, %#v) error: %#v", options, err) + } + + metadata, err := Metadata(newImg) + if metadata.Profile == true { + t.Fatal("Invalid profile data") + } + + size, _ := Size(newImg) + if size.Height != options.Height || size.Width != options.Width { + t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) + } +} + +func TestEmbedExtendColor(t *testing.T) { + options := Options{Width: 400, Height: 600, Crop: false, Embed: true, Extend: ExtendWhite, Background: Color{255, 20, 10}} + buf, _ := Read("fixtures/test_issue.jpg") + + newImg, err := Resize(buf, options) + if err != nil { + t.Errorf("Resize(imgData, %#v) error: %#v", options, err) + } + + size, _ := Size(newImg) + if size.Height != options.Height || size.Width != options.Width { + t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) + } + + Write("fixtures/test_extend_white_out.jpg", newImg) +} + +func TestEmbedExtendWithCustomColor(t *testing.T) { + options := Options{Width: 400, Height: 600, Crop: false, Embed: true, Extend: 5, Background: Color{255, 20, 10}} + buf, _ := Read("fixtures/test_issue.jpg") + + newImg, err := Resize(buf, options) + if err != nil { + t.Errorf("Resize(imgData, %#v) error: %#v", options, err) + } + + size, _ := Size(newImg) + if size.Height != options.Height || size.Width != options.Width { + t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) + } + + Write("fixtures/test_extend_background_out.jpg", newImg) +} + +func TestGaussianBlur(t *testing.T) { + options := Options{Width: 800, Height: 600, GaussianBlur: GaussianBlur{Sigma: 5}} + buf, _ := Read("fixtures/test.jpg") + + newImg, err := Resize(buf, options) + if err != nil { + t.Errorf("Resize(imgData, %#v) error: %#v", options, err) + } + + size, _ := Size(newImg) + if size.Height != options.Height || size.Width != options.Width { + t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) + } + + Write("fixtures/test_gaussian_out.jpg", newImg) +} + +func TestSharpen(t *testing.T) { + options := Options{Width: 800, Height: 600, Sharpen: Sharpen{Radius: 1, X1: 1.5, Y2: 20, Y3: 50, M1: 1, M2: 2}} + buf, _ := Read("fixtures/test.jpg") + + newImg, err := Resize(buf, options) + if err != nil { + t.Errorf("Resize(imgData, %#v) error: %#v", options, err) + } + + size, _ := Size(newImg) + if size.Height != options.Height || size.Width != options.Width { + t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) + } + + Write("fixtures/test_sharpen_out.jpg", newImg) +} + +func TestExtractWithDefaultAxis(t *testing.T) { + options := Options{AreaWidth: 200, AreaHeight: 200} + buf, _ := Read("fixtures/test.jpg") + + newImg, err := Resize(buf, options) + if err != nil { + t.Errorf("Resize(imgData, %#v) error: %#v", options, err) + } + + size, _ := Size(newImg) + if size.Height != options.AreaHeight || size.Width != options.AreaWidth { + t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) + } + + Write("fixtures/test_extract_defaults_out.jpg", newImg) +} + +func TestExtractCustomAxis(t *testing.T) { + options := Options{Top: 100, Left: 100, AreaWidth: 200, AreaHeight: 200} + buf, _ := Read("fixtures/test.jpg") + + newImg, err := Resize(buf, options) + if err != nil { + t.Errorf("Resize(imgData, %#v) error: %#v", options, err) + } + + size, _ := Size(newImg) + if size.Height != options.AreaHeight || size.Width != options.AreaWidth { + t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) + } + + Write("fixtures/test_extract_custom_axis_out.jpg", newImg) +} + +func TestConvert(t *testing.T) { + width, height := 300, 240 + formats := [3]ImageType{PNG, WEBP, JPEG} + + files := []string{ + "test.jpg", + "test.png", + "test.webp", + } + + for _, file := range files { + img, err := os.Open("fixtures/" + file) + if err != nil { + t.Fatal(err) + } + + buf, err := ioutil.ReadAll(img) + if err != nil { + t.Fatal(err) + } + img.Close() + + for _, format := range formats { + options := Options{Width: width, Height: height, Crop: true, Type: format} + + newImg, err := Resize(buf, options) + if err != nil { + t.Errorf("Resize(imgData, %#v) error: %#v", options, err) + } + + if DetermineImageType(newImg) != format { + t.Fatal("Image is not png") + } + + size, _ := Size(newImg) + if size.Height != height || size.Width != width { + t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) + } + } + } +} + +func TestResizePngWithTransparency(t *testing.T) { + width, height := 300, 240 + + options := Options{Width: width, Height: height, Crop: true} + img, err := os.Open("fixtures/transparent.png") + if err != nil { + t.Fatal(err) + } + defer img.Close() + + buf, err := ioutil.ReadAll(img) + if err != nil { + t.Fatal(err) + } + + newImg, err := Resize(buf, options) + if err != nil { + t.Errorf("Resize(imgData, %#v) error: %#v", options, err) + } + + if DetermineImageType(newImg) != PNG { + t.Fatal("Image is not png") + } + + size, _ := Size(newImg) + if size.Height != height || size.Width != width { + t.Fatal("Invalid image size") + } + + Write("fixtures/transparent_out.png", newImg) +} + +func TestIfBothSmartCropOptionsAreIdentical(t *testing.T) { + if !(VipsMajorVersion >= 8 && VipsMinorVersion > 4) { + t.Skipf("Skipping this test, libvips doesn't meet version requirement %s > 8.4", VipsVersion) + } + + benchmarkOptions := Options{Width: 100, Height: 100, Crop: true} + smartCropOptions := Options{Width: 100, Height: 100, Crop: true, SmartCrop: true} + gravityOptions := Options{Width: 100, Height: 100, Crop: true, Gravity: GravitySmart} + + testImg, err := os.Open("fixtures/northern_cardinal_bird.jpg") + if err != nil { + t.Fatal(err) + } + defer testImg.Close() + + testImgByte, err := ioutil.ReadAll(testImg) + if err != nil { + t.Fatal(err) + } + + scImg, err := Resize(testImgByte, smartCropOptions) + if err != nil { + t.Fatal(err) + } + + gImg, err := Resize(testImgByte, gravityOptions) + if err != nil { + t.Fatal(err) + } + + benchmarkImg, err := Resize(testImgByte, benchmarkOptions) + if err != nil { + t.Fatal(err) + } + + sch, gh, bh := md5.Sum(scImg), md5.Sum(gImg), md5.Sum(benchmarkImg) + if gh == bh || sch == bh { + t.Error("Expected both options produce a different result from a standard crop.") + } + + if sch != gh { + t.Errorf("Expected both options to result in the same output, %x != %x", sch, gh) + } +} + +func runBenchmarkResize(file string, o Options, b *testing.B) { + buf, _ := Read(path.Join("fixtures", file)) + + for n := 0; n < b.N; n++ { + Resize(buf, o) + } +} + +func BenchmarkRotateJpeg(b *testing.B) { + options := Options{Rotate: 180} + runBenchmarkResize("test.jpg", options, b) +} + +func BenchmarkResizeLargeJpeg(b *testing.B) { + options := Options{ + Width: 800, + Height: 600, + } + runBenchmarkResize("test.jpg", options, b) +} + +func BenchmarkResizePng(b *testing.B) { + options := Options{ + Width: 200, + Height: 200, + } + runBenchmarkResize("test.png", options, b) +} + +func BenchmarkResizeWebP(b *testing.B) { + options := Options{ + Width: 200, + Height: 200, + } + runBenchmarkResize("test.webp", options, b) +} + +func BenchmarkConvertToJpeg(b *testing.B) { + options := Options{Type: JPEG} + runBenchmarkResize("test.png", options, b) +} + +func BenchmarkConvertToPng(b *testing.B) { + options := Options{Type: PNG} + runBenchmarkResize("test.jpg", options, b) +} + +func BenchmarkConvertToWebp(b *testing.B) { + options := Options{Type: WEBP} + runBenchmarkResize("test.jpg", options, b) +} + +func BenchmarkCropJpeg(b *testing.B) { + options := Options{ + Width: 800, + Height: 600, + } + runBenchmarkResize("test.jpg", options, b) +} + +func BenchmarkCropPng(b *testing.B) { + options := Options{ + Width: 800, + Height: 600, + } + runBenchmarkResize("test.png", options, b) +} + +func BenchmarkCropWebP(b *testing.B) { + options := Options{ + Width: 800, + Height: 600, + } + runBenchmarkResize("test.webp", options, b) +} + +func BenchmarkExtractJpeg(b *testing.B) { + options := Options{ + Top: 100, + Left: 50, + AreaWidth: 600, + AreaHeight: 480, + } + runBenchmarkResize("test.jpg", options, b) +} + +func BenchmarkExtractPng(b *testing.B) { + options := Options{ + Top: 100, + Left: 50, + AreaWidth: 600, + AreaHeight: 480, + } + runBenchmarkResize("test.png", options, b) +} + +func BenchmarkExtractWebp(b *testing.B) { + options := Options{ + Top: 100, + Left: 50, + AreaWidth: 600, + AreaHeight: 480, + } + runBenchmarkResize("test.webp", options, b) +} + +func BenchmarkZoomJpeg(b *testing.B) { + options := Options{Zoom: 1} + runBenchmarkResize("test.jpg", options, b) +} + +func BenchmarkZoomPng(b *testing.B) { + options := Options{Zoom: 1} + runBenchmarkResize("test.png", options, b) +} + +func BenchmarkZoomWebp(b *testing.B) { + options := Options{Zoom: 1} + runBenchmarkResize("test.webp", options, b) +} + +func BenchmarkWatermarkJpeg(b *testing.B) { + options := Options{ + Watermark: Watermark{ + Text: "Chuck Norris (c) 2315", + Opacity: 0.25, + Width: 200, + DPI: 100, + Margin: 150, + Font: "sans bold 12", + Background: Color{255, 255, 255}, + }, + } + runBenchmarkResize("test.jpg", options, b) +} + +func BenchmarkWatermarPng(b *testing.B) { + options := Options{ + Watermark: Watermark{ + Text: "Chuck Norris (c) 2315", + Opacity: 0.25, + Width: 200, + DPI: 100, + Margin: 150, + Font: "sans bold 12", + Background: Color{255, 255, 255}, + }, + } + runBenchmarkResize("test.png", options, b) +} + +func BenchmarkWatermarWebp(b *testing.B) { + options := Options{ + Watermark: Watermark{ + Text: "Chuck Norris (c) 2315", + Opacity: 0.25, + Width: 200, + DPI: 100, + Margin: 150, + Font: "sans bold 12", + Background: Color{255, 255, 255}, + }, + } + runBenchmarkResize("test.webp", options, b) +} + +func BenchmarkWatermarkImageJpeg(b *testing.B) { + watermark := readFile("transparent.png") + options := Options{ + WatermarkImage: WatermarkImage{ + Buf: watermark, + Opacity: 0.25, + Left: 100, + Top: 100, + }, + } + runBenchmarkResize("test.jpg", options, b) +} + +func BenchmarkWatermarImagePng(b *testing.B) { + watermark := readFile("transparent.png") + options := Options{ + WatermarkImage: WatermarkImage{ + Buf: watermark, + Opacity: 0.25, + Left: 100, + Top: 100, + }, + } + runBenchmarkResize("test.png", options, b) +} + +func BenchmarkWatermarImageWebp(b *testing.B) { + watermark := readFile("transparent.png") + options := Options{ + WatermarkImage: WatermarkImage{ + Buf: watermark, + Opacity: 0.25, + Left: 100, + Top: 100, + }, + } + runBenchmarkResize("test.webp", options, b) +} diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/type.go b/vendor/src/gopkg.in/h2non/bimg.v1/type.go new file mode 100644 index 00000000..260adbf8 --- /dev/null +++ b/vendor/src/gopkg.in/h2non/bimg.v1/type.go @@ -0,0 +1,172 @@ +package bimg + +import ( + "regexp" + "sync" + "unicode/utf8" +) + +const ( + // UNKNOWN represents an unknow image type value. + UNKNOWN ImageType = iota + // JPEG represents the JPEG image type. + JPEG + // WEBP represents the WEBP image type. + WEBP + // PNG represents the PNG image type. + PNG + // TIFF represents the TIFF image type. + TIFF + // GIF represents the GIF image type. + GIF + // PDF represents the PDF type. + PDF + // SVG represents the SVG image type. + SVG + // MAGICK represents the libmagick compatible genetic image type. + MAGICK +) + +// ImageType represents an image type value. +type ImageType int + +var ( + htmlCommentRegex = regexp.MustCompile("(?i)") + svgRegex = regexp.MustCompile(`(?i)^\s*(?:<\?xml[^>]*>\s*)?(?:]*>\s*)?]*>[^*]*<\/svg>\s*$`) +) + +// ImageTypes stores as pairs of image types supported and its alias names. +var ImageTypes = map[ImageType]string{ + JPEG: "jpeg", + PNG: "png", + WEBP: "webp", + TIFF: "tiff", + GIF: "gif", + PDF: "pdf", + SVG: "svg", + MAGICK: "magick", +} + +// imageMutex is used to provide thread-safe synchronization +// for SupportedImageTypes map. +var imageMutex = &sync.RWMutex{} + +// SupportedImageType represents whether a type can be loaded and/or saved by +// the current libvips compilation. +type SupportedImageType struct { + Load bool + Save bool +} + +// SupportedImageTypes stores the optional image type supported +// by the current libvips compilation. +// Note: lazy evaluation as demand is required due +// to bootstrap runtime limitation with C/libvips world. +var SupportedImageTypes = map[ImageType]SupportedImageType{} + +// discoverSupportedImageTypes is used to fill SupportedImageTypes map. +func discoverSupportedImageTypes() { + imageMutex.Lock() + for imageType := range ImageTypes { + SupportedImageTypes[imageType] = SupportedImageType{ + Load: VipsIsTypeSupported(imageType), + Save: VipsIsTypeSupportedSave(imageType), + } + } + imageMutex.Unlock() +} + +// isBinary checks if the given buffer is a binary file. +func isBinary(buf []byte) bool { + if len(buf) < 24 { + return false + } + for i := 0; i < 24; i++ { + charCode, _ := utf8.DecodeRuneInString(string(buf[i])) + if charCode == 65533 || charCode <= 8 { + return true + } + } + return false +} + +// IsSVGImage returns true if the given buffer is a valid SVG image. +func IsSVGImage(buf []byte) bool { + return !isBinary(buf) && svgRegex.Match(htmlCommentRegex.ReplaceAll(buf, []byte{})) +} + +// DetermineImageType determines the image type format (jpeg, png, webp or tiff) +func DetermineImageType(buf []byte) ImageType { + return vipsImageType(buf) +} + +// DetermineImageTypeName determines the image type format by name (jpeg, png, webp or tiff) +func DetermineImageTypeName(buf []byte) string { + return ImageTypeName(vipsImageType(buf)) +} + +// IsImageTypeSupportedByVips returns true if the given image type +// is supported by current libvips compilation. +func IsImageTypeSupportedByVips(t ImageType) SupportedImageType { + imageMutex.RLock() + + // Discover supported image types and cache the result + itShouldDiscover := len(SupportedImageTypes) == 0 + if itShouldDiscover { + imageMutex.RUnlock() + discoverSupportedImageTypes() + } + + // Check if image type is actually supported + supported, ok := SupportedImageTypes[t] + if !itShouldDiscover { + imageMutex.RUnlock() + } + + if ok { + return supported + } + return SupportedImageType{Load: false, Save: false} +} + +// IsTypeSupported checks if a given image type is supported +func IsTypeSupported(t ImageType) bool { + _, ok := ImageTypes[t] + return ok && IsImageTypeSupportedByVips(t).Load +} + +// IsTypeNameSupported checks if a given image type name is supported +func IsTypeNameSupported(t string) bool { + for imageType, name := range ImageTypes { + if name == t { + return IsImageTypeSupportedByVips(imageType).Load + } + } + return false +} + +// IsTypeSupportedSave checks if a given image type is support for saving +func IsTypeSupportedSave(t ImageType) bool { + _, ok := ImageTypes[t] + return ok && IsImageTypeSupportedByVips(t).Save +} + +// IsTypeNameSupportedSave checks if a given image type name is supported for +// saving +func IsTypeNameSupportedSave(t string) bool { + for imageType, name := range ImageTypes { + if name == t { + return IsImageTypeSupportedByVips(imageType).Save + } + } + return false +} + +// ImageTypeName is used to get the human friendly name of an image format. +func ImageTypeName(t ImageType) string { + imageType := ImageTypes[t] + if imageType == "" { + return "unknown" + } + return imageType +} diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/type_test.go b/vendor/src/gopkg.in/h2non/bimg.v1/type_test.go new file mode 100644 index 00000000..9c83b0f5 --- /dev/null +++ b/vendor/src/gopkg.in/h2non/bimg.v1/type_test.go @@ -0,0 +1,128 @@ +package bimg + +import ( + "io/ioutil" + "os" + "path" + "testing" +) + +func TestDeterminateImageType(t *testing.T) { + files := []struct { + name string + expected ImageType + }{ + {"test.jpg", JPEG}, + {"test.png", PNG}, + {"test.webp", WEBP}, + {"test.gif", GIF}, + {"test.pdf", PDF}, + {"test.svg", SVG}, + {"test.jp2", MAGICK}, + } + + for _, file := range files { + img, _ := os.Open(path.Join("fixtures", file.name)) + buf, _ := ioutil.ReadAll(img) + defer img.Close() + + if DetermineImageType(buf) != file.expected { + t.Fatal("Image type is not valid") + } + } +} + +func TestDeterminateImageTypeName(t *testing.T) { + files := []struct { + name string + expected string + }{ + {"test.jpg", "jpeg"}, + {"test.png", "png"}, + {"test.webp", "webp"}, + {"test.gif", "gif"}, + {"test.pdf", "pdf"}, + {"test.svg", "svg"}, + {"test.jp2", "magick"}, + } + + for _, file := range files { + img, _ := os.Open(path.Join("fixtures", file.name)) + buf, _ := ioutil.ReadAll(img) + defer img.Close() + + if DetermineImageTypeName(buf) != file.expected { + t.Fatal("Image type is not valid") + } + } +} + +func TestIsTypeSupported(t *testing.T) { + types := []struct { + name ImageType + }{ + {JPEG}, {PNG}, {WEBP}, {GIF}, {PDF}, + } + + for _, n := range types { + if IsTypeSupported(n.name) == false { + t.Fatalf("Image type %#v is not valid", ImageTypes[n.name]) + } + } +} + +func TestIsTypeNameSupported(t *testing.T) { + types := []struct { + name string + expected bool + }{ + {"jpeg", true}, + {"png", true}, + {"webp", true}, + {"gif", true}, + {"pdf", true}, + } + + for _, n := range types { + if IsTypeNameSupported(n.name) != n.expected { + t.Fatalf("Image type %#v is not valid", n.name) + } + } +} + +func TestIsTypeSupportedSave(t *testing.T) { + types := []struct { + name ImageType + }{ + {JPEG}, {PNG}, {WEBP}, + } + if VipsVersion >= "8.5.0" { + types = append(types, struct{ name ImageType }{TIFF}) + } + + for _, n := range types { + if IsTypeSupportedSave(n.name) == false { + t.Fatalf("Image type %#v is not valid", ImageTypes[n.name]) + } + } +} + +func TestIsTypeNameSupportedSave(t *testing.T) { + types := []struct { + name string + expected bool + }{ + {"jpeg", true}, + {"png", true}, + {"webp", true}, + {"gif", false}, + {"pdf", false}, + {"tiff", VipsVersion >= "8.5.0"}, + } + + for _, n := range types { + if IsTypeNameSupportedSave(n.name) != n.expected { + t.Fatalf("Image type %#v is not valid", n.name) + } + } +} diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/version.go b/vendor/src/gopkg.in/h2non/bimg.v1/version.go new file mode 100644 index 00000000..682eaaaf --- /dev/null +++ b/vendor/src/gopkg.in/h2non/bimg.v1/version.go @@ -0,0 +1,4 @@ +package bimg + +// Version represents the current package semantic version. +const Version = "1.0.9" diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/vips.go b/vendor/src/gopkg.in/h2non/bimg.v1/vips.go new file mode 100644 index 00000000..ae654c5c --- /dev/null +++ b/vendor/src/gopkg.in/h2non/bimg.v1/vips.go @@ -0,0 +1,632 @@ +package bimg + +/* +#cgo pkg-config: vips +#include "vips.h" +*/ +import "C" + +import ( + "errors" + "fmt" + "math" + "os" + "runtime" + "strings" + "sync" + "unsafe" + + d "github.com/tj/go-debug" +) + +// debug is internally used to +var debug = d.Debug("bimg") + +// VipsVersion exposes the current libvips semantic version +const VipsVersion = string(C.VIPS_VERSION) + +// VipsMajorVersion exposes the current libvips major version number +const VipsMajorVersion = int(C.VIPS_MAJOR_VERSION) + +// VipsMinorVersion exposes the current libvips minor version number +const VipsMinorVersion = int(C.VIPS_MINOR_VERSION) + +const ( + maxCacheMem = 100 * 1024 * 1024 + maxCacheSize = 500 +) + +var ( + m sync.Mutex + initialized bool +) + +// VipsMemoryInfo represents the memory stats provided by libvips. +type VipsMemoryInfo struct { + Memory int64 + MemoryHighwater int64 + Allocations int64 +} + +// vipsSaveOptions represents the internal option used to talk with libvips. +type vipsSaveOptions struct { + Quality int + Compression int + Type ImageType + Interlace bool + NoProfile bool + Interpretation Interpretation +} + +type vipsWatermarkOptions struct { + Width C.int + DPI C.int + Margin C.int + NoReplicate C.int + Opacity C.float + Background [3]C.double +} + +type vipsWatermarkImageOptions struct { + Left C.int + Top C.int + Opacity C.float +} + +type vipsWatermarkTextOptions struct { + Text *C.char + Font *C.char +} + +func init() { + Initialize() +} + +// Initialize is used to explicitly start libvips in thread-safe way. +// Only call this function if you have previously turned off libvips. +func Initialize() { + if C.VIPS_MAJOR_VERSION <= 7 && C.VIPS_MINOR_VERSION < 40 { + panic("unsupported libvips version!") + } + + m.Lock() + runtime.LockOSThread() + defer m.Unlock() + defer runtime.UnlockOSThread() + + err := C.vips_init(C.CString("bimg")) + if err != 0 { + panic("unable to start vips!") + } + + // Set libvips cache params + C.vips_cache_set_max_mem(maxCacheMem) + C.vips_cache_set_max(maxCacheSize) + + // Define a custom thread concurrency limit in libvips (this may generate thread-unsafe issues) + // See: https://github.com/jcupitt/libvips/issues/261#issuecomment-92850414 + if os.Getenv("VIPS_CONCURRENCY") == "" { + C.vips_concurrency_set(1) + } + + // Enable libvips cache tracing + if os.Getenv("VIPS_TRACE") != "" { + C.vips_enable_cache_set_trace() + } + + initialized = true +} + +// Shutdown is used to shutdown libvips in a thread-safe way. +// You can call this to drop caches as well. +// If libvips was already initialized, the function is no-op +func Shutdown() { + m.Lock() + defer m.Unlock() + + if initialized { + C.vips_shutdown() + initialized = false + } +} + +// VipsDebugInfo outputs to stdout libvips collected data. Useful for debugging. +func VipsDebugInfo() { + C.im__print_all() +} + +// VipsMemory gets memory info stats from libvips (cache size, memory allocs...) +func VipsMemory() VipsMemoryInfo { + return VipsMemoryInfo{ + Memory: int64(C.vips_tracked_get_mem()), + MemoryHighwater: int64(C.vips_tracked_get_mem_highwater()), + Allocations: int64(C.vips_tracked_get_allocs()), + } +} + +// VipsIsTypeSupported returns true if the given image type +// is supported by the current libvips compilation. +func VipsIsTypeSupported(t ImageType) bool { + if t == JPEG { + return int(C.vips_type_find_bridge(C.JPEG)) != 0 + } + if t == WEBP { + return int(C.vips_type_find_bridge(C.WEBP)) != 0 + } + if t == PNG { + return int(C.vips_type_find_bridge(C.PNG)) != 0 + } + if t == GIF { + return int(C.vips_type_find_bridge(C.GIF)) != 0 + } + if t == PDF { + return int(C.vips_type_find_bridge(C.PDF)) != 0 + } + if t == SVG { + return int(C.vips_type_find_bridge(C.SVG)) != 0 + } + if t == TIFF { + return int(C.vips_type_find_bridge(C.TIFF)) != 0 + } + if t == MAGICK { + return int(C.vips_type_find_bridge(C.MAGICK)) != 0 + } + return false +} + +// VipsIsTypeSupportedSave returns true if the given image type +// is supported by the current libvips compilation for the +// save operation. +func VipsIsTypeSupportedSave(t ImageType) bool { + if t == JPEG { + return int(C.vips_type_find_save_bridge(C.JPEG)) != 0 + } + if t == WEBP { + return int(C.vips_type_find_save_bridge(C.WEBP)) != 0 + } + if t == PNG { + return int(C.vips_type_find_save_bridge(C.PNG)) != 0 + } + if t == TIFF { + return int(C.vips_type_find_save_bridge(C.TIFF)) != 0 + } + return false +} + +func vipsExifOrientation(image *C.VipsImage) int { + return int(C.vips_exif_orientation(image)) +} + +func vipsHasAlpha(image *C.VipsImage) bool { + return int(C.has_alpha_channel(image)) > 0 +} + +func vipsHasProfile(image *C.VipsImage) bool { + return int(C.has_profile_embed(image)) > 0 +} + +func vipsWindowSize(name string) float64 { + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + return float64(C.interpolator_window_size(cname)) +} + +func vipsSpace(image *C.VipsImage) string { + return C.GoString(C.vips_enum_nick_bridge(image)) +} + +func vipsRotate(image *C.VipsImage, angle Angle) (*C.VipsImage, error) { + var out *C.VipsImage + defer C.g_object_unref(C.gpointer(image)) + + err := C.vips_rotate(image, &out, C.int(angle)) + if err != 0 { + return nil, catchVipsError() + } + + return out, nil +} + +func vipsFlip(image *C.VipsImage, direction Direction) (*C.VipsImage, error) { + var out *C.VipsImage + defer C.g_object_unref(C.gpointer(image)) + + err := C.vips_flip_bridge(image, &out, C.int(direction)) + if err != 0 { + return nil, catchVipsError() + } + + return out, nil +} + +func vipsZoom(image *C.VipsImage, zoom int) (*C.VipsImage, error) { + var out *C.VipsImage + defer C.g_object_unref(C.gpointer(image)) + + err := C.vips_zoom_bridge(image, &out, C.int(zoom), C.int(zoom)) + if err != 0 { + return nil, catchVipsError() + } + + return out, nil +} + +func vipsWatermark(image *C.VipsImage, w Watermark) (*C.VipsImage, error) { + var out *C.VipsImage + + // Defaults + noReplicate := 0 + if w.NoReplicate { + noReplicate = 1 + } + + text := C.CString(w.Text) + font := C.CString(w.Font) + background := [3]C.double{C.double(w.Background.R), C.double(w.Background.G), C.double(w.Background.B)} + + textOpts := vipsWatermarkTextOptions{text, font} + opts := vipsWatermarkOptions{C.int(w.Width), C.int(w.DPI), C.int(w.Margin), C.int(noReplicate), C.float(w.Opacity), background} + + defer C.free(unsafe.Pointer(text)) + defer C.free(unsafe.Pointer(font)) + + err := C.vips_watermark(image, &out, (*C.WatermarkTextOptions)(unsafe.Pointer(&textOpts)), (*C.WatermarkOptions)(unsafe.Pointer(&opts))) + if err != 0 { + return nil, catchVipsError() + } + + return out, nil +} + +func vipsRead(buf []byte) (*C.VipsImage, ImageType, error) { + var image *C.VipsImage + imageType := vipsImageType(buf) + + if imageType == UNKNOWN { + return nil, UNKNOWN, errors.New("Unsupported image format") + } + + length := C.size_t(len(buf)) + imageBuf := unsafe.Pointer(&buf[0]) + + err := C.vips_init_image(imageBuf, length, C.int(imageType), &image) + if err != 0 { + return nil, UNKNOWN, catchVipsError() + } + + return image, imageType, nil +} + +func vipsColourspaceIsSupportedBuffer(buf []byte) (bool, error) { + image, _, err := vipsRead(buf) + if err != nil { + return false, err + } + C.g_object_unref(C.gpointer(image)) + return vipsColourspaceIsSupported(image), nil +} + +func vipsColourspaceIsSupported(image *C.VipsImage) bool { + return int(C.vips_colourspace_issupported_bridge(image)) == 1 +} + +func vipsInterpretationBuffer(buf []byte) (Interpretation, error) { + image, _, err := vipsRead(buf) + if err != nil { + return InterpretationError, err + } + C.g_object_unref(C.gpointer(image)) + return vipsInterpretation(image), nil +} + +func vipsInterpretation(image *C.VipsImage) Interpretation { + return Interpretation(C.vips_image_guess_interpretation_bridge(image)) +} + +func vipsFlattenBackground(image *C.VipsImage, background Color) (*C.VipsImage, error) { + var outImage *C.VipsImage + + backgroundC := [3]C.double{ + C.double(background.R), + C.double(background.G), + C.double(background.B), + } + + if vipsHasAlpha(image) { + err := C.vips_flatten_background_brigde(image, &outImage, + backgroundC[0], backgroundC[1], backgroundC[2]) + if int(err) != 0 { + return nil, catchVipsError() + } + C.g_object_unref(C.gpointer(image)) + image = outImage + } + + return image, nil +} + +func vipsPreSave(image *C.VipsImage, o *vipsSaveOptions) (*C.VipsImage, error) { + // Remove ICC profile metadata + if o.NoProfile { + C.remove_profile(image) + } + + // Use a default interpretation and cast it to C type + if o.Interpretation == 0 { + o.Interpretation = InterpretationSRGB + } + interpretation := C.VipsInterpretation(o.Interpretation) + + // Apply the proper colour space + var outImage *C.VipsImage + if vipsColourspaceIsSupported(image) { + err := C.vips_colourspace_bridge(image, &outImage, interpretation) + if int(err) != 0 { + return nil, catchVipsError() + } + image = outImage + } + + return image, nil +} + +func vipsSave(image *C.VipsImage, o vipsSaveOptions) ([]byte, error) { + defer C.g_object_unref(C.gpointer(image)) + + tmpImage, err := vipsPreSave(image, &o) + if err != nil { + return nil, err + } + + // When an image has an unsupported color space, vipsPreSave + // returns the pointer of the image passed to it unmodified. + // When this occurs, we must take care to not dereference the + // original image a second time; we may otherwise erroneously + // free the object twice. + if tmpImage != image { + defer C.g_object_unref(C.gpointer(tmpImage)) + } + + length := C.size_t(0) + saveErr := C.int(0) + interlace := C.int(boolToInt(o.Interlace)) + quality := C.int(o.Quality) + + if o.Type != 0 && !IsTypeSupportedSave(o.Type) { + return nil, fmt.Errorf("VIPS cannot save to %#v", ImageTypes[o.Type]) + } + var ptr unsafe.Pointer + switch o.Type { + case WEBP: + saveErr = C.vips_webpsave_bridge(tmpImage, &ptr, &length, 1, quality) + case PNG: + saveErr = C.vips_pngsave_bridge(tmpImage, &ptr, &length, 1, C.int(o.Compression), quality, interlace) + case TIFF: + saveErr = C.vips_tiffsave_bridge(tmpImage, &ptr, &length) + default: + saveErr = C.vips_jpegsave_bridge(tmpImage, &ptr, &length, 1, quality, interlace) + } + + if int(saveErr) != 0 { + return nil, catchVipsError() + } + + buf := C.GoBytes(ptr, C.int(length)) + + // Clean up + C.g_free(C.gpointer(ptr)) + C.vips_error_clear() + + return buf, nil +} + +func getImageBuffer(image *C.VipsImage) ([]byte, error) { + var ptr unsafe.Pointer + + length := C.size_t(0) + interlace := C.int(0) + quality := C.int(100) + + err := C.int(0) + err = C.vips_jpegsave_bridge(image, &ptr, &length, 1, quality, interlace) + if int(err) != 0 { + return nil, catchVipsError() + } + + defer C.g_free(C.gpointer(ptr)) + defer C.vips_error_clear() + + return C.GoBytes(ptr, C.int(length)), nil +} + +func vipsExtract(image *C.VipsImage, left, top, width, height int) (*C.VipsImage, error) { + var buf *C.VipsImage + defer C.g_object_unref(C.gpointer(image)) + + if width > MaxSize || height > MaxSize { + return nil, errors.New("Maximum image size exceeded") + } + + top, left = max(top), max(left) + err := C.vips_extract_area_bridge(image, &buf, C.int(left), C.int(top), C.int(width), C.int(height)) + if err != 0 { + return nil, catchVipsError() + } + + return buf, nil +} + +func vipsSmartCrop(image *C.VipsImage, width, height int) (*C.VipsImage, error) { + var buf *C.VipsImage + defer C.g_object_unref(C.gpointer(image)) + + if width > MaxSize || height > MaxSize { + return nil, errors.New("Maximum image size exceeded") + } + + err := C.vips_smartcrop_bridge(image, &buf, C.int(width), C.int(height)) + if err != 0 { + return nil, catchVipsError() + } + + return buf, nil +} + +func vipsShrinkJpeg(buf []byte, input *C.VipsImage, shrink int) (*C.VipsImage, error) { + var image *C.VipsImage + var ptr = unsafe.Pointer(&buf[0]) + defer C.g_object_unref(C.gpointer(input)) + + err := C.vips_jpegload_buffer_shrink(ptr, C.size_t(len(buf)), &image, C.int(shrink)) + if err != 0 { + return nil, catchVipsError() + } + + return image, nil +} + +func vipsShrink(input *C.VipsImage, shrink int) (*C.VipsImage, error) { + var image *C.VipsImage + defer C.g_object_unref(C.gpointer(input)) + + err := C.vips_shrink_bridge(input, &image, C.double(float64(shrink)), C.double(float64(shrink))) + if err != 0 { + return nil, catchVipsError() + } + + return image, nil +} + +func vipsEmbed(input *C.VipsImage, left, top, width, height int, extend Extend, background Color) (*C.VipsImage, error) { + var image *C.VipsImage + + // Max extend value, see: http://www.vips.ecs.soton.ac.uk/supported/8.4/doc/html/libvips/libvips-conversion.html#VipsExtend + if extend > 5 { + extend = ExtendBackground + } + + defer C.g_object_unref(C.gpointer(input)) + err := C.vips_embed_bridge(input, &image, C.int(left), C.int(top), C.int(width), + C.int(height), C.int(extend), C.double(background.R), C.double(background.G), C.double(background.B)) + if err != 0 { + return nil, catchVipsError() + } + + return image, nil +} + +func vipsAffine(input *C.VipsImage, residualx, residualy float64, i Interpolator) (*C.VipsImage, error) { + var image *C.VipsImage + cstring := C.CString(i.String()) + interpolator := C.vips_interpolate_new(cstring) + + defer C.free(unsafe.Pointer(cstring)) + defer C.g_object_unref(C.gpointer(input)) + defer C.g_object_unref(C.gpointer(interpolator)) + + err := C.vips_affine_interpolator(input, &image, C.double(residualx), 0, 0, C.double(residualy), interpolator) + if err != 0 { + return nil, catchVipsError() + } + + return image, nil +} + +func vipsImageType(buf []byte) ImageType { + if len(buf) == 0 { + return UNKNOWN + } + if buf[0] == 0x89 && buf[1] == 0x50 && buf[2] == 0x4E && buf[3] == 0x47 { + return PNG + } + if buf[0] == 0xFF && buf[1] == 0xD8 && buf[2] == 0xFF { + return JPEG + } + if IsTypeSupported(WEBP) && buf[8] == 0x57 && buf[9] == 0x45 && buf[10] == 0x42 && buf[11] == 0x50 { + return WEBP + } + if IsTypeSupported(TIFF) && + ((buf[0] == 0x49 && buf[1] == 0x49 && buf[2] == 0x2A && buf[3] == 0x0) || + (buf[0] == 0x4D && buf[1] == 0x4D && buf[2] == 0x0 && buf[3] == 0x2A)) { + return TIFF + } + if IsTypeSupported(GIF) && buf[0] == 0x47 && buf[1] == 0x49 && buf[2] == 0x46 { + return GIF + } + if IsTypeSupported(PDF) && buf[0] == 0x25 && buf[1] == 0x50 && buf[2] == 0x44 && buf[3] == 0x46 { + return PDF + } + if IsTypeSupported(SVG) && IsSVGImage(buf) { + return SVG + } + if IsTypeSupported(MAGICK) && strings.HasSuffix(readImageType(buf), "MagickBuffer") { + return MAGICK + } + return UNKNOWN +} + +func readImageType(buf []byte) string { + length := C.size_t(len(buf)) + imageBuf := unsafe.Pointer(&buf[0]) + load := C.vips_foreign_find_load_buffer(imageBuf, length) + return C.GoString(load) +} + +func catchVipsError() error { + s := C.GoString(C.vips_error_buffer()) + C.vips_error_clear() + C.vips_thread_shutdown() + return errors.New(s) +} + +func boolToInt(b bool) int { + if b { + return 1 + } + return 0 +} + +func vipsGaussianBlur(image *C.VipsImage, o GaussianBlur) (*C.VipsImage, error) { + var out *C.VipsImage + defer C.g_object_unref(C.gpointer(image)) + + err := C.vips_gaussblur_bridge(image, &out, C.double(o.Sigma), C.double(o.MinAmpl)) + if err != 0 { + return nil, catchVipsError() + } + return out, nil +} + +func vipsSharpen(image *C.VipsImage, o Sharpen) (*C.VipsImage, error) { + var out *C.VipsImage + defer C.g_object_unref(C.gpointer(image)) + + 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)) + if err != 0 { + return nil, catchVipsError() + } + return out, nil +} + +func max(x int) int { + return int(math.Max(float64(x), 0)) +} + +func vipsDrawWatermark(image *C.VipsImage, o WatermarkImage) (*C.VipsImage, error) { + var out *C.VipsImage + + watermark, _, e := vipsRead(o.Buf) + if e != nil { + return nil, e + } + + opts := vipsWatermarkImageOptions{C.int(o.Left), C.int(o.Top), C.float(o.Opacity)} + + err := C.vips_watermark_image(image, watermark, &out, (*C.WatermarkImageOptions)(unsafe.Pointer(&opts))) + + if err != 0 { + return nil, catchVipsError() + } + + return out, nil +} diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/vips.h b/vendor/src/gopkg.in/h2non/bimg.v1/vips.h new file mode 100644 index 00000000..6d765502 --- /dev/null +++ b/vendor/src/gopkg.in/h2non/bimg.v1/vips.h @@ -0,0 +1,532 @@ +#include +#include +#include +#include +#include + +/** + * Starting libvips 7.41, VIPS_ANGLE_x has been renamed to VIPS_ANGLE_Dx + * "to help python". So we provide the macro to correctly build for versions + * before 7.41.x. + * https://github.com/jcupitt/libvips/blob/master/ChangeLog#L128 + */ + +#if (VIPS_MAJOR_VERSION == 7 && VIPS_MINOR_VERSION < 41) +#define VIPS_ANGLE_D0 VIPS_ANGLE_0 +#define VIPS_ANGLE_D90 VIPS_ANGLE_90 +#define VIPS_ANGLE_D180 VIPS_ANGLE_180 +#define VIPS_ANGLE_D270 VIPS_ANGLE_270 +#endif + +#define EXIF_IFD0_ORIENTATION "exif-ifd0-Orientation" + +enum types { + UNKNOWN = 0, + JPEG, + WEBP, + PNG, + TIFF, + GIF, + PDF, + SVG, + MAGICK +}; + +typedef struct { + const char *Text; + const char *Font; +} WatermarkTextOptions; + +typedef struct { + int Width; + int DPI; + int Margin; + int NoReplicate; + float Opacity; + double Background[3]; +} WatermarkOptions; + +typedef struct { + int Left; + int Top; + float Opacity; +} WatermarkImageOptions; + +static unsigned long +has_profile_embed(VipsImage *image) { + return vips_image_get_typeof(image, VIPS_META_ICC_NAME); +} + +static void +remove_profile(VipsImage *image) { + vips_image_remove(image, VIPS_META_ICC_NAME); +} + +static gboolean +with_interlace(int interlace) { + return interlace > 0 ? TRUE : FALSE; +} + +static int +has_alpha_channel(VipsImage *image) { + return ( + (image->Bands == 2 && image->Type == VIPS_INTERPRETATION_B_W) || + (image->Bands == 4 && image->Type != VIPS_INTERPRETATION_CMYK) || + (image->Bands == 5 && image->Type == VIPS_INTERPRETATION_CMYK) + ) ? 1 : 0; +} + +/** + * This method is here to handle the weird initialization of the vips lib. + * libvips use a macro VIPS_INIT() that call vips__init() in version < 7.41, + * or calls vips_init() in version >= 7.41. + * + * Anyway, it's not possible to build bimg on Debian Jessie with libvips 7.40.x, + * as vips_init() is a macro to VIPS_INIT(), which is also a macro, hence, cgo + * is unable to determine the return type of vips_init(), making the build impossible. + * In order to correctly build bimg, for version < 7.41, we should undef vips_init and + * creates a vips_init() method that calls VIPS_INIT(). + */ + +#if (VIPS_MAJOR_VERSION == 7 && VIPS_MINOR_VERSION < 41) +#undef vips_init +int +vips_init(const char *argv0) +{ + return VIPS_INIT(argv0); +} +#endif + +void +vips_enable_cache_set_trace() { + vips_cache_set_trace(TRUE); +} + +int +vips_affine_interpolator(VipsImage *in, VipsImage **out, double a, double b, double c, double d, VipsInterpolate *interpolator) { + return vips_affine(in, out, a, b, c, d, "interpolate", interpolator, NULL); +} + +int +vips_jpegload_buffer_shrink(void *buf, size_t len, VipsImage **out, int shrink) { + return vips_jpegload_buffer(buf, len, out, "shrink", shrink, NULL); +} + +int +vips_flip_bridge(VipsImage *in, VipsImage **out, int direction) { + return vips_flip(in, out, direction, NULL); +} + +int +vips_shrink_bridge(VipsImage *in, VipsImage **out, double xshrink, double yshrink) { + return vips_shrink(in, out, xshrink, yshrink, NULL); +} + +int +vips_type_find_bridge(int t) { + if (t == GIF) { + return vips_type_find("VipsOperation", "gifload"); + } + if (t == PDF) { + return vips_type_find("VipsOperation", "pdfload"); + } + if (t == TIFF) { + return vips_type_find("VipsOperation", "tiffload"); + } + if (t == SVG) { + return vips_type_find("VipsOperation", "svgload"); + } + if (t == WEBP) { + return vips_type_find("VipsOperation", "webpload"); + } + if (t == PNG) { + return vips_type_find("VipsOperation", "pngload"); + } + if (t == JPEG) { + return vips_type_find("VipsOperation", "jpegload"); + } + if (t == MAGICK) { + return vips_type_find("VipsOperation", "magickload"); + } + return 0; +} + +int +vips_type_find_save_bridge(int t) { + if (t == TIFF) { + return vips_type_find("VipsOperation", "tiffsave_buffer"); + } + if (t == WEBP) { + return vips_type_find("VipsOperation", "webpsave_buffer"); + } + if (t == PNG) { + return vips_type_find("VipsOperation", "pngsave_buffer"); + } + if (t == JPEG) { + return vips_type_find("VipsOperation", "jpegsave_buffer"); + } + return 0; +} + +int +vips_rotate(VipsImage *in, VipsImage **out, int angle) { + int rotate = VIPS_ANGLE_D0; + + angle %= 360; + + if (angle == 45) { + rotate = VIPS_ANGLE45_D45; + } else if (angle == 90) { + rotate = VIPS_ANGLE_D90; + } else if (angle == 135) { + rotate = VIPS_ANGLE45_D135; + } else if (angle == 180) { + rotate = VIPS_ANGLE_D180; + } else if (angle == 225) { + rotate = VIPS_ANGLE45_D225; + } else if (angle == 270) { + rotate = VIPS_ANGLE_D270; + } else if (angle == 315) { + rotate = VIPS_ANGLE45_D315; + } else { + angle = 0; + } + + if (angle > 0 && angle % 90 != 0) { + return vips_rot45(in, out, "angle", rotate, NULL); + } else { + return vips_rot(in, out, rotate, NULL); + } +} + +int +vips_exif_orientation(VipsImage *image) { + int orientation = 0; + const char *exif; + if ( + vips_image_get_typeof(image, EXIF_IFD0_ORIENTATION) != 0 && + !vips_image_get_string(image, EXIF_IFD0_ORIENTATION, &exif) + ) { + orientation = atoi(&exif[0]); + } + return orientation; +} + +int +interpolator_window_size(char const *name) { + VipsInterpolate *interpolator = vips_interpolate_new(name); + int window_size = vips_interpolate_get_window_size(interpolator); + g_object_unref(interpolator); + return window_size; +} + +const char * +vips_enum_nick_bridge(VipsImage *image) { + return vips_enum_nick(VIPS_TYPE_INTERPRETATION, image->Type); +} + +int +vips_zoom_bridge(VipsImage *in, VipsImage **out, int xfac, int yfac) { + return vips_zoom(in, out, xfac, yfac, NULL); +} + +int +vips_embed_bridge(VipsImage *in, VipsImage **out, int left, int top, int width, int height, int extend, double r, double g, double b) { + if (extend == VIPS_EXTEND_BACKGROUND) { + double background[3] = {r, g, b}; + VipsArrayDouble *vipsBackground = vips_array_double_new(background, 3); + return vips_embed(in, out, left, top, width, height, "extend", extend, "background", vipsBackground, NULL); + } + return vips_embed(in, out, left, top, width, height, "extend", extend, NULL); +} + +int +vips_extract_area_bridge(VipsImage *in, VipsImage **out, int left, int top, int width, int height) { + return vips_extract_area(in, out, left, top, width, height, NULL); +} + +int +vips_colourspace_issupported_bridge(VipsImage *in) { + return vips_colourspace_issupported(in) ? 1 : 0; +} + +VipsInterpretation +vips_image_guess_interpretation_bridge(VipsImage *in) { + return vips_image_guess_interpretation(in); +} + +int +vips_colourspace_bridge(VipsImage *in, VipsImage **out, VipsInterpretation space) { + return vips_colourspace(in, out, space, NULL); +} + +int +vips_jpegsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality, int interlace) { + return vips_jpegsave_buffer(in, buf, len, + "strip", strip, + "Q", quality, + "optimize_coding", TRUE, + "interlace", with_interlace(interlace), + NULL + ); +} + +int +vips_pngsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int compression, int quality, int interlace) { +#if (VIPS_MAJOR_VERSION >= 8 || (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 42)) + return vips_pngsave_buffer(in, buf, len, + "strip", FALSE, + "compression", compression, + "interlace", with_interlace(interlace), + "filter", VIPS_FOREIGN_PNG_FILTER_NONE, + NULL + ); +#else + return vips_pngsave_buffer(in, buf, len, + "strip", FALSE, + "compression", compression, + "interlace", with_interlace(interlace), + NULL + ); +#endif +} + +int +vips_webpsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality) { + return vips_webpsave_buffer(in, buf, len, + "strip", strip, + "Q", quality, + NULL + ); +} + +int +vips_tiffsave_bridge(VipsImage *in, void **buf, size_t *len) { +#if (VIPS_MAJOR_VERSION >= 8 && VIPS_MINOR_VERSION >= 5) + return vips_tiffsave_buffer(in, buf, len, NULL); +#else + return 0; +#endif +} + +int +vips_is_16bit (VipsInterpretation interpretation) { + return interpretation == VIPS_INTERPRETATION_RGB16 || interpretation == VIPS_INTERPRETATION_GREY16; +} + +int +vips_flatten_background_brigde(VipsImage *in, VipsImage **out, double r, double g, double b) { + if (vips_is_16bit(in->Type)) { + r = 65535 * r / 255; + g = 65535 * g / 255; + b = 65535 * b / 255; + } + + double background[3] = {r, g, b}; + VipsArrayDouble *vipsBackground = vips_array_double_new(background, 3); + + return vips_flatten(in, out, + "background", vipsBackground, + "max_alpha", vips_is_16bit(in->Type) ? 65535.0 : 255.0, + NULL + ); +} + +int +vips_init_image (void *buf, size_t len, int imageType, VipsImage **out) { + int code = 1; + + if (imageType == JPEG) { + code = vips_jpegload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); + } else if (imageType == PNG) { + code = vips_pngload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); + } else if (imageType == WEBP) { + code = vips_webpload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); + } else if (imageType == TIFF) { + code = vips_tiffload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); +#if (VIPS_MAJOR_VERSION >= 8) +#if (VIPS_MINOR_VERSION >= 3) + } else if (imageType == GIF) { + code = vips_gifload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); + } else if (imageType == PDF) { + code = vips_pdfload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); + } else if (imageType == SVG) { + code = vips_svgload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); +#endif + } else if (imageType == MAGICK) { + code = vips_magickload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); +#endif + } + + return code; +} + +int +vips_watermark_replicate (VipsImage *orig, VipsImage *in, VipsImage **out) { + VipsImage *cache = vips_image_new(); + + if ( + vips_replicate(in, &cache, + 1 + orig->Xsize / in->Xsize, + 1 + orig->Ysize / in->Ysize, NULL) || + vips_crop(cache, out, 0, 0, orig->Xsize, orig->Ysize, NULL) + ) { + g_object_unref(cache); + return 1; + } + + g_object_unref(cache); + return 0; +} + +int +vips_watermark(VipsImage *in, VipsImage **out, WatermarkTextOptions *to, WatermarkOptions *o) { + double ones[3] = { 1, 1, 1 }; + + VipsImage *base = vips_image_new(); + VipsImage **t = (VipsImage **) vips_object_local_array(VIPS_OBJECT(base), 10); + t[0] = in; + + // Make the mask. + if ( + vips_text(&t[1], to->Text, + "width", o->Width, + "dpi", o->DPI, + "font", to->Font, + NULL) || + vips_linear1(t[1], &t[2], o->Opacity, 0.0, NULL) || + vips_cast(t[2], &t[3], VIPS_FORMAT_UCHAR, NULL) || + vips_embed(t[3], &t[4], 100, 100, t[3]->Xsize + o->Margin, t[3]->Ysize + o->Margin, NULL) + ) { + g_object_unref(base); + return 1; + } + + // Replicate if necessary + if (o->NoReplicate != 1) { + VipsImage *cache = vips_image_new(); + if (vips_watermark_replicate(t[0], t[4], &cache)) { + g_object_unref(cache); + g_object_unref(base); + return 1; + } + g_object_unref(t[4]); + t[4] = cache; + } + + // Make the constant image to paint the text with. + if ( + vips_black(&t[5], 1, 1, NULL) || + vips_linear(t[5], &t[6], ones, o->Background, 3, NULL) || + vips_cast(t[6], &t[7], VIPS_FORMAT_UCHAR, NULL) || + vips_copy(t[7], &t[8], "interpretation", t[0]->Type, NULL) || + vips_embed(t[8], &t[9], 0, 0, t[0]->Xsize, t[0]->Ysize, "extend", VIPS_EXTEND_COPY, NULL) + ) { + g_object_unref(base); + return 1; + } + + // Blend the mask and text and write to output. + if (vips_ifthenelse(t[4], t[9], t[0], out, "blend", TRUE, NULL)) { + g_object_unref(base); + return 1; + } + + g_object_unref(base); + return 0; +} + +int +vips_gaussblur_bridge(VipsImage *in, VipsImage **out, double sigma, double min_ampl) { +#if (VIPS_MAJOR_VERSION == 7 && VIPS_MINOR_VERSION < 41) + return vips_gaussblur(in, out, (int) sigma, NULL); +#else + return vips_gaussblur(in, out, sigma, NULL, "min_ampl", min_ampl, NULL); +#endif +} + +int +vips_sharpen_bridge(VipsImage *in, VipsImage **out, int radius, double x1, double y2, double y3, double m1, double m2) { +#if (VIPS_MAJOR_VERSION == 7 && VIPS_MINOR_VERSION < 41) + return vips_sharpen(in, out, radius, x1, y2, y3, m1, m2, NULL); +#else + return vips_sharpen(in, out, "radius", radius, "x1", x1, "y2", y2, "y3", y3, "m1", m1, "m2", m2, NULL); +#endif +} + +int +vips_add_band(VipsImage *in, VipsImage **out, double c) { +#if (VIPS_MAJOR_VERSION > 8 || (VIPS_MAJOR_VERSION >= 8 && VIPS_MINOR_VERSION >= 2)) + return vips_bandjoin_const1(in, out, c, NULL); +#else + VipsImage *base = vips_image_new(); + if ( + vips_black(&base, in->Xsize, in->Ysize, NULL) || + vips_linear1(base, &base, 1, c, NULL)) { + g_object_unref(base); + return 1; + } + g_object_unref(base); + return vips_bandjoin2(in, base, out, c, NULL); +#endif +} + +int +vips_watermark_image(VipsImage *in, VipsImage *sub, VipsImage **out, WatermarkImageOptions *o) { + VipsImage *base = vips_image_new(); + VipsImage **t = (VipsImage **) vips_object_local_array(VIPS_OBJECT(base), 10); + + // add in and sub for unreffing and later use + t[0] = in; + t[1] = sub; + + if (has_alpha_channel(in) == 0) { + vips_add_band(in, &t[0], 255.0); + // in is no longer in the array and won't be unreffed, so add it at the end + t[8] = in; + } + + if (has_alpha_channel(sub) == 0) { + vips_add_band(sub, &t[1], 255.0); + // sub is no longer in the array and won't be unreffed, so add it at the end + t[9] = sub; + } + + // Place watermark image in the right place and size it to the size of the + // image that should be watermarked + if ( + vips_embed(t[1], &t[2], o->Left, o->Top, t[0]->Xsize, t[0]->Ysize, NULL)) { + g_object_unref(base); + return 1; + } + + // Create a mask image based on the alpha band from the watermark image + // and place it in the right position + if ( + vips_extract_band(t[1], &t[3], t[1]->Bands - 1, "n", 1, NULL) || + vips_linear1(t[3], &t[4], o->Opacity, 0.0, NULL) || + vips_cast(t[4], &t[5], VIPS_FORMAT_UCHAR, NULL) || + vips_copy(t[5], &t[6], "interpretation", t[0]->Type, NULL) || + vips_embed(t[6], &t[7], o->Left, o->Top, t[0]->Xsize, t[0]->Ysize, NULL)) { + g_object_unref(base); + return 1; + } + + // Blend the mask and watermark image and write to output. + if (vips_ifthenelse(t[7], t[2], t[0], out, "blend", TRUE, NULL)) { + g_object_unref(base); + return 1; + } + + g_object_unref(base); + return 0; +} + +int +vips_smartcrop_bridge(VipsImage *in, VipsImage **out, int width, int height) { +#if (VIPS_MAJOR_VERSION >= 8 && VIPS_MINOR_VERSION >= 5) + return vips_smartcrop(in, out, width, height, NULL); +#else + return 0; +#endif +} diff --git a/vendor/src/gopkg.in/h2non/bimg.v1/vips_test.go b/vendor/src/gopkg.in/h2non/bimg.v1/vips_test.go new file mode 100644 index 00000000..47b3f414 --- /dev/null +++ b/vendor/src/gopkg.in/h2non/bimg.v1/vips_test.go @@ -0,0 +1,163 @@ +package bimg + +import ( + "io/ioutil" + "os" + "path" + "testing" +) + +func TestVipsRead(t *testing.T) { + files := []struct { + name string + expected ImageType + }{ + {"test.jpg", JPEG}, + {"test.png", PNG}, + {"test.webp", WEBP}, + } + + for _, file := range files { + image, imageType, _ := vipsRead(readImage(file.name)) + if image == nil { + t.Fatal("Empty image") + } + if imageType != file.expected { + t.Fatal("Invalid image type") + } + } +} + +func TestVipsSave(t *testing.T) { + types := [...]ImageType{JPEG, PNG, WEBP} + + for _, typ := range types { + image, _, _ := vipsRead(readImage("test.jpg")) + options := vipsSaveOptions{Quality: 95, Type: typ} + + buf, err := vipsSave(image, options) + if err != nil { + t.Fatalf("Cannot save the image as '%v'", ImageTypes[typ]) + } + if len(buf) == 0 { + t.Fatalf("Empty saved '%v' image", ImageTypes[typ]) + } + } +} + +func TestVipsSaveTiff(t *testing.T) { + if !IsTypeSupportedSave(TIFF) { + t.Skipf("Format %#v is not supported", ImageTypes[TIFF]) + } + image, _, _ := vipsRead(readImage("test.jpg")) + options := vipsSaveOptions{Quality: 95, Type: TIFF} + buf, _ := vipsSave(image, options) + + if len(buf) == 0 { + t.Fatalf("Empty saved '%v' image", ImageTypes[TIFF]) + } +} + +func TestVipsRotate(t *testing.T) { + files := []struct { + name string + rotate Angle + }{ + {"test.jpg", D90}, + {"test_square.jpg", D45}, + } + + for _, file := range files { + image, _, _ := vipsRead(readImage(file.name)) + + newImg, err := vipsRotate(image, file.rotate) + if err != nil { + t.Fatal("Cannot rotate the image") + } + + buf, _ := vipsSave(newImg, vipsSaveOptions{Quality: 95}) + if len(buf) == 0 { + t.Fatal("Empty image") + } + } +} + +func TestVipsZoom(t *testing.T) { + image, _, _ := vipsRead(readImage("test.jpg")) + + newImg, err := vipsZoom(image, 1) + if err != nil { + t.Fatal("Cannot save the image") + } + + buf, _ := vipsSave(newImg, vipsSaveOptions{Quality: 95}) + if len(buf) == 0 { + t.Fatal("Empty image") + } +} + +func TestVipsWatermark(t *testing.T) { + image, _, _ := vipsRead(readImage("test.jpg")) + + watermark := Watermark{ + Text: "Copy me if you can", + Font: "sans bold 12", + Opacity: 0.5, + Width: 200, + DPI: 100, + Margin: 100, + Background: Color{255, 255, 255}, + } + + newImg, err := vipsWatermark(image, watermark) + if err != nil { + t.Errorf("Cannot add watermark: %s", err) + } + + buf, _ := vipsSave(newImg, vipsSaveOptions{Quality: 95}) + if len(buf) == 0 { + t.Fatal("Empty image") + } +} + +func TestVipsWatermarkWithImage(t *testing.T) { + image, _, _ := vipsRead(readImage("test.jpg")) + + watermark := readImage("transparent.png") + + options := WatermarkImage{Left: 100, Top: 100, Opacity: 1.0, Buf: watermark} + newImg, err := vipsDrawWatermark(image, options) + if err != nil { + t.Errorf("Cannot add watermark: %s", err) + } + + buf, _ := vipsSave(newImg, vipsSaveOptions{Quality: 95}) + if len(buf) == 0 { + t.Fatal("Empty image") + } +} + +func TestVipsImageType(t *testing.T) { + imgType := vipsImageType(readImage("test.jpg")) + if imgType != JPEG { + t.Fatal("Invalid image type") + } +} + +func TestVipsMemory(t *testing.T) { + mem := VipsMemory() + + if mem.Memory < 1024 { + t.Fatal("Invalid memory") + } + if mem.Allocations == 0 { + t.Fatal("Invalid memory allocations") + } +} + +func readImage(file string) []byte { + img, _ := os.Open(path.Join("fixtures", file)) + buf, _ := ioutil.ReadAll(img) + defer img.Close() + return buf +}