mediaapi: Add thumbnail support (#132)
* vendor: Add bimg image processing library bimg is MIT licensed. It depends on the C library libvips which is LGPL v2.1+ licensed. libvips must be installed separately. * mediaapi: Add YAML config file support * mediaapi: Add thumbnail support * mediaapi: Add missing thumbnail files * travis: Add ppa and install libvips-dev * travis: Another ppa and install libvips-dev attempt * travis: Add sudo: required for sudo apt* usage * mediaapi/thumbnailer: Make comparison code more readable * mediaapi: Simplify logging of thumbnail properties * mediaapi/thumbnailer: Rename metrics to fitness Metrics is used in the context of monitoring with Prometheus so renaming to avoid confusion. * mediaapi/thumbnailer: Use math.Inf() for max aspect and size * mediaapi/thumbnailer: Limit number of parallel generators Fall back to selecting from already-/pre-generated thumbnails or serving the original. * mediaapi/thumbnailer: Split bimg code into separate file * vendor: Add github.com/nfnt/resize pure go image scaler * mediaapi/thumbnailer: Add nfnt/resize thumbnailer * travis: Don't install libvips-dev via ppa * mediaapi: Add notes to README about resizers * mediaapi: Elaborate on scaling libs in READMEmain
|
@ -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
|
|
@ -15,10 +15,14 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/common"
|
"github.com/matrix-org/dendrite/common"
|
||||||
"github.com/matrix-org/dendrite/mediaapi/config"
|
"github.com/matrix-org/dendrite/mediaapi/config"
|
||||||
|
@ -28,6 +32,7 @@ import (
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
|
||||||
log "github.com/Sirupsen/logrus"
|
log "github.com/Sirupsen/logrus"
|
||||||
|
yaml "gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -38,36 +43,25 @@ var (
|
||||||
basePath = os.Getenv("BASE_PATH")
|
basePath = os.Getenv("BASE_PATH")
|
||||||
// Note: if the MAX_FILE_SIZE_BYTES is set to 0, it will be unlimited
|
// Note: if the MAX_FILE_SIZE_BYTES is set to 0, it will be unlimited
|
||||||
maxFileSizeBytesString = os.Getenv("MAX_FILE_SIZE_BYTES")
|
maxFileSizeBytesString = os.Getenv("MAX_FILE_SIZE_BYTES")
|
||||||
|
configPath = os.Getenv("CONFIG_PATH")
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
common.SetupLogging(logDir)
|
common.SetupLogging(logDir)
|
||||||
|
|
||||||
if bindAddr == "" {
|
log.WithFields(log.Fields{
|
||||||
log.Panic("No BIND_ADDRESS environment variable found.")
|
"BIND_ADDRESS": bindAddr,
|
||||||
}
|
"DATABASE": dataSource,
|
||||||
if basePath == "" {
|
"LOG_DIR": logDir,
|
||||||
log.Panic("No BASE_PATH environment variable found.")
|
"SERVER_NAME": serverName,
|
||||||
}
|
"BASE_PATH": basePath,
|
||||||
absBasePath, err := filepath.Abs(basePath)
|
"MAX_FILE_SIZE_BYTES": maxFileSizeBytesString,
|
||||||
if err != nil {
|
"CONFIG_PATH": configPath,
|
||||||
log.WithError(err).WithField("BASE_PATH", basePath).Panic("BASE_PATH is invalid (must be able to make absolute)")
|
}).Info("Loading configuration based on config file and environment variables")
|
||||||
}
|
|
||||||
|
|
||||||
if serverName == "" {
|
cfg, err := configureServer()
|
||||||
serverName = "localhost"
|
|
||||||
}
|
|
||||||
maxFileSizeBytes, err := strconv.ParseInt(maxFileSizeBytesString, 10, 64)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
maxFileSizeBytes = 10 * 1024 * 1024
|
log.WithError(err).Fatal("Invalid configuration")
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := storage.Open(cfg.DataSource)
|
db, err := storage.Open(cfg.DataSource)
|
||||||
|
@ -76,14 +70,182 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"BASE_PATH": absBasePath,
|
|
||||||
"BIND_ADDRESS": bindAddr,
|
"BIND_ADDRESS": bindAddr,
|
||||||
"DATABASE": dataSource,
|
|
||||||
"LOG_DIR": logDir,
|
"LOG_DIR": logDir,
|
||||||
"MAX_FILE_SIZE_BYTES": maxFileSizeBytes,
|
"CONFIG_PATH": configPath,
|
||||||
"SERVER_NAME": serverName,
|
"ServerName": cfg.ServerName,
|
||||||
}).Info("Starting mediaapi")
|
"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)
|
routing.Setup(http.DefaultServeMux, http.DefaultClient, cfg, db)
|
||||||
log.Fatal(http.ListenAndServe(bindAddr, nil))
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
After Width: | Height: | Size: 4.1 KiB |
|
@ -23,12 +23,20 @@ import (
|
||||||
type MediaAPI struct {
|
type MediaAPI struct {
|
||||||
// The name of the server. This is usually the domain name, e.g 'matrix.org', 'localhost'.
|
// The name of the server. This is usually the domain name, e.g 'matrix.org', 'localhost'.
|
||||||
ServerName gomatrixserverlib.ServerName `yaml:"server_name"`
|
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.
|
// 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.
|
// 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 MaxFileSizeBytes is set to 0, the size is unlimited.
|
||||||
// Note: if max_file_size_bytes is not set, it will default to 10485760 (10MB)
|
// 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
|
// The postgres connection config for connecting to the database e.g a postgres:// URI
|
||||||
DataSource string `yaml:"database"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|
After Width: | Height: | Size: 4.8 KiB |
|
@ -35,16 +35,32 @@ const pathPrefixR0 = "/_matrix/media/v1"
|
||||||
func Setup(servMux *http.ServeMux, httpClient *http.Client, cfg *config.MediaAPI, db *storage.Database) {
|
func Setup(servMux *http.ServeMux, httpClient *http.Client, cfg *config.MediaAPI, db *storage.Database) {
|
||||||
apiMux := mux.NewRouter()
|
apiMux := mux.NewRouter()
|
||||||
r0mux := apiMux.PathPrefix(pathPrefixR0).Subrouter()
|
r0mux := apiMux.PathPrefix(pathPrefixR0).Subrouter()
|
||||||
|
|
||||||
|
activeThumbnailGeneration := &types.ActiveThumbnailGeneration{
|
||||||
|
PathToResult: map[string]*types.ThumbnailGenerationResult{},
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: /upload should use common.MakeAuthAPI()
|
// FIXME: /upload should use common.MakeAuthAPI()
|
||||||
r0mux.Handle("/upload", common.MakeAPI("upload", func(req *http.Request) util.JSONResponse {
|
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{
|
activeRemoteRequests := &types.ActiveRemoteRequests{
|
||||||
MXCToResult: map[string]*types.RemoteRequestResult{},
|
MXCToResult: map[string]*types.RemoteRequestResult{},
|
||||||
}
|
}
|
||||||
r0mux.Handle("/download/{serverName}/{mediaId}",
|
r0mux.Handle("/download/{serverName}/{mediaId}",
|
||||||
prometheus.InstrumentHandler("download", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
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)
|
req = util.RequestWithLogging(req)
|
||||||
|
|
||||||
// Set common headers returned regardless of the outcome of the request
|
// Set common headers returned regardless of the outcome of the request
|
||||||
|
@ -53,10 +69,6 @@ func Setup(servMux *http.ServeMux, httpClient *http.Client, cfg *config.MediaAPI
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
vars := mux.Vars(req)
|
vars := mux.Vars(req)
|
||||||
writers.Download(w, req, gomatrixserverlib.ServerName(vars["serverName"]), types.MediaID(vars["mediaId"]), cfg, db, activeRemoteRequests)
|
writers.Download(w, req, gomatrixserverlib.ServerName(vars["serverName"]), types.MediaID(vars["mediaId"]), cfg, db, activeRemoteRequests, activeThumbnailGeneration, name == "thumbnail")
|
||||||
})),
|
}))
|
||||||
)
|
|
||||||
|
|
||||||
servMux.Handle("/metrics", prometheus.Handler())
|
|
||||||
servMux.Handle("/api/", http.StripPrefix("/api", apiMux))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,13 +19,17 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type statements struct {
|
type statements struct {
|
||||||
mediaStatements
|
media mediaStatements
|
||||||
|
thumbnail thumbnailStatements
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *statements) prepare(db *sql.DB) error {
|
func (s *statements) prepare(db *sql.DB) error {
|
||||||
var err 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,16 +45,44 @@ func Open(dataSourceName string) (*Database, error) {
|
||||||
// StoreMediaMetadata inserts the metadata about the uploaded media into the database.
|
// 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.
|
// Returns an error if the combination of MediaID and Origin are not unique in the table.
|
||||||
func (d *Database) StoreMediaMetadata(mediaMetadata *types.MediaMetadata) error {
|
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.
|
// 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.
|
// 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.
|
// 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) {
|
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 {
|
if err != nil && err == sql.ErrNoRows {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return mediaMetadata, err
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -77,3 +77,37 @@ type ActiveRemoteRequests struct {
|
||||||
// The string key is an mxc:// URL
|
// The string key is an mxc:// URL
|
||||||
MXCToResult map[string]*RemoteRequestResult
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
log "github.com/Sirupsen/logrus"
|
log "github.com/Sirupsen/logrus"
|
||||||
|
@ -31,6 +32,7 @@ import (
|
||||||
"github.com/matrix-org/dendrite/mediaapi/config"
|
"github.com/matrix-org/dendrite/mediaapi/config"
|
||||||
"github.com/matrix-org/dendrite/mediaapi/fileutils"
|
"github.com/matrix-org/dendrite/mediaapi/fileutils"
|
||||||
"github.com/matrix-org/dendrite/mediaapi/storage"
|
"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/dendrite/mediaapi/types"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/util"
|
"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
|
// Note: unfortunately regex.MustCompile() cannot be assigned to a const
|
||||||
var mediaIDRegex = regexp.MustCompile("[" + mediaIDCharacters + "]+")
|
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
|
// 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 {
|
type downloadRequest struct {
|
||||||
MediaMetadata *types.MediaMetadata
|
MediaMetadata *types.MediaMetadata
|
||||||
|
IsThumbnailRequest bool
|
||||||
|
ThumbnailSize types.ThumbnailSize
|
||||||
Logger *log.Entry
|
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 this server (i.e. origin == cfg.ServerName) are served directly
|
||||||
// Files from remote servers (i.e. origin != cfg.ServerName) are cached locally.
|
// 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 present in the cache, they are served directly.
|
||||||
// If they are not present in the cache, they are obtained from the remote server and
|
// 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.
|
// 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{
|
r := &downloadRequest{
|
||||||
MediaMetadata: &types.MediaMetadata{
|
MediaMetadata: &types.MediaMetadata{
|
||||||
MediaID: mediaID,
|
MediaID: mediaID,
|
||||||
Origin: origin,
|
Origin: origin,
|
||||||
},
|
},
|
||||||
|
IsThumbnailRequest: isThumbnailRequest,
|
||||||
Logger: util.GetLogger(req.Context()).WithFields(log.Fields{
|
Logger: util.GetLogger(req.Context()).WithFields(log.Fields{
|
||||||
"Origin": origin,
|
"Origin": origin,
|
||||||
"MediaID": mediaID,
|
"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
|
// request validation
|
||||||
if req.Method != "GET" {
|
if req.Method != "GET" {
|
||||||
r.jsonErrorResponse(w, util.JSONResponse{
|
r.jsonErrorResponse(w, util.JSONResponse{
|
||||||
|
@ -80,7 +107,7 @@ func Download(w http.ResponseWriter, req *http.Request, origin gomatrixserverlib
|
||||||
return
|
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)
|
r.jsonErrorResponse(w, *resErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -118,10 +145,29 @@ func (r *downloadRequest) Validate() *util.JSONResponse {
|
||||||
JSON: jsonerror.NotFound("serverName must be a non-empty string"),
|
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
|
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
|
// check if we have a record of the media in our database
|
||||||
mediaMetadata, err := db.GetMediaMetadata(r.MediaMetadata.MediaID, r.MediaMetadata.Origin)
|
mediaMetadata, err := db.GetMediaMetadata(r.MediaMetadata.MediaID, r.MediaMetadata.Origin)
|
||||||
if err != nil {
|
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
|
// 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 {
|
if resErr != nil {
|
||||||
return resErr
|
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
|
// If we have a record, we can respond from the local file
|
||||||
r.MediaMetadata = mediaMetadata
|
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
|
// respondFromLocalFile reads a file from local storage and writes it to the http.ResponseWriter
|
||||||
// Returns a util.JSONResponse error in case of error
|
// 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)
|
filePath, err := fileutils.GetPathFromBase64Hash(r.MediaMetadata.Base64Hash, absBasePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.Logger.WithError(err).Error("Failed to get file path from metadata")
|
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
|
return &resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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{
|
r.Logger.WithFields(log.Fields{
|
||||||
"UploadName": r.MediaMetadata.UploadName,
|
"UploadName": r.MediaMetadata.UploadName,
|
||||||
"Base64Hash": r.MediaMetadata.Base64Hash,
|
"Base64Hash": r.MediaMetadata.Base64Hash,
|
||||||
"FileSizeBytes": r.MediaMetadata.FileSizeBytes,
|
"FileSizeBytes": r.MediaMetadata.FileSizeBytes,
|
||||||
"Content-Type": r.MediaMetadata.ContentType,
|
"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")
|
}).Info("Responding with file")
|
||||||
|
responseFile = file
|
||||||
|
responseMetadata = r.MediaMetadata
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", string(r.MediaMetadata.ContentType))
|
w.Header().Set("Content-Type", string(responseMetadata.ContentType))
|
||||||
w.Header().Set("Content-Length", strconv.FormatInt(int64(r.MediaMetadata.FileSizeBytes), 10))
|
w.Header().Set("Content-Length", strconv.FormatInt(int64(responseMetadata.FileSizeBytes), 10))
|
||||||
contentSecurityPolicy := "default-src 'none';" +
|
contentSecurityPolicy := "default-src 'none';" +
|
||||||
" script-src 'none';" +
|
" script-src 'none';" +
|
||||||
" plugin-types application/pdf;" +
|
" plugin-types application/pdf;" +
|
||||||
|
@ -197,7 +271,7 @@ func (r *downloadRequest) respondFromLocalFile(w http.ResponseWriter, absBasePat
|
||||||
" object-src 'self';"
|
" object-src 'self';"
|
||||||
w.Header().Set("Content-Security-Policy", contentSecurityPolicy)
|
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")
|
r.Logger.WithError(err).Warn("Failed to copy from cache")
|
||||||
if bytesResponded == 0 {
|
if bytesResponded == 0 {
|
||||||
resErr := jsonerror.InternalServerError()
|
resErr := jsonerror.InternalServerError()
|
||||||
|
@ -209,12 +283,107 @@ func (r *downloadRequest) respondFromLocalFile(w http.ResponseWriter, absBasePat
|
||||||
return nil
|
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
|
// 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,
|
// 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.
|
// 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.
|
// 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
|
// 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
|
// Note: getMediaMetadataFromActiveRequest uses mutexes and conditions from activeRemoteRequests
|
||||||
mediaMetadata, resErr := r.getMediaMetadataFromActiveRequest(activeRemoteRequests)
|
mediaMetadata, resErr := r.getMediaMetadataFromActiveRequest(activeRemoteRequests)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
|
@ -245,7 +414,7 @@ func (r *downloadRequest) getRemoteFile(cfg *config.MediaAPI, db *storage.Databa
|
||||||
|
|
||||||
if mediaMetadata == nil {
|
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
|
// 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 {
|
if resErr != nil {
|
||||||
return resErr
|
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
|
// 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)
|
finalPath, duplicate, resErr := r.fetchRemoteFile(absBasePath, maxFileSizeBytes)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return resErr
|
return resErr
|
||||||
|
@ -317,7 +486,7 @@ func (r *downloadRequest) fetchRemoteFileAndStoreMetadata(absBasePath types.Path
|
||||||
"Base64Hash": r.MediaMetadata.Base64Hash,
|
"Base64Hash": r.MediaMetadata.Base64Hash,
|
||||||
"UploadName": r.MediaMetadata.UploadName,
|
"UploadName": r.MediaMetadata.UploadName,
|
||||||
"FileSizeBytes": r.MediaMetadata.FileSizeBytes,
|
"FileSizeBytes": r.MediaMetadata.FileSizeBytes,
|
||||||
"Content-Type": r.MediaMetadata.ContentType,
|
"ContentType": r.MediaMetadata.ContentType,
|
||||||
}).Info("Storing file metadata to media repository database")
|
}).Info("Storing file metadata to media repository database")
|
||||||
|
|
||||||
// FIXME: timeout db request
|
// FIXME: timeout db request
|
||||||
|
@ -335,13 +504,21 @@ func (r *downloadRequest) fetchRemoteFileAndStoreMetadata(absBasePath types.Path
|
||||||
return &resErr
|
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{
|
r.Logger.WithFields(log.Fields{
|
||||||
"UploadName": r.MediaMetadata.UploadName,
|
"UploadName": r.MediaMetadata.UploadName,
|
||||||
"Base64Hash": r.MediaMetadata.Base64Hash,
|
"Base64Hash": r.MediaMetadata.Base64Hash,
|
||||||
"FileSizeBytes": r.MediaMetadata.FileSizeBytes,
|
"FileSizeBytes": r.MediaMetadata.FileSizeBytes,
|
||||||
"Content-Type": r.MediaMetadata.ContentType,
|
"ContentType": r.MediaMetadata.ContentType,
|
||||||
}).Infof("Remote file cached")
|
}).Infof("Remote file cached")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -27,6 +27,7 @@ import (
|
||||||
"github.com/matrix-org/dendrite/mediaapi/config"
|
"github.com/matrix-org/dendrite/mediaapi/config"
|
||||||
"github.com/matrix-org/dendrite/mediaapi/fileutils"
|
"github.com/matrix-org/dendrite/mediaapi/fileutils"
|
||||||
"github.com/matrix-org/dendrite/mediaapi/storage"
|
"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/dendrite/mediaapi/types"
|
||||||
"github.com/matrix-org/util"
|
"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.
|
// 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.
|
// 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.
|
// 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)
|
r, resErr := parseAndValidateRequest(req, cfg)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
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
|
return *resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,14 +90,14 @@ func parseAndValidateRequest(req *http.Request, cfg *config.MediaAPI) (*uploadRe
|
||||||
Logger: util.GetLogger(req.Context()).WithField("Origin", cfg.ServerName),
|
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 nil, resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
return r, nil
|
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{
|
r.Logger.WithFields(log.Fields{
|
||||||
"UploadName": r.MediaMetadata.UploadName,
|
"UploadName": r.MediaMetadata.UploadName,
|
||||||
"FileSizeBytes": r.MediaMetadata.FileSizeBytes,
|
"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
|
// method of deduplicating files to save storage, as well as a way to conduct
|
||||||
// integrity checks on the file data in the repository.
|
// 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.
|
// 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 {
|
if err != nil {
|
||||||
r.Logger.WithError(err).WithFields(log.Fields{
|
r.Logger.WithError(err).WithFields(log.Fields{
|
||||||
"MaxFileSizeBytes": cfg.MaxFileSizeBytes,
|
"MaxFileSizeBytes": *cfg.MaxFileSizeBytes,
|
||||||
}).Warn("Error while transferring file")
|
}).Warn("Error while transferring file")
|
||||||
fileutils.RemoveDir(tmpDir, r.Logger)
|
fileutils.RemoveDir(tmpDir, r.Logger)
|
||||||
return &util.JSONResponse{
|
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, cfg.ThumbnailSizes, activeThumbnailGeneration, cfg.MaxThumbnailGenerators); resErr != nil {
|
||||||
|
|
||||||
if resErr := r.storeFileAndMetadata(tmpDir, cfg.AbsBasePath, db); resErr != nil {
|
|
||||||
return resErr
|
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
|
// 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.
|
// 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.
|
// 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)
|
finalPath, duplicate, err := fileutils.MoveFileWithHashCheck(tmpDir, r.MediaMetadata, absBasePath, r.Logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.Logger.WithError(err).Error("Failed to move file.")
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,6 +114,12 @@
|
||||||
"branch": "master",
|
"branch": "master",
|
||||||
"path": "/pbutil"
|
"path": "/pbutil"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"importpath": "github.com/nfnt/resize",
|
||||||
|
"repository": "https://github.com/nfnt/resize",
|
||||||
|
"revision": "891127d8d1b52734debe1b3c3d7e747502b6c366",
|
||||||
|
"branch": "master"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"importpath": "github.com/pierrec/lz4",
|
"importpath": "github.com/pierrec/lz4",
|
||||||
"repository": "https://github.com/pierrec/lz4",
|
"repository": "https://github.com/pierrec/lz4",
|
||||||
|
@ -179,6 +185,12 @@
|
||||||
"revision": "61e43dc76f7ee59a82bdf3d71033dc12bea4c77d",
|
"revision": "61e43dc76f7ee59a82bdf3d71033dc12bea4c77d",
|
||||||
"branch": "master"
|
"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",
|
"importpath": "golang.org/x/crypto/bcrypt",
|
||||||
"repository": "https://go.googlesource.com/crypto",
|
"repository": "https://go.googlesource.com/crypto",
|
||||||
|
@ -225,6 +237,12 @@
|
||||||
"revision": "bfee1239d796830ca346767650cce5ba90d58c57",
|
"revision": "bfee1239d796830ca346767650cce5ba90d58c57",
|
||||||
"branch": "master"
|
"branch": "master"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"importpath": "gopkg.in/h2non/bimg.v1",
|
||||||
|
"repository": "https://gopkg.in/h2non/bimg.v1",
|
||||||
|
"revision": "45f8993550e71ee7b8001d40c681c6c9fa822357",
|
||||||
|
"branch": "master"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"importpath": "gopkg.in/yaml.v2",
|
"importpath": "gopkg.in/yaml.v2",
|
||||||
"repository": "https://gopkg.in/yaml.v2",
|
"repository": "https://gopkg.in/yaml.v2",
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
Copyright (c) 2012, Jan Schlicht <jan.schlicht@gmail.com>
|
||||||
|
|
||||||
|
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.
|
|
@ -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)
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th><img src="http://nfnt.github.com/img/rings_300_NearestNeighbor.png" /><br>Nearest-Neighbor</th>
|
||||||
|
<th><img src="http://nfnt.github.com/img/rings_300_Bilinear.png" /><br>Bilinear</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><img src="http://nfnt.github.com/img/rings_300_Bicubic.png" /><br>Bicubic</th>
|
||||||
|
<th><img src="http://nfnt.github.com/img/rings_300_MitchellNetravali.png" /><br>Mitchell-Netravali</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><img src="http://nfnt.github.com/img/rings_300_Lanczos2.png" /><br>Lanczos2</th>
|
||||||
|
<th><img src="http://nfnt.github.com/img/rings_300_Lanczos3.png" /><br>Lanczos3</th>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
### Real-Life sample
|
||||||
|
|
||||||
|
Original image
|
||||||
|
![Original](http://nfnt.github.com/img/IMG_3694_720.jpg)
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th><img src="http://nfnt.github.com/img/IMG_3694_300_NearestNeighbor.png" /><br>Nearest-Neighbor</th>
|
||||||
|
<th><img src="http://nfnt.github.com/img/IMG_3694_300_Bilinear.png" /><br>Bilinear</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><img src="http://nfnt.github.com/img/IMG_3694_300_Bicubic.png" /><br>Bicubic</th>
|
||||||
|
<th><img src="http://nfnt.github.com/img/IMG_3694_300_MitchellNetravali.png" /><br>Mitchell-Netravali</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><img src="http://nfnt.github.com/img/IMG_3694_300_Lanczos2.png" /><br>Lanczos2</th>
|
||||||
|
<th><img src="http://nfnt.github.com/img/IMG_3694_300_Lanczos3.png" /><br>Lanczos3</th>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
Copyright (c) 2012 Jan Schlicht <janschlicht@gmail.com>
|
||||||
|
Resize is released under a MIT style license.
|
|
@ -0,0 +1,438 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) 2012, Jan Schlicht <jan.schlicht@gmail.com>
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,143 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) 2012, Jan Schlicht <jan.schlicht@gmail.com>
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,318 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) 2014, Charlie Vieth <charlie.vieth@gmail.com>
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) 2014, Charlie Vieth <charlie.vieth@gmail.com>
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,614 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) 2012, Jan Schlicht <jan.schlicht@gmail.com>
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) 2012, Jan Schlicht <jan.schlicht@gmail.com>
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,227 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) 2014, Charlie Vieth <charlie.vieth@gmail.com>
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,214 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) 2014, Charlie Vieth <charlie.vieth@gmail.com>
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
test:
|
||||||
|
@go test
|
||||||
|
|
||||||
|
bench:
|
||||||
|
@go test -bench=.
|
||||||
|
|
||||||
|
.PHONY: bench test
|
|
@ -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
|
|
@ -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
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
|
@ -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.
|
|
@ -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)
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 810 KiB |
After Width: | Height: | Size: 635 KiB |
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 604 KiB |
|
@ -0,0 +1,725 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg id="svg2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 900" version="1.1">
|
||||||
|
<g id="g4" fill="none" transform="matrix(1.7656463,0,0,1.7656463,324.90716,255.00942)">
|
||||||
|
<g id="g6" stroke-width="0.17200001" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path8" d="m-122.3,84.285s0.1,1.894-0.73,1.875c-0.82-0.019-17.27-48.094-37.8-45.851,0,0,17.78-7.353,38.53,43.976z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g10" stroke-width="0.17200001" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path12" d="m-118.77,81.262s-0.55,1.816-1.32,1.517c-0.77-0.298,0.11-51.104-19.95-55.978,0,0,19.22-0.864,21.27,54.461z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g14" stroke-width="0.17200001" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path16" d="m-91.284,123.59s1.636,0.96,1.166,1.64c-0.471,0.67-49.642-12.13-59.102,6.23,0,0,3.68-18.89,57.936-7.87z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g18" stroke-width="0.17200001" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path20" d="m-94.093,133.8s1.856,0.4,1.622,1.19c-0.233,0.79-50.939,4.13-54.129,24.53,0,0-2.46-19.08,52.507-25.72z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g22" stroke-width="0.17200001" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path24" d="m-98.304,128.28s1.778,0.66,1.432,1.41-50.998-3.34-57.128,16.37c0,0,0.35-19.24,55.696-17.78z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g26" stroke-width="0.17200001" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path28" d="m-109.01,110.07s1.31,1.38,0.67,1.9-44.38-25.336-58.53-10.29c0,0,8.74-17.147,57.86,8.39z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g30" stroke-width="0.17200001" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path32" d="m-116.55,114.26s1.45,1.22,0.88,1.81c-0.58,0.59-46.97-20.148-59.32-3.6,0,0,6.74-18.023,58.44,1.79z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g34" stroke-width="0.17200001" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path36" d="m-119.15,118.34s1.6,1,1.11,1.67c-0.49,0.66-49.27-13.56-59.25,4.51,0,0,4.22-18.77,58.14-6.18z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g38" stroke-width="0.17200001" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path40" d="m-108.42,118.95s1.12,1.53,0.42,1.97c-0.7,0.43-40.77-30.818-56.73-17.71,0,0,10.87-15.884,56.31,15.74z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g42" stroke-width="0.17200001" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path44" d="m-128.2,90s0.6,1.8-0.2,2-29.4-41.8-48.6-34.2c0,0,15.2-11.8,48.8,32.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g46" stroke-width="0.17200001" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path48" d="m-127.5,96.979s0.97,1.629,0.23,1.996c-0.74,0.368-37.72-34.476-54.83-22.914,0,0,12.3-14.8,54.6,20.918z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g50" stroke-width="0.17200001" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path52" d="m-127.62,101.35s1.12,1.53,0.42,1.97c-0.7,0.43-40.77-30.818-56.73-17.713,0,0,10.87-15.881,56.31,15.743z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g54" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path56" d="m-129.83,103.06c0.5,6.05,1.49,12.62,3.23,15.74,0,0-3.6,12.4,5.2,25.6,0,0-0.4,7.2,1.2,10.4,0,0,4,8.4,8.8,9.2,3.88,0.65,12.607,3.72,22.468,5.12,0,0,17.132,14.08,13.932,26.88,0,0-0.4,16.4-4,18,0,0,11.6-11.2,2,5.6l-4.4,18.8s25.6-21.6,10-3.2l-10,26s19.6-18.4,12.4-10l-3.2,8.8s43.2-27.2,12.4,2.4c0,0,8-3.6,12.4-0.8,0,0,6.8-1.2,6,0.4,0,0-20.8,10.4-24.4,28.8,0,0,8.4-10,5.2,0.8l0.4,11.6s4-21.6,3.6,16c0,0,19.2-18,7.6,2.8v16.8s15.2-16.4,8.8-3.6c0,0,10-8.8,6,6.4,0,0-0.8,10.4,3.6-0.8,0,0,16-30.6,10-4.4,0,0-0.8,19.2,4,4.4,0,0,0.4,10.4,9.6,17.6,0,0-1.2-50.8,11.6-14.8l4,16.4s2.8-9.2,2.4-14.4l8,8s15.2-22.8,12-9.6c0,0-7.6,16-6,20.8,0,0,16.8-34.8,18-36.4,0,0-2,42.4,8.8,6.4,0,0,5.6,12,2.8,16.4,0,0,8-8,7.2-11.2,0,0,4.6-8.2,7.4,5.4,0,0,1.8,9.4,3.4,6.2,0,0,4,24,5.2,1.2,0,0,1.6-13.6-5.6-25.2,0,0,0.8-3.2-2-7.2,0,0,13.6,21.6,6.4-7.2,0,0,11.201,8,12.401,8,0,0-13.601-23.2-4.801-18.4,0,0-5.2-10.4,12.801,1.6,0,0-16.001-16,1.6-6.4,0,0,7.999,6.4,0.4-3.6,0,0-14.401-16,7.599,2,0,0,11.6,16.4,12.4,19.2,0,0-10-29.2-14.4-32,0,0,8.4-36.4,49.6-20.8,0,0,6.8,17.2,11.2-1.2,0,0,12.8-6.4,24,21.2,0,0,4-13.6,3.2-16.4,0,0,6.8,1.2,6,0,0,0,13.2,4.4,14.4,3.6,0,0,6.8,6.8,7.2,3.2,0,0,9.2,2.8,7.2-0.8,0,0,8.8,15.6,9.2,19.2l2.4-14,2,2.8s1.6-7.6,0.8-8.8,20,6.8,24.8,27.6l2,8.4s6-14.8,4.4-18.8c0,0,5.2,0.8,5.6,5.2,0,0,4-23.2-0.8-29.2,0,0,4.4-0.8,5.6,2.8v-7.2s7.2,0.8,7.2-1.6c0,0,4.4-4,6.4,0.8,0,0-12.4-35.2,6-16,0,0,7.2,10.8,3.6-8s-7.6-20.4-2.8-20.8c0,0,0.8-3.6-1.2-5.2s1.2,0,1.2,0,4.8,4-0.4-18c0,0,6.4,1.6-5.6-27.6,0,0,2.8-2.4-1.2-10.8,0,0,8,4.4,10.8,2.8,0,0-0.4-1.6-3.6-5.6,0,0-21.6-54.8-1.2-32.8,0,0,11.85,13.55,5.45-9.25,0,0-9.11-24.009-8.33-28.305l-429.55,23.015z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g58" stroke="#000" fill="#cc7226">
|
||||||
|
<path id="path60" d="m299.72,80.245c0.62,0.181,2.83,1.305,4.08,2.955,0,0,6.8,10.8,1.6-7.6,0,0-9.2-28.8-0.4-17.6,0,0,6,7.2,2.8-6.4-3.86-16.427-6.4-22.8-6.4-22.8s11.6,4.8-15.2-34.8l8.8,3.6s-19.6-39.6-41.2-44.8l-8-6s38.4-38,25.6-74.8c0,0-6.8-5.2-16.4,4,0,0-6.4,4.8-12.4,3.2,0,0-30.8,1.2-32.8,1.2s-36.8-37.2-102.4-19.6c0,0-5.2,2-9.599,0.8,0,0-18.401-16-67.201,6.8,0,0-10,2-11.6,2s-4.4,0-12.4,6.4-8.4,7.2-10.4,8.8c0,0-16.4,11.2-21.2,12,0,0-11.6,6.4-16,16.4l-3.6,1.2s-1.6,7.2-2,8.4c0,0-4.8,3.6-5.6,9.2,0,0-8.8,6-8.4,10.4,0,0-1.6,5.2-2.4,10,0,0-7.2,4.8-6.4,7.6,0,0-7.6,14-6.4,20.8,0,0-6.4-0.4-9.2,2,0,0-0.8,4.8-2.4,5.2,0,0-2.8,1.2-0.4,5.2,0,0-1.6,2.8-2,4.4,0,0,0.8,2.8-3.6,8.4,0,0-6.4,18.8-4.4,24,0,0,0.4,4.8-2.4,6.4,0,0-3.6-0.4,4.8,11.6,0,0,0.8,1.2-2.4,3.6,0,0-17.2,3.6-19.6,20,0,0-13.6,14.8-13.6,20,0,2.305,0.27,5.452,0.97,10.06,0,0-0.57,8.34,27.03,9.14s402.72-31.355,402.72-31.355z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g62" fill="#cc7226">
|
||||||
|
<path id="path64" d="m-115.6,102.6c-25-39.4-10.6,17-10.6,17,8.8,34.4,138.4-3.2,138.4-3.2s168.8-30.4,180-34.4,106.4,2.4,106.4,2.4l-5.6-16.8c-64.8-46.4-84-23.2-97.6-27.2s-11.2,5.6-14.4,6.4-42.4-24-48.8-23.2-31.74-22.951-16.8,8.8c16,34-58.4,39.2-75.2,28s7.2,18.4,7.2,18.4c18.4,20-16,3.2-16,3.2-34.4-12.8-58.4,12.8-61.6,13.6s-8,4-8.8-2.4-8.31-23.101-40,3.2c-20,16.6-33.8-5.4-33.8-5.4l-2.8,11.6z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g66" fill="#e87f3a">
|
||||||
|
<path id="path68" d="m133.51,25.346c-6.4,0.8-31.77-22.939-16.8,8.8,16.6,35.2-58.4,39.2-75.2,28-16.801-11.2,7.2,18.4,7.2,18.4,18.4,20.004-16.001,3.2-16.001,3.2-34.4-12.8-58.4,12.8-61.6,13.6s-8,4.004-8.8-2.4c-0.8-6.4-8.179-22.934-40,3.2-21.236,17.344-34.729-4.109-34.729-4.109l-3.2,10.113c-25-39.804-9.93,18.51-9.93,18.51,8.81,34.4,139.06-4.51,139.06-4.51s168.8-30.404,180-34.404,105.53,2.327,105.53,2.327l-5.53-17.309c-64.8-46.4-83.2-22.618-96.8-26.618s-11.2,5.6-14.4,6.4-42.4-24-48.8-23.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g70" fill="#ea8c4d">
|
||||||
|
<path id="path72" d="m134.82,27.091c-6.4,0.8-31.14-23.229-16.8,8.8,16.2,36.201-58.401,39.201-75.201,28.001s7.2,18.4,7.2,18.4c18.4,19.998-16,3.2-16,3.2-34.4-12.8-58.401,12.8-61.601,13.6s-8,3.998-8.8-2.4c-0.8-6.4-8.048-22.767-40,3.2-22.473,18.088-35.658-2.818-35.658-2.818l-3.6,8.616c-23.8-38.998-9.25,20.02-9.25,20.02,8.8,34.4,139.71-5.82,139.71-5.82s168.8-30.398,180-34.398,104.65,2.254,104.65,2.254l-5.45-17.818c-64.8-46.4-82.4-22.037-96-26.037s-11.2,5.6-14.4,6.401c-3.2,0.8-42.4-24.001-48.8-23.201z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g74" fill="#ec9961">
|
||||||
|
<path id="path76" d="m136.13,28.837c-6.4,0.8-31.13-23.232-16.8,8.8,16.8,37.556-58.936,38.845-75.202,28-16.8-11.2,7.2,18.4,7.2,18.4,18.4,20.003-16,3.2-16,3.2-34.4-12.8-58.4,12.803-61.6,13.603s-8,4-8.8-2.403c-0.8-6.4-7.917-22.598-40.001,3.203-23.709,18.83-36.587-1.53-36.587-1.53l-4,7.13c-21.8-36.803-8.58,21.52-8.58,21.52,8.8,34.4,140.37-7.12,140.37-7.12s168.8-30.403,180-34.403,103.78,2.182,103.78,2.182l-5.38-18.327c-64.8-46.401-81.6-21.455-95.2-25.455s-11.2,5.6-14.4,6.4-42.4-24-48.8-23.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g78" fill="#eea575">
|
||||||
|
<path id="path80" d="m137.44,30.583c-6.4,0.8-30.63-23.454-16.8,8.8,16.8,39.2-58.403,39.2-75.203,28s7.2,18.4,7.2,18.4c18.4,19.997-16,3.2-16,3.2-34.4-12.8-58.4,12.797-61.6,13.597s-8,4-8.8-2.4c-0.8-6.397-7.785-22.428-40,3.2-24.946,19.58-37.507-0.23-37.507-0.23l-4.4,5.63c-19.8-34.798-7.91,23.04-7.91,23.04,8.8,34.4,141.02-8.44,141.02-8.44s168.8-30.397,180-34.397,102.91,2.109,102.91,2.109l-5.31-18.837c-64.8-46.4-80.8-20.872-94.4-24.872s-11.2,5.6-14.4,6.4-42.4-24-48.8-23.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g82" fill="#f1b288">
|
||||||
|
<path id="path84" d="m138.75,32.328c-6.4,0.8-32.37-22.651-16.8,8.8,19.2,38.8-58.404,39.2-75.204,28s7.2,18.4,7.2,18.4c18.4,20.002-16,3.2-16,3.2-34.4-12.8-58.4,12.802-61.6,13.602s-8,4-8.8-2.4c-0.8-6.402-7.654-22.265-40,3.2-26.182,20.33-38.436,1.05-38.436,1.05l-4.8,4.15c-18-33.202-7.24,24.54-7.24,24.54,8.8,34.4,141.68-9.74,141.68-9.74s168.8-30.402,180-34.402,102.03,2.036,102.03,2.036l-5.23-19.345c-64.8-46.4-80-20.291-93.6-24.291s-11.2,5.6-14.4,6.4-42.4-24-48.8-23.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g86" fill="#f3bf9c">
|
||||||
|
<path id="path88" d="m140.06,34.073c-6.4,0.8-32.75-22.46-16.8,8.8,20.4,40.001-58.405,39.201-75.205,28.001s7.2,18.4,7.2,18.4c18.4,19.996-16,3.2-16,3.2-34.4-12.8-58.4,12.796-61.6,13.596s-8,4-8.8-2.4c-0.8-6.396-7.523-22.092-40,3.2-27.419,21.08-39.365,2.35-39.365,2.35l-5.2,2.65c-16-30.196-6.56,26.06-6.56,26.06,8.8,34.4,142.32-11.06,142.32-11.06s168.8-30.396,180-34.396,101.16,1.963,101.16,1.963l-5.16-19.854c-64.8-46.4-79.2-19.709-92.8-23.709-13.6-4.001-11.2,5.6-14.4,6.4s-42.4-24.001-48.8-23.201z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g90" fill="#f5ccb0">
|
||||||
|
<path id="path92" d="m141.36,35.819c-6.4,0.8-33.84-21.875-16.8,8.8,22,39.6-58.396,39.2-75.196,28s7.2,18.4,7.2,18.4c18.4,20.001-16,3.2-16,3.2-34.4-12.8-58.4,12.801-61.6,13.601s-8,4-8.8-2.4c-0.8-6.401-7.391-21.928-40,3.2-28.655,21.82-40.294,3.64-40.294,3.64l-5.6,1.16c-14.4-28.401-5.89,27.56-5.89,27.56,8.8,34.4,142.98-12.36,142.98-12.36s168.8-30.401,180-34.401,100.3,1.891,100.3,1.891l-5.1-20.364c-64.8-46.4-78.4-19.127-92-23.127s-11.2,5.6-14.4,6.4-42.4-24-48.8-23.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g94" fill="#f8d8c4">
|
||||||
|
<path id="path96" d="m142.67,37.565c-6.4,0.8-33.84-21.876-16.8,8.8,22,39.6-58.396,39.2-75.196,28s7.2,18.4,7.2,18.4c18.4,19.995-16,3.2-16,3.2-34.401-12.8-58.401,12.795-61.601,13.595s-8,4-8.8-2.4-7.259-21.755-40,3.2c-29.891,22.57-41.213,4.93-41.213,4.93l-6-0.33c-13.61-26.396-5.22,29.08-5.22,29.08,8.8,34.4,143.63-13.68,143.63-13.68s168.8-30.395,180-34.395,99.42,1.818,99.42,1.818l-5.01-20.873c-64.81-46.4-77.61-18.545-91.21-22.545s-11.2,5.6-14.4,6.4-42.4-24-48.8-23.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g98" fill="#fae5d7">
|
||||||
|
<path id="path100" d="m143.98,39.31c-6.4,0.8-33.45-22.087-16.8,8.8,22,40.8-58.397,39.2-75.197,28s7.2,18.4,7.2,18.4c18.4,20-16,3.2-16,3.2-34.4-12.8-58.4,12.8-61.6,13.6-3.201,0.8-8.001,4-8.801-2.4s-7.128-21.592-40,3.2c-31.127,23.31-42.142,6.22-42.142,6.22l-6.4-1.82c-13-24-4.55,30.58-4.55,30.58,8.8,34.4,144.29-14.98,144.29-14.98s168.8-30.4,180-34.4,98.55,1.746,98.55,1.746l-4.95-21.382c-64.8-46.401-76.8-17.964-90.4-21.964s-11.2,5.6-14.4,6.4-42.4-24-48.8-23.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g102" fill="#fcf2eb">
|
||||||
|
<path id="path104" d="m145.29,41.055c-6.4,0.8-32.37-22.644-16.8,8.8,21.2,42.801-58.398,39.201-75.198,28.001s7.2,18.4,7.2,18.4c18.4,20.004-16,3.2-16,3.2-34.4-12.8-58.4,12.804-61.6,13.604s-8,4-8.8-2.4-6.997-21.428-40,3.2c-32.365,24.05-43.072,7.5-43.072,7.5l-6.8-3.3c-12.8-23.204-3.87,32.09-3.87,32.09,8.8,34.4,144.94-16.29,144.94-16.29s168.8-30.4,180-34.404c11.2-4,97.67,1.674,97.67,1.674l-4.87-21.893c-64.8-46.4-76-17.381-89.6-21.381-13.6-4.001-11.2,5.6-14.4,6.4s-42.4-24.001-48.8-23.201z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g106" fill="#FFF">
|
||||||
|
<path id="path108" d="m-115.8,119.6c-12.8-22-3.2,33.6-3.2,33.6,8.8,34.4,145.6-17.6,145.6-17.6s168.8-30.4,180-34.4,96.8,1.6,96.8,1.6l-4.8-22.4c-64.8-46.4-75.2-16.8-88.8-20.8s-11.2,5.6-14.4,6.4-42.4-24-48.8-23.2-31.62-23.007-16.8,8.8c22.23,47.707-60.759,37.627-75.2,28-16.8-11.2,7.2,18.4,7.2,18.4,18.4,20-16,3.2-16,3.2-34.4-12.8-58.4,12.8-61.6,13.6s-8,4-8.8-2.4-6.865-21.256-40,3.2c-33.6,24.8-44,8.8-44,8.8l-7.2-4.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g110" fill="#000">
|
||||||
|
<path id="path112" d="m-74.2,149.6s-7.2,11.6,13.6,24.8c0,0,1.4,1.4-16.6-2.8,0,0-6.2-2-7.8-12.4,0,0-4.8-4.4-9.6-10s20.4,0.4,20.4,0.4z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g114" fill="#CCC">
|
||||||
|
<path id="path116" d="m65.8,102s17.698,26.82,17.1,31.6c-1.3,10.4-1.5,20,1.7,24,3.201,4,12.001,37.2,12.001,37.2s-0.4,1.2,11.999-36.8c0,0,11.6-16-8.4-34.4,0,0-35.2-28.8-34.4-21.6z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g118" fill="#000">
|
||||||
|
<path id="path120" d="m-54.2,176.4s11.2,7.2-3.2,38.4l6.4-2.4s-0.8,11.2-4,13.6l7.2-3.2s4.8,8,0.8,12.8c0,0,16.8,8,16,14.4,0,0,6.4-8,2.4-14.4s-11.2-2.4-10.4-20.8l-8.8,3.2s5.6-8.8,5.6-15.2l-8,2.4s15.469-26.58,4.8-28c-6-0.8-8.8-0.8-8.8-0.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g122" fill="#CCC">
|
||||||
|
<path id="path124" d="m-21.8,193.2s2.8-4.4,0-3.6-34,15.6-40,25.2c0,0,34.4-24.4,40-21.6z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g126" fill="#CCC">
|
||||||
|
<path id="path128" d="m-11.4,201.2s2.8-4.4,0-3.6-34,15.6-40,25.2c0,0,34.4-24.4,40-21.6z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g130" fill="#CCC">
|
||||||
|
<path id="path132" d="m1.8,186s2.8-4.4,0-3.6-34,15.6-40,25.2c0,0,34.4-24.4,40-21.6z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g134" fill="#CCC">
|
||||||
|
<path id="path136" d="m-21.4,229.6s0-6-2.8-5.2-38.8,18.4-44.8,28c0,0,42-25.6,47.6-22.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g138" fill="#CCC">
|
||||||
|
<path id="path140" d="m-20.2,218.8s1.2-4.8-1.6-4c-2,0-28.4,11.6-34.4,21.2,0,0,29.6-21.6,36-17.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g142" fill="#CCC">
|
||||||
|
<path id="path144" d="m-34.6,266.4-10,7.6s10.4-7.6,14-6.4c0,0-6.8,11.2-7.6,16.4,0,0,10.4-12.8,16-12.4,0,0,7.6,0.4,7.6,11.2,0,0,5.6-10.4,8.8-10,0,0,1.2,6.4,0,13.2,0,0,4-7.6,8-6,0,0,6.4-2,5.6,9.6,0,0,0,10.4-0.8,13.2,0,0,5.6-26.4,8-26.8,0,0,8-1.2,12.8,7.6,0,0-4-7.6,0.8-5.6,0,0,10.8,1.6,14,8.4,0,0-6.8-12-1.2-8.8l8,6.4s8.4,21.2,10.4,22.8c0,0-7.6-21.6-6-21.6,0,0-2-12,3.2,2.8,0,0-3.2-14,2.4-13.2s10,10.8,18.4,8.4c0,0,9.601,5.6,11.601-63.6l-124,46.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g146" fill="#000">
|
||||||
|
<path id="path148" d="m-29.8,173.6s14.8-6,54.8,0c0,0,7.2,0.4,14-8.4s33.6-16,40-14l9.601,6.4,0.8,1.2s12.399,10.4,12.799,18-14.399,55.6-24,71.6c-9.6,16-19.2,28.4-38.4,26,0,0-20.8-4-46.4,0,0,0-29.2-1.6-32-9.6s11.2-23.2,11.2-23.2,4.4-8.4,3.2-22.8-0.8-42.4-5.6-45.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g150" fill="#e5668c">
|
||||||
|
<path id="path152" d="M-7.8,175.6c8.4,18.4-21.2,83.6-21.2,83.6-2,1.6,12.66,7.65,22.8,5.2,10.946-2.64,51.2,1.6,51.2,1.6,23.6-15.6,36.4-60,36.4-60s10.401-24-7.2-27.2c-17.6-3.2-82-3.2-82-3.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g154" fill="#b23259">
|
||||||
|
<path id="path156" d="m-9.831,206.5c3.326-12.79,4.91-24.59,2.031-30.9,0,0,62.4,6.4,73.6-14.4,4.241-7.87,19.001,22.8,18.6,32.4,0,0-63,14.4-77.8,3.2l-16.431,9.7z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g158" fill="#a5264c">
|
||||||
|
<path id="path160" d="m-5.4,222.8s2,7.2-0.4,11.2c0,0-1.6,0.8-2.8,1.2,0,0,1.2,3.6,7.2,5.2,0,0,2,4.4,4.4,4.8s7.2,6,11.2,4.8,15.2-5.2,15.2-5.2,5.6-3.2,14.4,0.4c0,0,2.375-0.8,2.8-4.8,0.5-4.7,3.6-8.4,5.6-10.4s11.6-14.8,10.4-15.2-68,8-68,8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g162" stroke="#000" fill="#ff727f">
|
||||||
|
<path id="path164" d="m-9.8,174.4s-2.8,22.4,0.4,30.8,2.4,10.4,1.6,14.4,3.6,14,9.2,20l12,1.6s15.2-3.6,24.4-0.8c0,0,8.994,1.34,12.4-13.6,0,0,4.8-6.4,12-9.2s14.4-44.4,10.4-52.4-18.4-12.4-34.4,3.2-18-1.2-48,6z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g166" stroke-width="0.5" stroke="#000" fill="#FFC">
|
||||||
|
<path id="path168" d="m-8.2,249.2s-0.8-2-5.2-2.4c0,0-22.4-3.6-30.8-16,0,0-6.8-5.6-2.4,6,0,0,10.4,20.4,17.2,23.2,0,0,16.4,4,21.2-10.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g170" fill="#cc3f4c">
|
||||||
|
<path id="path172" d="m71.742,185.23c0.659-7.91,2.612-16.52,0.858-20.03-6.446-12.89-23.419-7.5-34.4,3.2-16,15.6-18-1.2-48,6,0,0-1.745,13.96-0.905,23.98,0,0,37.305-11.58,38.105-5.98,0,0,1.6-3.2,10.8-3.2s31.942-1.17,33.542-3.97z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g174" stroke-width="2" stroke="#a51926">
|
||||||
|
<path id="path176" d="m28.6,175.2s4.8,4.8,1.2,14.4c0,0-14.4,16-12.4,30"/>
|
||||||
|
</g>
|
||||||
|
<g id="g178" stroke-width="0.5" stroke="#000" fill="#FFC">
|
||||||
|
<path id="path180" d="m-19.4,260s-4.4-12.8,4.4-6l3.6,3.6c-1.2,1.6-6.8,5.6-8,2.4z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g182" stroke-width="0.5" stroke="#000" fill="#FFC">
|
||||||
|
<path id="path184" d="m-14.36,261.2s-3.52-10.24,3.52-4.8l2.88,2.88c-4.56,1.28,0,3.84-6.4,1.92z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g186" stroke-width="0.5" stroke="#000" fill="#FFC">
|
||||||
|
<path id="path188" d="m-9.56,261.2s-3.52-10.24,3.52-4.8l2.88,2.88c-3.36,1.28,0,3.84-6.4,1.92z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g190" stroke-width="0.5" stroke="#000" fill="#FFC">
|
||||||
|
<path id="path192" d="m-2.96,261.4s-3.52-10.24,3.52-4.8c0,0,4.383,2.33,2.881,2.88-2.961,1.08,0,3.84-6.401,1.92z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g194" stroke-width="0.5" stroke="#000" fill="#FFC">
|
||||||
|
<path id="path196" d="m3.52,261.32s-3.52-10.24,3.521-4.8l2.88,2.88c-0.96,1.28,0,3.84-6.401,1.92z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g198" stroke-width="0.5" stroke="#000" fill="#FFC">
|
||||||
|
<path id="path200" d="m10.2,262s-4.8-12.4,4.4-6l3.6,3.6c-1.2,1.6,0,4.8-8,2.4z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g202" stroke-width="2" stroke="#a5264c">
|
||||||
|
<path id="path204" d="m-18.2,244.8s13.2-2.8,19.2,0.4c0,0,6,1.2,7.2,0.8s4.4-0.8,4.4-0.8"/>
|
||||||
|
</g>
|
||||||
|
<g id="g206" stroke-width="2" stroke="#a5264c">
|
||||||
|
<path id="path208" d="m15.8,253.6s12-13.6,24-9.2c7.016,2.57,6-0.8,6.8-3.6s1-7,6-10"/>
|
||||||
|
</g>
|
||||||
|
<g id="g210" stroke-width="0.5" stroke="#000" fill="#FFC">
|
||||||
|
<path id="path212" d="m33,237.6s-4-10.8-6.8,2-6,16.4-7.6,19.2c0,0,0,5.2,8.4,4.8,0,0,10.8-0.4,11.2-3.2s-1.2-14.4-5.2-22.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g214" stroke-width="2" stroke="#a5264c">
|
||||||
|
<path id="path216" d="m47,244.8s3.6-2.4,6-1.2"/>
|
||||||
|
</g>
|
||||||
|
<g id="g218" stroke-width="2" stroke="#a5264c">
|
||||||
|
<path id="path220" d="m53.5,228.4s2.9-4.9,7.7-5.7"/>
|
||||||
|
</g>
|
||||||
|
<g id="g222" fill="#b2b2b2">
|
||||||
|
<path id="path224" d="m-25.8,265.2s18,3.2,22.4,1.6l0.4,2-20.8-1.2s-11.6-5.6-2-2.4z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g226" stroke-width="0.5" stroke="#000" fill="#FFC">
|
||||||
|
<path id="path228" d="m-11.8,172,19.6,0.8s7.2,30.8,3.6,38.4c0,0-1.2,2.8-4-2.8,0,0-18.4-32.8-21.6-34.8s1.2-1.6,2.4-1.6z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g230" stroke-width="0.5" stroke="#000" fill="#FFC">
|
||||||
|
<path id="path232" d="m-88.9,169.3s8.9,1.7,21.5,4.3c0,0,4.8,22.4,8,27.2s-0.4,4.8-4,2-18.4-16.8-20.4-21.2-5.1-12.3-5.1-12.3z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g234" stroke-width="0.5" stroke="#000" fill="#FFC">
|
||||||
|
<path id="path236" d="m-67.039,173.82s5.8,1.55,6.809,3.76c1.008,2.22-1.202,5.51-1.202,5.51s-1,3.31-2.202,1.15c-1.202-2.17-4.074-9.83-3.405-10.42z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g238" fill="#000">
|
||||||
|
<path id="path240" d="m-67,173.6s3.6,5.2,7.2,5.2,3.982-0.41,6.8,0.2c4.6,1,4.2-1,10.8,0.2,2.64,0.48,5.2-0.4,8,0.8s6,0.4,7.2-1.6,6-6.2,6-6.2-12.8,1.8-15.6,2.6c0,0-22.4,1.2-30.4-1.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g242" stroke-width="0.5" stroke="#000" fill="#FFC">
|
||||||
|
<path id="path244" d="m-22.4,173.8s-6.45,3.5-6.85,5.9,5.25,6.1,5.25,6.1,2.75,4.6,3.35,2.2-0.95-13.8-1.75-14.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g246" stroke-width="0.5" stroke="#000" fill="#FFC">
|
||||||
|
<path id="path248" d="m-59.885,179.26s7.007,11.19,7.224-0.02c0,0,0.557-1.26-1.203-1.28-6.075-0.07-4.554-4.18-6.021,1.3z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g250" stroke-width="0.5" stroke="#000" fill="#FFC">
|
||||||
|
<path id="path252" d="m-52.707,179.51s7.921,11.19,7.285-0.09c0,0,0.007-0.33-1.746-0.48-4.747-0.42-4.402-4.94-5.539,0.57z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g254" stroke-width="0.5" stroke="#000" fill="#FFC">
|
||||||
|
<path id="path256" d="m-45.494,179.52s7.96,10.63,7.291,0.96c0,0,0.119-1.23-1.535-1.53-3.892-0.71-4.103-3.95-5.756,0.57z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g258" stroke-width="0.5" stroke="#000" fill="#FFC">
|
||||||
|
<path id="path260" d="m-38.618,179.6s7.9,11.56,8.248,1.78c0,0,1.644-1.38-0.102-1.6-5.818-0.74-5.02-5.19-8.146-0.18z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g262" fill="#e5e5b2">
|
||||||
|
<path id="path264" d="m-74.792,183.13-7.658-1.53c-2.6-5-4.7-11.15-4.7-11.15s6.35,1,18.85,3.8c0,0,0.876,3.32,2.348,9.11l-8.84-0.23z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g266" fill="#e5e5b2">
|
||||||
|
<path id="path268" d="m-9.724,178.47c-1.666-2.51-2.983-4.26-3.633-4.67-3.013-1.88,1.13-1.51,2.259-1.51l18.454,0.76s0.524,2.24,1.208,5.63c0,0-10.088-2.01-18.288-0.21z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g270" fill="#cc7226">
|
||||||
|
<path id="path272" d="m43.88,40.321c27.721,3.96,53.241-31.68,55.001-41.361,1.759-9.68-8.36-21.56-8.36-21.56,1.32-3.08-3.52-17.16-8.8-26.4s-21.181-8.266-38.721-9.24c-15.84-0.88-34.32,22.44-35.64,24.2s4.84,40.041,6.16,45.761-1.32,32.12-1.32,32.12c34.24-9.1,3.96-7.48,31.68-3.52z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g274" fill="#ea8e51">
|
||||||
|
<path id="path276" d="m8.088-33.392c-1.296,1.728,4.752,39.313,6.048,44.929s-1.296,31.536-1.296,31.536c32.672-8.88,3.888-7.344,31.104-3.456,27.217,3.888,52.273-31.104,54.001-40.609,1.728-9.504-8.208-21.168-8.208-21.168,1.296-3.024-3.456-16.848-8.64-25.92s-20.795-8.115-38.017-9.072c-15.552-0.864-33.696,22.032-34.992,23.76z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g278" fill="#efaa7c">
|
||||||
|
<path id="path280" d="m8.816-32.744c-1.272,1.696,4.664,38.585,5.936,44.097s-1.272,30.952-1.272,30.952c31.404-9.16,3.816-7.208,30.528-3.392,26.713,3.816,51.305-30.528,53.001-39.857,1.696-9.328-8.056-20.776-8.056-20.776,1.272-2.968-3.392-16.536-8.48-25.44s-20.41-7.965-37.313-8.904c-15.264-0.848-33.072,21.624-34.344,23.32z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g282" fill="#f4c6a8">
|
||||||
|
<path id="path284" d="m9.544-32.096c-1.248,1.664,4.576,37.857,5.824,43.265s-1.248,30.368-1.248,30.368c29.436-9.04,3.744-7.072,29.952-3.328,26.209,3.744,50.337-29.952,52.001-39.104,1.664-9.153-7.904-20.385-7.904-20.385,1.248-2.912-3.328-16.224-8.32-24.96s-20.025-7.815-36.609-8.736c-14.976-0.832-32.448,21.216-33.696,22.88z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g286" fill="#f9e2d3">
|
||||||
|
<path id="path288" d="m10.272-31.448c-1.224,1.632,4.488,37.129,5.712,42.433s-1.224,29.784-1.224,29.784c27.868-8.92,3.672-6.936,29.376-3.264,25.705,3.672,49.369-29.376,51.001-38.353,1.632-8.976-7.752-19.992-7.752-19.992,1.224-2.856-3.264-15.912-8.16-24.48s-19.64-7.665-35.905-8.568c-14.688-0.816-31.824,20.808-33.048,22.44z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g290" fill="#FFF">
|
||||||
|
<path id="path292" d="M44.2,36.8c25.2,3.6,48.401-28.8,50.001-37.6s-7.6-19.6-7.6-19.6c1.2-2.8-3.201-15.6-8.001-24s-19.254-7.514-35.2-8.4c-14.4-0.8-31.2,20.4-32.4,22s4.4,36.4,5.6,41.6-1.2,29.2-1.2,29.2c25.5-8.6,3.6-6.8,28.8-3.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g294" fill="#CCC">
|
||||||
|
<path id="path296" d="m90.601,2.8s-27.801,7.6-39.401,6c0,0-15.8-6.6-24.6,15.2,0,0-3.6,7.2-5.6,9.2s69.601-30.4,69.601-30.4z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g298" fill="#000">
|
||||||
|
<path id="path300" d="m94.401,0.6s-29.001,12.2-39.001,11.8c0,0-16.4-4.6-24.8,10,0,0-8.4,9.2-11.6,10.8,0,0-0.4,1.6,6-2.4l10.4,5.2s14.8,9.6,24.4-6.4c0,0,4-11.2,4-13.2s21.2-7.6,22.801-8c1.6-0.4,8.2-4.6,7.8-7.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g302" fill="#99cc32">
|
||||||
|
<path id="path304" d="m47,36.514c-6.872,0-15.245-3.865-15.245-10.114,0-6.248,8.373-12.513,15.245-12.513,6.874,0,12.446,5.065,12.446,11.313,0,6.249-5.572,11.314-12.446,11.314z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g306" fill="#659900">
|
||||||
|
<path id="path308" d="m43.377,19.83c-4.846,0.722-9.935,2.225-9.863,2.009,1.54-4.619,7.901-7.952,13.486-7.952,4.296,0,8.084,1.978,10.32,4.988,0,0-5.316-0.33-13.943,0.955z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g310" fill="#FFF">
|
||||||
|
<path id="path312" d="m55.4,19.6s-4.4-3.2-4.4-1c0,0,3.6,4.4,4.4,1z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g314" fill="#000">
|
||||||
|
<path id="path316" d="m45.4,27.726c-2.499,0-4.525-2.026-4.525-4.526,0-2.499,2.026-4.525,4.525-4.525,2.5,0,4.526,2.026,4.526,4.525,0,2.5-2.026,4.526-4.526,4.526z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g318" fill="#cc7226">
|
||||||
|
<path id="path320" d="m-58.6,14.4s-3.2-21.2-0.8-25.6c0,0,10.8-10,10.4-13.6,0,0-0.4-18-1.6-18.8s-8.8-6.8-14.8-0.4c0,0-10.4,18-9.6,24.4v2s-7.6-0.4-9.2,1.6c0,0-1.2,5.2-2.4,5.6,0,0-2.8,2.4-0.8,5.2,0,0-2,2.4-1.6,6.4l7.6,4s2,14.4,12.8,19.6c4.836,2.329,8-4.4,10-10.4z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g322" fill="#FFF">
|
||||||
|
<path id="path324" d="m-59.6,12.56s-2.88-19.08-0.72-23.04c0,0,9.72-9,9.36-12.24,0,0-0.36-16.2-1.44-16.92s-7.92-6.12-13.32-0.36c0,0-9.36,16.2-8.64,21.96v1.8s-6.84-0.36-8.28,1.44c0,0-1.08,4.68-2.16,5.04,0,0-2.52,2.16-0.72,4.68,0,0-1.8,2.16-1.44,5.76l6.84,3.6s1.8,12.96,11.52,17.64c4.352,2.095,7.2-3.96,9-9.36z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g326" fill="#eb955c">
|
||||||
|
<path id="path328" d="m-51.05-42.61c-1.09-0.86-8.58-6.63-14.43-0.39,0,0-10.14,17.55-9.36,23.79v1.95s-7.41-0.39-8.97,1.56c0,0-1.17,5.07-2.34,5.46,0,0-2.73,2.34-0.78,5.07,0,0-1.95,2.34-1.56,6.24l7.41,3.9s1.95,14.04,12.48,19.11c4.714,2.27,7.8-4.29,9.75-10.14,0,0-3.12-20.67-0.78-24.96,0,0,10.53-9.75,10.14-13.26,0,0-0.39-17.55-1.56-18.33z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g330" fill="#f2b892">
|
||||||
|
<path id="path332" d="m-51.5-41.62c-0.98-0.92-8.36-6.46-14.06-0.38,0,0-9.88,17.1-9.12,23.18v1.9s-7.22-0.38-8.74,1.52c0,0-1.14,4.94-2.28,5.32,0,0-2.66,2.28-0.76,4.94,0,0-1.9,2.28-1.52,6.08l7.22,3.8s1.9,13.68,12.16,18.62c4.594,2.212,7.6-4.18,9.5-9.88,0,0-3.04-20.14-0.76-24.32,0,0,10.26-9.5,9.88-12.92,0,0-0.38-17.1-1.52-17.86z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g334" fill="#f8dcc8">
|
||||||
|
<path id="path336" d="m-51.95-40.63c-0.87-0.98-8.14-6.29-13.69-0.37,0,0-9.62,16.65-8.88,22.57v1.85s-7.03-0.37-8.51,1.48c0,0-1.11,4.81-2.22,5.18,0,0-2.59,2.22-0.74,4.81,0,0-1.85,2.22-1.48,5.92l7.03,3.7s1.85,13.32,11.84,18.13c4.473,2.154,7.4-4.07,9.25-9.62,0,0-2.96-19.61-0.74-23.68,0,0,9.99-9.25,9.62-12.58,0,0-0.37-16.65-1.48-17.39z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g338" fill="#FFF">
|
||||||
|
<path id="path340" d="m-59.6,12.46s-2.88-18.98-0.72-22.94c0,0,9.72-9,9.36-12.24,0,0-0.36-16.2-1.44-16.92-0.76-1.04-7.92-6.12-13.32-0.36,0,0-9.36,16.2-8.64,21.96v1.8s-6.84-0.36-8.28,1.44c0,0-1.08,4.68-2.16,5.04,0,0-2.52,2.16-0.72,4.68,0,0-1.8,2.16-1.44,5.76l6.84,3.6s1.8,12.96,11.52,17.64c4.352,2.095,7.2-4.06,9-9.46z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g342" fill="#CCC">
|
||||||
|
<path id="path344" d="m-62.7,6.2s-21.6-10.2-22.5-11c0,0,9.1,8.2,9.9,8.2s12.6,2.8,12.6,2.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g346" fill="#000">
|
||||||
|
<path id="path348" d="m-79.8,0s18.4,3.6,18.4,8c0,2.912-0.243,16.331-5.6,14.8-8.4-2.4-4.8-16.8-12.8-22.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g350" fill="#99cc32">
|
||||||
|
<path id="path352" d="m-71.4,3.8s8.978,1.474,10,4.2c0.6,1.6,1.263,9.908-4.2,11-4.552,0.911-6.782-9.31-5.8-15.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g354" fill="#000">
|
||||||
|
<path id="path356" d="m14.595,46.349c-0.497-1.742,0.814-1.611,2.605-2.149,2-0.6,14.2-4.4,15-7s14,1.8,14,1.8c1.8,0.8,6.2,3.4,6.2,3.4,4.8,1.2,11.4,1.6,11.4,1.6,2.4,1,5.8,3.8,5.8,3.8,14.6,10.2,27.001,3,27.001,3,19.999-6.6,13.999-23.8,13.999-23.8-3-9,0.2-12.4,0.2-12.4,0.2-3.8,7.4,2.6,7.4,2.6,2.6,4.2,3.4,9.2,3.4,9.2,8,11.2,4.6-6.6,4.6-6.6,0.2-1-2.6-4.6-2.6-5.8s-1.8-4.6-1.8-4.6c-3-3.4-0.6-10.4-0.6-10.4,1.8-13.8-0.4-12-0.4-12-1.2-1.8-10.4,8.2-10.4,8.2-2.2,3.4-8.2,5-8.2,5-2.799,1.8-6.199,0.4-6.199,0.4-2.6-0.4-8.2,6.6-8.2,6.6,2.8-0.2,5.2,4.2,7.6,4.4s4.2-2.4,5.799-3c1.6-0.6,4.4,5.2,4.4,5.2,0.4,2.6-5.2,7.4-5.2,7.4-0.4,4.6-1.999,3-1.999,3-3-0.6-4.2,3.2-5.2,7.8s-5.2,5-5.2,5c-1.6,7.4-2.801,4.4-2.801,4.4-0.2-5.6-6.2,0.2-6.2,0.2-1.2,2-5.8-0.2-5.8-0.2-6.8-2-4.4-4-4.4-4,1.8-2.2,13,0,13,0,2.2-1.6-5.8-5.6-5.8-5.6-0.6-1.8,0.4-6.2,0.4-6.2,1.2-3.2,8-8.8,8-8.8,9.401-1.2,6.601-2.8,6.601-2.8-6.2-5.2-12.001,2.4-12.001,2.4-2.2,6.2-19.6,21.2-19.6,21.2-4.8,3.4-2.2-3.4-6.2,0s-24.6-5.6-24.6-5.6c-11.562-1.193-14.294,14.549-17.823,11.429,0,0,5.418,8.52,3.818,2.92z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g358" fill="#000">
|
||||||
|
<path id="path360" d="m209.4-120s-25.6,8-28.4,26.8c0,0-2.4,22.8,18,40.4,0,0,0.4,6.4,2.4,9.6,0,0-1.6,4.8,17.2-2.8l27.2-8.4s6.4-2.4,11.6-11.2,20.4-27.6,16.8-52.8c0,0,1.2-11.2-4.8-11.6,0,0-8.4-1.6-15.6,6,0,0-6.8,3.2-9.2,2.8l-35.2,1.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g362" fill="#000">
|
||||||
|
<path id="path364" d="m264.02-120.99s2.1-8.93-2.74-4.09c0,0-7.04,5.72-14.52,5.72,0,0-14.52,2.2-18.92,15.4,0,0-3.96,26.84,3.96,32.56,0,0,4.84,7.48,11.88,0.88s22.54-36.83,20.34-50.47z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g366" fill="#323232">
|
||||||
|
<path id="path368" d="m263.65-120.63s2.09-8.75-2.66-3.99c0,0-6.92,5.61-14.26,5.61,0,0-14.26,2.16-18.58,15.12,0,0-3.89,26.354,3.89,31.97,0,0,4.75,7.344,11.66,0.864,6.92-6.48,22.11-36.184,19.95-49.574z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g370" fill="#666">
|
||||||
|
<path id="path372" d="m263.27-120.27s2.08-8.56-2.58-3.9c0,0-6.78,5.51-13.99,5.51,0,0-14,2.12-18.24,14.84,0,0-3.81,25.868,3.82,31.38,0,0,4.66,7.208,11.45,0.848,6.78-6.36,21.66-35.538,19.54-48.678z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g374" fill="#999">
|
||||||
|
<path id="path376" d="m262.9-119.92s2.07-8.37-2.51-3.79c0,0-6.65,5.41-13.73,5.41,0,0-13.72,2.08-17.88,14.56,0,0-3.75,25.372,3.74,30.78,0,0,4.58,7.072,11.23,0.832,6.66-6.24,21.23-34.892,19.15-47.792z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g378" fill="#CCC">
|
||||||
|
<path id="path380" d="m262.53-119.56s2.06-8.18-2.43-3.7c0,0-6.53,5.31-13.47,5.31,0,0-13.46,2.04-17.54,14.28,0,0-3.67,24.886,3.67,30.19,0,0,4.49,6.936,11.02,0.816,6.52-6.12,20.79-34.246,18.75-46.896z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g382" fill="#FFF">
|
||||||
|
<path id="path384" d="m262.15-119.2s2.05-8-2.35-3.6c0,0-6.4,5.2-13.2,5.2,0,0-13.2,2-17.2,14,0,0-3.6,24.4,3.6,29.6,0,0,4.4,6.8,10.8,0.8s20.35-33.6,18.35-46z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g386" fill="#992600">
|
||||||
|
<path id="path388" d="m50.6,84s-20.4-19.2-28.4-20c0,0-34.4-4-49.2,14,0,0,17.6-20.4,45.2-14.8,0,0-21.6-4.4-34-1.2l-26.4,14-2.8,4.8s4-14.8,22.4-20.8c0,0,22.8-4.8,33.6,0,0,0-21.6-6.8-31.6-4.8,0,0-30.4-2.4-43.2,24,0,0,4-14.4,18.8-21.6,0,0,13.6-8.8,34-6,0,0,14.4,3.2,19.6,5.6s4-0.4-4.4-5.2c0,0-5.6-10-19.6-9.6,0,0-42.8,3.6-53.2,15.6,0,0,13.6-11.2,24-14,0,0,22.4-8,30.8-7.2,0,0,24.8,1,32.4-3,0,0-11.2,5-8,8.2s10,10.8,10,12,24.2,23.3,27.8,27.7l2.2,2.3z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g390" fill="#CCC">
|
||||||
|
<path id="path392" d="m189,278s-15.5-36.5-28-46c0,0,26,16,29.5,34,0,0,0,10-1.5,12z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g394" fill="#CCC">
|
||||||
|
<path id="path396" d="m236,285.5s-26.5-55-45-79c0,0,43.5,37.5,48.5,64l0.5,5.5-3-2.5s-0.5,9-1,12z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g398" fill="#CCC">
|
||||||
|
<path id="path400" d="m292.5,237s-62.5-59.5-64-62c0,0,60.5,66,63.5,73.5,0,0-2-9,0.5-11.5z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g402" fill="#CCC">
|
||||||
|
<path id="path404" d="m104,280.5s19.5-52,38.5-29.5c0,0,15,10,14.5,13,0,0-4-6.5-22-6,0,0-19-3-31,22.5z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g406" fill="#CCC">
|
||||||
|
<path id="path408" d="m294.5,153s-45-28.5-52.5-30c-11.81-2.36,49.5,29,54.5,39.5,0,0,2-2.5-2-9.5z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g410" fill="#000">
|
||||||
|
<path id="path412" d="m143.8,259.6s20.4-2,27.2-8.8l4.4,3.6,17.6-38.4,3.6,5.2s14.4-14.8,13.6-22.8,12.8,6,12.8,6-0.8-11.6,6.4-4.8c0,0-2.4-15.6,6-7.6,0,0-10.54-30.16,12-4.4,5.6,6.4,1.2-0.4,1.2-0.4s-26-48-4.4-33.6c0,0,2-22.8,0.8-27.2s-3.2-26.8-8-32,0.4-6.8,6-1.6c0,0-11.2-24,2-12,0,0-3.6-15.2-8-18,0,0-5.6-17.2,9.6-6.4,0,0-4.4-12.4-7.6-15.6,0,0-11.6-27.6-4.4-22.8l4.4,3.6s-6.8-14-0.4-9.6,6.4,4,6.4,4-21.2-33.2-0.8-15.6c0,0-8.16-13.918-11.6-20.8,0,0-18.8-20.4-4.4-14l4.8,1.6s-8.8-10-16.8-11.6,2.4-8,8.8-6,22,9.6,22,9.6,12.8,18.8,16.8,19.2c0,0-20-7.6-14,0.4,0,0,14.4,14,7.2,13.6,0,0-6,7.2-1.2,16,0,0-18.46-18.391-3.6,7.2l6.8,16.4s-24.4-24.8-13.2-2.8c0,0,17.2,23.6,19.2,24s6.4,9.2,6.4,9.2l-4.4-2,5.2,8.8s-11.2-12-5.2,1.2l5.6,14.4s-20.4-22-6.8,7.6c0,0-16.4-5.2-7.6,12,0,0-1.6,16-1.2,21.2s1.6,33.6-2.8,41.6,6,27.2,8,31.2,5.6,14.8-3.2,5.6-4.4-3.6-2.4,5.2,8,24.4,7.2,30c0,0-1.2,1.2-4.4-2.4,0,0-14.8-22.8-13.2-8.4,0,0-1.2,8-4.4,16.8,0,0-3.2,10.8-3.2,2,0,0-3.2-16.8-6-9.2s-6.4,13.6-9.2,16-8-20.4-9.2-10c0,0-12-12.4-16.8,4l-11.6,16.4s-0.4-12.4-1.6-6.4c0,0-30,6-40.4,1.6z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g414" fill="#000">
|
||||||
|
<path id="path416" d="m109.4-97.2s-11.599-8-15.599-7.6,27.599-8.8,68.799,18.8c0,0,4.8,2.8,8.4,2.4,0,0,3.2,2.4,0.4,6,0,0-8.8,9.6,2.4,20.8,0,0,18.4,6.8,12.8-2,0,0,10.8,4,13.2,8s1.2,0,1.2,0l-12.4-12.4s-5.2-2-8-10.4-5.2-18.4-0.8-21.6c0,0-4,4.4-3.2,0.4s4.4-7.6,6-8,18-16.2,24.8-16.6c0,0-9.2,1.4-12.2,0.4s-29.6-12.4-35.6-13.6c0,0-16.8-6.6-4.8-4.6,0,0,35.8,3.8,54,17,0,0-7.2-8.4-25.6-15.4,0,0-22.2-12.6-57.4-7.6,0,0-17.8,3.2-25.6,5,0,0-2.599-0.6-3.199-1s-12.401-9.4-40.001-2.4c0,0-17,4.6-25.6,9.4,0,0-15.2,1.2-18.8,4.4,0,0-18.6,14.6-20.6,15.4s-13.4,8.4-14.2,8.8c0,0,24.6-6.6,27-9s19.8-5,22.2-3.6,10.8,0.8,1.2,1.4c0,0,75.6,14.8,76.4,16.8s4.8,0.8,4.8,0.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g418" fill="#cc7226">
|
||||||
|
<path id="path420" d="m180.8-106.4s-10.2-7.4-12.2-7.4-14.4-10.2-18.6-9.8-16.4-9.6-43.8-1.4c0,0-0.6-2,3-2.8,0,0,6.4-2.2,6.8-2.8,0,0,20.2-4.2,27.4-0.6,0,0,9.2,2.6,15.4,8.8,0,0,11.2,3.2,14.4,2.2,0,0,8.8,2.2,9.2,4,0,0,5.8,3,4,5.6,0,0,0.4,1.6-5.6,4.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g422" fill="#cc7226">
|
||||||
|
<path id="path424" d="m168.33-108.51c0.81,0.63,1.83,0.73,2.43,1.54,0.24,0.31-0.05,0.64-0.37,0.74-1.04,0.31-2.1-0.26-3.24,0.33-0.4,0.21-1.04,0.03-1.6-0.12-1.63-0.44-3.46-0.47-5.15,0.22-1.98-1.13-4.34-0.54-6.42-1.55-0.06-0.02-0.28,0.32-0.36,0.3-3.04-1.15-6.79-0.87-9.22-3.15-2.43-0.41-4.78-0.87-7.21-1.55-1.82-0.51-3.23-1.5-4.85-2.33-1.38-0.71-2.83-1.23-4.37-1.61-1.86-0.45-3.69-0.34-5.58-0.86-0.1-0.02-0.29,0.32-0.37,0.3-0.32-0.11-0.62-0.69-0.79-0.64-1.68,0.52-3.17-0.45-4.83-0.11-1.18-1.22-2.9-0.98-4.45-1.42-2.97-0.85-6.12,0.42-9.15-0.58,4.11-1.84,8.8-0.61,12.86-2.68,2.33-1.18,4.99-0.08,7.56-0.84,0.49-0.15,1.18-0.35,1.58,0.32,0.14-0.14,0.32-0.37,0.38-0.35,2.44,1.16,4.76,2.43,7.24,3.5,0.34,0.15,0.88-0.09,1.13,0.12,1.52,1.21,3.46,1.11,4.85,2.33,1.7-0.5,3.49-0.12,5.22-0.75,0.08-0.02,0.31,0.32,0.34,0.3,1.14-0.75,2.29-0.48,3.18-0.18,0.34,0.12,1,0.37,1.31,0.44,1.12,0.27,1.98,0.75,3.16,0.94,0.11,0.02,0.3-0.32,0.37-0.3,1.12,0.44,2.16,0.39,2.82,1.55,0.14-0.14,0.3-0.37,0.38-0.35,1.03,0.34,1.68,1.1,2.78,1.34,0.48,0.1,1.1,0.73,1.67,0.91,2.39,0.73,4.24,2.26,6.43,3.15,0.76,0.31,1.64,0.55,2.27,1.04z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g426" fill="#cc7226">
|
||||||
|
<path id="path428" d="m91.696-122.74c-2.518-1.72-4.886-2.83-7.328-4.62-0.181-0.13-0.541,0.04-0.743-0.08-1.007-0.61-1.895-1.19-2.877-1.89-0.539-0.38-1.36-0.37-1.868-0.63-2.544-1.29-5.173-1.85-7.68-3.04,0.682-0.64,1.804-0.39,2.4-1.2,0.195,0.28,0.433,0.56,0.786,0.37,1.678-0.9,3.528-1.05,5.204-0.96,1.704,0.09,3.424,0.39,5.199,0.67,0.307,0.04,0.506,0.56,0.829,0.66,2.228,0.66,4.617,0.14,6.736,0.98,1.591,0.63,3.161,1.45,4.4,2.72,0.252,0.26-0.073,0.57-0.353,0.76,0.388-0.11,0.661,0.1,0.772,0.41,0.084,0.24,0.084,0.54,0,0.78-0.112,0.31-0.391,0.41-0.765,0.46-1.407,0.19,0.365-1.19-0.335-0.74-1.273,0.82-0.527,2.22-1.272,3.49-0.28-0.19-0.51-0.41-0.4-0.8,0.234,0.52-0.368,0.81-0.536,1.13-0.385,0.72-1.284,2.14-2.169,1.53z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g430" fill="#cc7226">
|
||||||
|
<path id="path432" d="m59.198-115.39c-3.154-0.79-6.204-0.68-9.22-1.96-0.067-0.02-0.29,0.32-0.354,0.3-1.366-0.6-2.284-1.56-3.36-2.61-0.913-0.89-2.571-0.5-3.845-0.99-0.324-0.12-0.527-0.63-0.828-0.67-1.219-0.16-2.146-1.11-3.191-1.68,2.336-0.8,4.747-0.76,7.209-1.15,0.113-0.02,0.258,0.31,0.391,0.31,0.136,0,0.266-0.23,0.4-0.36,0.195,0.28,0.497,0.61,0.754,0.35,0.548-0.54,1.104-0.35,1.644-0.31,0.144,0.01,0.269,0.32,0.402,0.32,0.136,0,0.267-0.32,0.4-0.32,0.136,0,0.267,0.32,0.4,0.32,0.136,0,0.266-0.23,0.4-0.36,0.692,0.78,1.577,0.23,2.399,0.41,1.038,0.22,1.305,1.37,2.379,1.67,4.715,1.3,8.852,3.45,13.215,5.54,0.307,0.14,0.517,0.39,0.407,0.78,0.267,0,0.58-0.09,0.77,0.04,1.058,0.74,2.099,1.28,2.796,2.38,0.216,0.34-0.113,0.75-0.346,0.7-4.429-1-8.435-1.61-12.822-2.71z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g434" fill="#cc7226">
|
||||||
|
<path id="path436" d="m45.338-71.179c-1.592-1.219-2.176-3.25-3.304-5.042-0.214-0.34,0.06-0.654,0.377-0.743,0.56-0.159,1.103,0.319,1.512,0.521,1.745,0.862,3.28,2.104,5.277,2.243,1.99,2.234,6.25,2.619,6.257,6,0.001,0.859-1.427-0.059-1.857,0.8-2.451-1.003-4.84-0.9-7.22-2.367-0.617-0.381-0.287-0.834-1.042-1.412z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g438" fill="#cc7226">
|
||||||
|
<path id="path440" d="m17.8-123.76c0.135,0,7.166,0.24,7.149,0.35-0.045,0.31-7.775,1.36-8.139,1.19-0.164-0.08-7.676,2.35-7.81,2.22,0.268-0.14,8.534-3.76,8.8-3.76z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g442" fill="#000">
|
||||||
|
<path id="path444" d="m33.2-114s-14.8,1.8-19.2,3-23,8.8-26,10.8c0,0-13.4,5.4-30.4,25.4,0,0,7.6-3.4,9.8-6.2,0,0,13.6-12.6,13.4-10,0,0,12.2-8.6,11.6-6.4,0,0,24.4-11.2,22.4-8,0,0,21.6-4.6,20.6-2.6,0,0,18.8,4.4,16,4.6,0,0-5.8,1.2,0.6,4.8,0,0-3.4,4.4-8.8,0.4s-2.4-1.8-7.4-0.8c0,0-2.6,0.8-7.2-3.2,0,0-5.6-4.6-14.4-1,0,0-30.6,12.6-32.6,13.2,0,0-3.6,2.8-6,6.4,0,0-5.8,4.4-8.8,5.8,0,0-12.8,11.6-14,13,0,0-3.4,5.2-4.2,5.6,0,0,6.4-3.8,8.4-5.8,0,0,14-10,19.4-10.8,0,0,4.4-3,5.2-4.4,0,0,14.4-9.2,18.6-9.2,0,0,9.2,5.2,11.6-1.8,0,0,5.8-1.8,11.4-0.6,0,0,3.2-2.6,2.4-4.8,0,0,1.6-1.8,2.6,2,0,0,3.4,3.6,8.2,1.6,0,0,4-0.2,2,2.2,0,0-4.4,3.8-16.2,4,0,0-12.4,0.6-28.8,8.2,0,0-29.8,10.4-39,20.8,0,0-6.4,8.8-11.8,10,0,0-5.8,0.8-11.8,8.2,0,0,9.8-5.8,18.8-5.8,0,0,4-2.4,0.2,1.2,0,0-3.6,7.6-2,13,0,0-0.6,5.2-1.4,6.8,0,0-7.8,12.8-7.8,15.2s1.2,12.2,1.6,12.8-1-1.6,2.8,0.8,6.6,4,7.4,6.8-2-5.4-2.2-7.2-4.4-9-3.6-11.4c0,0,1,1,1.8,2.4,0,0-0.6-0.6,0-4.2,0,0,0.8-5.2,2.2-8.4s3.4-7,3.8-7.8,0.4-6.6,1.8-4l3.4,2.6s-2.8-2.6-0.6-4.8c0,0-1-5.6,0.8-8.2,0,0,7-8.4,8.6-9.4s0.2-0.6,0.2-0.6,6-4.2,0.2-2.6c0,0-4,1.6-7,1.6,0,0-7.6,2-3.6-2.2s14-9.6,17.8-9.4l0.8,1.6,11.2-2.4-1.2,0.8s-0.2-0.2,4-0.6,10,1,11.4-0.8,4.8-2.8,4.4-1.4-0.6,3.4-0.6,3.4,5-5.8,4.4-3.6-8.8,7.4-10.2,13.6l10.4-8.2,3.6-3s3.6,2.2,3.8,0.6,4.8-7.4,6-7.2,3.2-2.6,3,0,7.4,8,7.4,8,3.2-1.8,4.6-0.4,5.6-19.8,5.6-19.8l25-10.6,43.6-3.4-16.999-6.8-61.001-11.4z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g446" stroke-width="2" stroke="#4c0000">
|
||||||
|
<path id="path448" d="m51.4,85s-15-16.8-23.4-19.4c0,0-13.4-6.8-38,1"/>
|
||||||
|
</g>
|
||||||
|
<g id="g450" stroke-width="2" stroke="#4c0000">
|
||||||
|
<path id="path452" d="m24.8,64.2s-25.2-8-40.6-3.8c0,0-18.4,2-26.8,15.8"/>
|
||||||
|
</g>
|
||||||
|
<g id="g454" stroke-width="2" stroke="#4c0000">
|
||||||
|
<path id="path456" d="m21.2,63s-17-7.2-31.8-9.4c0,0-16.6-2.6-33.2,4.6,0,0-12.2,6-17.6,16.2"/>
|
||||||
|
</g>
|
||||||
|
<g id="g458" stroke-width="2" stroke="#4c0000">
|
||||||
|
<path id="path460" d="m22.2,63.4s-15.4-11-16.4-12.4c0,0-7-11-20-11.4,0,0-21.4,0.8-38.6,8.8"/>
|
||||||
|
</g>
|
||||||
|
<g id="g462" fill="#000">
|
||||||
|
<path id="path464" d="M20.895,54.407c1.542,1.463,28.505,30.393,28.505,30.393,35.2,36.6,7.2,2.4,7.2,2.4-7.6-4.8-16.8-23.6-16.8-23.6-1.2-2.8,14,7.2,14,7.2,4,0.8,17.6,20,17.6,20-6.8-2.4-2,4.8-2,4.8,2.8,2,23.201,17.6,23.201,17.6,3.6,4,7.599,5.6,7.599,5.6,14-5.2,7.6,8,7.6,8,2.4,6.8,8-4.8,8-4.8,11.2-16.8-5.2-14.4-5.2-14.4-30,2.8-36.8-13.2-36.8-13.2-2.4-2.4,6.4,0,6.4,0,8.401,2-7.2-12.4-7.2-12.4,2.4,0,11.6,6.8,11.6,6.8,10.401,9.2,12.401,7.2,12.401,7.2,17.999-8.8,28.399-1.2,28.399-1.2,2,1.6-3.6,8.4-2,13.6s6.4,17.6,6.4,17.6c-2.4,1.6-2,12.4-2,12.4,16.8,23.2,7.2,21.2,7.2,21.2-15.6-0.4-0.8,7.2-0.8,7.2,3.2,2,12,9.2,12,9.2-2.8-1.2-4.4,4-4.4,4,4.8,4,2,8.8,2,8.8-6,1.2-7.2,5.2-7.2,5.2,6.8,8-3.2,8.4-3.2,8.4,3.6,4.4-1.2,16.4-1.2,16.4-4.8,0-11.2,5.6-11.2,5.6,2.4,4.8-8,10.4-8,10.4-8.4,1.6-5.6,8.4-5.6,8.4-7.999,6-10.399,22-10.399,22-0.8,10.4-3.2,13.6,2,11.6,5.199-2,4.399-14.4,4.399-14.4-4.799-15.6,38-31.6,38-31.6,4-1.6,4.8-6.8,4.8-6.8,2,0.4,10.8,8,10.8,8,7.6,11.2,8,2,8,2,1.2-3.6-0.4-9.6-0.4-9.6,6-21.6-8-28-8-28-10-33.6,4-25.2,4-25.2,2.8,5.6,13.6,10.8,13.6,10.8l3.6-2.4c-1.6-4.8,6.8-10.8,6.8-10.8,2.8,6.4,8.8-1.6,8.8-1.6,3.6-24.4,16-10,16-10,4,1.2,5.2-5.6,5.2-5.6,3.6-10.4,0-24,0-24,3.6-0.4,13.2,5.6,13.2,5.6,2.8-3.6-6.4-20.4-2.4-18s8.4,4,8.4,4c0.8-2-9.2-14.4-9.2-14.4-4.4-2.8-9.6-23.2-9.6-23.2,7.2,3.6-2.8-11.6-2.8-11.6,0-3.2,6-14.4,6-14.4-0.8-6.8,0-6.4,0-6.4,2.8,1.2,10.8,2.8,4-3.6s0.8-11.2,0.8-11.2c4.4-2.8-9.2-2.4-9.2-2.4-5.2-4.4-4.8-8.4-4.8-8.4,8,2-6.4-12.4-8.8-16s7.2-8.8,7.2-8.8c13.2-3.6,1.6-6.8,1.6-6.8-19.6,0.4-8.8-10.4-8.8-10.4,6,0.4,4.4-2,4.4-2-5.2-1.2-14.8-7.6-14.8-7.6-4-3.6-0.4-2.8-0.4-2.8,16.8,1.2-12-10-12-10,8,0-10-10.4-10-10.4-2-1.6-5.2-9.2-5.2-9.2-6-5.2-10.8-12-10.8-12-0.4-4.4-5.2-9.2-5.2-9.2-11.6-13.6-17.2-13.2-17.2-13.2-14.8-3.6-20-2.8-20-2.8l-52.8,4.4c-26.4,12.8-18.6,33.8-18.6,33.8,6.4,8.4,15.6,4.6,15.6,4.6,4.6-6.2,16.2-4,16.2-4,20.401,3.2,17.801-0.4,17.801-0.4-2.4-4.6-18.601-10.8-18.801-11.4s-9-4-9-4c-3-1.2-7.4-10.4-7.4-10.4-3.2-3.4,12.6,2.4,12.6,2.4-1.2,1,6.2,5,6.2,5,17.401-1,28.001,9.8,28.001,9.8,10.799,16.6,10.999,8.4,10.999,8.4,2.8-9.4-9-30.6-9-30.6,0.4-2,8.6,4.6,8.6,4.6,1.4-2,2.2,3.8,2.2,3.8,0.2,2.4,4,10.4,4,10.4,2.8,13,6.4,5.6,6.4,5.6l4.6,9.4c1.4,2.6-4.6,10.2-4.6,10.2-0.2,2.8,0.6,2.6-5,10.2s-2.2,12-2.2,12c-1.4,6.6,7.4,6.2,7.4,6.2,2.6,2.2,6,2.2,6,2.2,1.8,2,4.2,1.4,4.2,1.4,1.6-3.8,7.8-1.8,7.8-1.8,1.4-2.4,9.6-2.8,9.6-2.8,1-2.6,1.4-4.2,4.8-4.8s-21.2-43.6-21.2-43.6c6.4-0.8-1.8-13.2-1.8-13.2-2.2-6.6,9.2,8,11.4,9.4s3.2,3.6,1.6,3.4-3.4,2-2,2.2,14.4,15.2,17.8,25.4,9.4,14.2,15.6,20.2,5.4,30.2,5.4,30.2c-0.4,8.8,5.6,19.4,5.6,19.4,2,3.8-2.2,22-2.2,22-2,2.2-0.6,3-0.6,3,1,1.2,7.8,14.4,7.8,14.4-1.8-0.2,1.8,3.4,1.8,3.4,5.2,6-1.2,3-1.2,3-6-1.6,1,8.2,1,8.2,1.2,1.8-7.8-2.8-7.8-2.8-9.2-0.6,2.4,6.6,2.4,6.6,8.6,7.2-2.8,2.8-2.8,2.8-4.6-1.8-1.4,5-1.4,5,3.2,1.6,20.4,8.6,20.4,8.6,0.4,3.8-2.6,8.8-2.6,8.8,0.4,4-1.8,7.4-1.8,7.4-1.2,8.2-1.8,9-1.8,9-4.2,0.2-11.6,14-11.6,14-1.8,2.6-12,14.6-12,14.6-2,7-20-0.2-20-0.2-6.6,3.4-4.6,0-4.6,0-0.4-2.2,4.4-8.2,4.4-8.2,7-2.6,4.4-13.4,4.4-13.4,4-1.4-7.2-4.2-7-5.4s6-2.6,6-2.6c8-2,3.6-4.4,3.6-4.4-0.6-4,2.4-9.6,2.4-9.6,11.6-0.8,0-17,0-17-10.8-7.6-11.8-13.4-11.8-13.4,12.6-8.2,4.4-20.6,4.6-24.2s1.4-25.2,1.4-25.2c-2-6.2-5-19.8-5-19.8,2.2-5.2,9.6-17.8,9.6-17.8,2.8-4.2,11.6-9,9.4-12s-10-1.2-10-1.2c-7.8-1.4-7.2,3.8-7.2,3.8-1.6,1-2.4,6-2.4,6-0.72,7.933-9.6,14.2-9.6,14.2-11.2,6.2-2,10.2-2,10.2,6,6.6-3.8,6.8-3.8,6.8-11-1.8-2.8,8.4-2.8,8.4,10.8,12.8,7.8,15.6,7.8,15.6-10.2,1,2.4,10.2,2.4,10.2s-0.8-2-0.6-0.2,3.2,6,4,8-3.2,2.2-3.2,2.2c0.6,9.6-14.8,5.4-14.8,5.4l-1.6,0.2c-1.6,0.2-12.8-0.6-18.6-2.8s-12.599-2.2-12.599-2.2-4,1.8-11.601,1.6c-7.6-0.2-15.6,2.6-15.6,2.6-4.4-0.4,4.2-4.8,4.4-4.6s5.8-5.4-2.2-4.8c-21.797,1.635-32.6-8.6-32.6-8.6-2-1.4-4.6-4.2-4.6-4.2-10-2,1.4,12.4,1.4,12.4,1.2,1.4-0.2,2.4-0.2,2.4-0.8-1.6-8.6-7-8.6-7-2.811-0.973-4.174-2.307-6.505-4.793z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g466" fill="#4c0000">
|
||||||
|
<path id="path468" d="m-3,42.8s11.6,5.6,14.2,8.4,16.6,14.2,16.6,14.2-5.4-2-8-3.8-13.4-10-13.4-10-3.8-6-9.4-8.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g470" fill="#99cc32">
|
||||||
|
<path id="path472" d="M-61.009,11.603c0.337-0.148-0.187-2.86-0.391-3.403-1.022-2.726-10-4.2-10-4.2-0.227,1.365-0.282,2.961-0.176,4.599,0,0,4.868,5.519,10.567,3.004z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g474" fill="#659900">
|
||||||
|
<path id="path476" d="M-61.009,11.403c-0.449,0.158-0.015-2.734-0.191-3.203-1.022-2.726-10.2-4.3-10.2-4.3-0.227,1.365-0.282,2.961-0.176,4.599,0,0,4.268,5.119,10.567,2.904z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g478" fill="#000">
|
||||||
|
<path id="path480" d="m-65.4,11.546c-0.625,0-1.131-1.14-1.131-2.546,0-1.405,0.506-2.545,1.131-2.545s1.132,1.14,1.132,2.545c0,1.406-0.507,2.546-1.132,2.546z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g482" fill="#000">
|
||||||
|
<path id="path484" d="M-65.4,9z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g486" fill="#000">
|
||||||
|
<path id="path488" d="m-111,109.6s-5.6,10,19.2,4c0,0,14-1.2,16.4-3.6,1.2,0.8,9.566,3.73,12.4,4.4,6.8,1.6,15.2-8.4,15.2-8.4s4.6-10.5,7.4-10.5-0.4,1.6-0.4,1.6-6.6,10.1-6.2,11.7c0,0-5.2,20-21.2,20.8,0,0-16.15,0.95-14.8,6.8,0,0,8.8-2.4,11.2,0,0,0,10.8-0.4,2.8,6l-6.8,11.6s0.14,3.92-10,0.4c-9.8-3.4-20.1-16.3-20.1-16.3s-15.95-14.55-5.1-28.5z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g490" fill="#e59999">
|
||||||
|
<path id="path492" d="m-112.2,113.6s-2,9.6,34.8-0.8l6.8,0.8c2.4,0.8,14.4,3.6,16.4,2.4,0,0-7.2,13.6-18.8,12,0,0-13.2,1.6-12.8,6.4,0,0,4,7.2,8.8,9.6,0,0,2.8,2.4,2.4,5.6s-3.2,4.8-5.2,5.6-5.2-2.4-6.8-2.4-10-6.4-14.4-11.2-12.8-16.8-12.4-19.6,1.2-8.4,1.2-8.4z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g494" fill="#b26565">
|
||||||
|
<path id="path496" d="m-109,131.05c2.6,3.95,5.8,8.15,8,10.55,4.4,4.8,12.8,11.2,14.4,11.2s4.8,3.2,6.8,2.4,4.8-2.4,5.2-5.6-2.4-5.6-2.4-5.6c-3.066-1.53-5.806-5.02-7.385-7.35,0,0,0.185,2.55-5.015,1.75s-10.4-3.6-12-6.8-4-5.6-2.4-2,4,7.2,5.6,7.6,1.2,1.6-1.2,1.2-5.2-0.8-9.6-6z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g498" fill="#992600">
|
||||||
|
<path id="path500" d="m-111.6,110s1.8-13.6,3-17.6c0,0-0.8-6.8,1.6-11s4.4-10.4,7.4-15.8,3.2-9.4,7.2-11,10-10.2,12.8-11.2,2.6-0.2,2.6-0.2,6.8-14.8,20.4-10.8c0,0-16.2-2.8-0.4-12.2,0,0-4.8,1.1-1.5-5.9,2.201-4.668,1.7,2.1-9.3,13.9,0,0-5,8.6-10.2,11.6s-17.2,10-18.4,13.8-4.4,9.6-6.4,11.2-4.8,5.8-5.2,9.2c0,0-1.2,4-2.6,5.2s-1.6,4.4-1.6,6.4-2,4.8-1.8,7.2c0,0,0.8,19,0.4,21l2-3.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g502" fill="#FFF">
|
||||||
|
<path id="path504" d="m-120.2,114.6s-2-1.4-6.4,4.6c0,0,7.3,33,7.3,34.4,0,0,1.1-2.1-0.2-9.3s-2.2-19.9-2.2-19.9l1.5-9.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g506" fill="#992600">
|
||||||
|
<path id="path508" d="m-98.6,54s-17.6,3.2-17.2,32.4l-0.8,24.8s-1.2-25.6-2.4-27.2,2.8-12.8-0.4-6.8c0,0-14,14-6,35.2,0,0,1.5,3.3-1.5-1.3,0,0-4.6-12.6-3.5-19,0,0,0.2-2.2,2.1-5,0,0,8.6-11.7,11.3-14,0,0,1.8-14.4,17.2-19.6,0,0,5.7-2.3,1.2,0.5z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g510" fill="#000">
|
||||||
|
<path id="path512" d="m40.8-12.2c0.66-0.354,0.651-1.324,1.231-1.497,1.149-0.344,1.313-1.411,1.831-2.195,0.873-1.319,1.066-2.852,1.648-4.343,0.272-0.7,0.299-1.655-0.014-2.315-1.174-2.481-1.876-4.93-3.318-7.356-0.268-0.45-0.53-1.244-0.731-1.842-0.463-1.384-1.72-2.375-2.58-3.695-0.288-0.441,0.237-1.366-0.479-1.45-0.897-0.105-2.346-0.685-2.579,0.341-0.588,2.587,0.423,5.11,1.391,7.552-0.782,0.692-0.448,1.613-0.296,2.38,0.71,3.606-0.488,6.958-1.249,10.432-0.023,0.104,0.319,0.302,0.291,0.364-1.222,2.686-2.674,5.131-4.493,7.512-0.758,0.992-1.63,1.908-2.127,2.971-0.368,0.787-0.776,1.753-0.526,2.741-3.435,2.78-5.685,6.625-8.296,10.471-0.462,0.68-0.171,1.889,0.38,2.158,0.813,0.398,1.769-0.626,2.239-1.472,0.389-0.698,0.742-1.348,1.233-1.991,0.133-0.175-0.046-0.594,0.089-0.715,2.633-2.347,4.302-5.283,6.755-7.651,1.95-0.329,3.487-1.327,5.235-2.34,0.308-0.179,0.832,0.07,1.122-0.125,1.753-1.177,1.751-3.213,1.857-5.123,0.05-0.884,0.246-2.201,1.386-2.812z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g514" fill="#000">
|
||||||
|
<path id="path516" d="m31.959-16.666c0.124-0.077-0.031-0.5,0.078-0.716,0.162-0.324,0.565-0.512,0.727-0.836,0.109-0.216-0.054-0.596,0.082-0.738,2.333-2.447,2.59-5.471,1.554-8.444,1.024-0.62,1.085-1.882,0.66-2.729-0.853-1.7-1.046-3.626-2.021-5.169-0.802-1.269-2.38-2.513-3.751-1.21-0.421,0.4-0.742,1.187-0.464,1.899,0.064,0.163,0.349,0.309,0.322,0.391-0.107,0.324-0.653,0.548-0.659,0.82-0.03,1.496-0.984,3.007-0.354,4.336,0.772,1.629,1.591,3.486,2.267,5.262-1.234,2.116-0.201,4.565-1.954,6.442-0.136,0.146-0.127,0.532-0.005,0.734,0.292,0.486,0.698,0.892,1.184,1.184,0.202,0.121,0.55,0.123,0.75-0.001,0.578-0.362,0.976-0.849,1.584-1.225z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g518" fill="#000">
|
||||||
|
<path id="path520" d="m94.771-26.977c1.389,1.792,1.679,4.587-0.37,5.977,0.55,3.309,3.901,1.33,5.999,0.8-0.11-0.388,0.12-0.732,0.4-0.737,1.06-0.015,1.74-1.047,2.8-0.863,0.44-1.557,2.07-2.259,2.72-3.639,1.72-3.695,1.13-7.968-1.45-11.214-0.2-0.254,0.01-0.771-0.11-1.133-0.76-2.211-2.82-2.526-4.76-3.214-1.176-3.875-1.837-7.906-3.599-11.6-1.614-0.25-2.312-1.989-3.649-2.709-1.333-0.719-1.901,0.86-1.86,1.906,0.007,0.205,0.459,0.429,0.289,0.794-0.076,0.164-0.336,0.275-0.336,0.409,0.001,0.135,0.222,0.266,0.356,0.4-0.918,0.82-2.341,1.297-2.636,2.442-0.954,3.71,1.619,6.835,3.287,10.036,0.591,1.135-0.145,2.406-0.905,3.614-0.438,0.695-0.33,1.822-0.054,2.678,0.752,2.331,2.343,4.07,3.878,6.053z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g522" fill="#000">
|
||||||
|
<path id="path524" d="m57.611-8.591c-1.487,1.851-4.899,4.42-1.982,6.348,0.194,0.129,0.564,0.133,0.737-0.001,2.021-1.565,4.024-2.468,6.46-3.05,0.124-0.029,0.398,0.438,0.767,0.277,1.613-0.703,3.623-0.645,4.807-1.983,3.767,0.224,7.332-0.892,10.723-2.2,1.161-0.448,2.431-1.007,3.632-1.509,1.376-0.576,2.58-1.504,3.692-2.645,0.133-0.136,0.487-0.046,0.754-0.046-0.04-0.863,0.922-0.99,1.169-1.612,0.092-0.232-0.058-0.628,0.075-0.73,2.138-1.63,3.058-3.648,1.889-6.025-0.285-0.578-0.534-1.196-1.1-1.672-1.085-0.911-2.187-0.057-3.234-0.361-0.159,0.628-0.888,0.456-1.274,0.654-0.859,0.439-2.192-0.146-3.051,0.292-1.362,0.695-2.603,0.864-4.025,1.241-0.312,0.082-1.09-0.014-1.25,0.613-0.134-0.134-0.282-0.368-0.388-0.346-1.908,0.396-3.168,0.61-4.469,2.302-0.103,0.133-0.545-0.046-0.704,0.089-0.957,0.808-1.362,2.042-2.463,2.714-0.201,0.123-0.553-0.045-0.747,0.084-0.646,0.431-1.013,1.072-1.655,1.519-0.329,0.229-0.729-0.096-0.697-0.352,0.245-1.947,0.898-3.734,0.323-5.61,2.077-2.52,4.594-4.469,6.4-7.2,0.015-2.166,0.707-4.312,0.594-6.389-0.01-0.193-0.298-0.926-0.424-1.273-0.312-0.854,0.594-1.92-0.25-2.644-1.404-1.203-2.696-0.327-3.52,1.106-1.838,0.39-3.904,1.083-5.482-0.151-1.007-0.787-1.585-1.693-2.384-2.749-0.985-1.302-0.65-2.738-0.58-4.302,0.006-0.128-0.309-0.264-0.309-0.398,0.001-0.135,0.221-0.266,0.355-0.4-0.706-0.626-0.981-1.684-2-2,0.305-1.092-0.371-1.976-1.242-2.278-1.995-0.691-3.672,1.221-5.564,1.294-0.514,0.019-0.981-1.019-1.63-1.344-0.432-0.216-1.136-0.249-1.498,0.017-0.688,0.504-1.277,0.618-2.035,0.823-1.617,0.436-2.895,1.53-4.375,2.385-1.485,0.857-2.44,2.294-3.52,3.614-0.941,1.152-1.077,3.566,0.343,4.066,1.843,0.65,3.147-2.053,5.113-1.727,0.312,0.051,0.518,0.362,0.408,0.75,0.389,0.109,0.607-0.12,0.8-0.4,0.858,1.019,2.022,1.356,2.96,2.229,0.97,0.904,2.716,0.486,3.731,1.483,1.529,1.502,0.97,4.183,2.909,5.488-0.586,1.313-1.193,2.59-1.528,4.017-0.282,1.206,0.712,2.403,1.923,2.312,1.258-0.094,1.52-0.853,2.005-1.929,0.267,0.267,0.736,0.564,0.695,0.78-0.457,2.387-1.484,4.38-1.942,6.811-0.059,0.317-0.364,0.519-0.753,0.409-0.468,4.149-4.52,6.543-7.065,9.708-0.403,0.502-0.407,1.751,0.002,2.154,1.403,1.387,3.363-0.159,5.063-0.662,0.213-1.206,1.072-2.148,2.404-2.092,0.256,0.01,0.491-0.532,0.815-0.662,0.348-0.138,0.85,0.086,1.136-0.112,1.729-1.195,3.137-2.301,4.875-3.49,0.192-0.131,0.536,0.028,0.752-0.08,0.325-0.162,0.512-0.549,0.835-0.734,0.348-0.2,0.59,0.09,0.783,0.37-0.646,0.349-0.65,1.306-1.232,1.508-0.775,0.268-1.336,0.781-2.01,1.228-0.292,0.193-0.951-0.055-1.055,0.124-0.598,1.028-1.782,1.466-2.492,2.349z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g526" fill="#000">
|
||||||
|
<path id="path528" d="m2.2-58s-9.238-2.872-20.4,22.8c0,0-2.4,5.2-4.8,7.2s-13.6,5.6-15.6,9.6l-10.4,16s14.8-16,18-18.4c0,0,8-8.4,4.8-1.6,0,0-14,10.8-12.8,20,0,0-5.6,14.4-6.4,16.4,0,0,16-32,18.4-33.2s3.6-1.2,2.4,2.4-1.6,20-4.4,22c0,0,8-20.4,7.2-23.6,0,0,3.2-3.6,5.6,1.6l-1.2,16,4.4,12s-2.4-11.2-0.8-26.8c0,0-2-10.4,2-4.8s13.6,11.6,13.6,16.4c0,0-5.2-17.6-14.4-22.4l-4,6-1.2-2s-3.6-0.8,0.8-7.6,4-7.6,4-7.6,6.4,7.2,8,7.2c0,0,13.2-7.6,14.4,16.8,0,0,6.8-14.4-2.4-21.2,0,0-14.8-2-13.6-7.2l7.2-12.4c3.6-5.2,2-2.4,2-2.4z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g530" fill="#000">
|
||||||
|
<path id="path532" d="m-17.8-41.6-16,5.2-7.2,9.6s17.2-10,21.2-11.2,2-3.6,2-3.6z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g534" fill="#000">
|
||||||
|
<path id="path536" d="m-57.8-35.2s-2,1.2-2.4,4-2.8,3.2-2,6,2.8,5.2,2.8,1.2,1.6-6,2.4-7.2,2.4-5.6-0.8-4z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g538" fill="#000">
|
||||||
|
<path id="path540" d="m-66.6,26s-8.4-4-11.6-7.6-2.748,1.566-7.6,1.2c-5.847-0.441-4.8-16.4-4.8-16.4l-4,7.6s-1.2,14.4,6.8,12c3.907-1.172,5.2,0.4,3.6,1.2s5.6,1.2,2.8,2.8,11.6-3.6,9.2,6.8l5.6-7.6z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g542" fill="#000">
|
||||||
|
<path id="path544" d="m-79.2,40.4s-15.4,4.4-19-5.2c0,0-4.8,2.4-2.6,5.4s3.4,3.4,3.4,3.4,5.4,1.2,4.8,2-3,4.2-3,4.2,10.2-6,16.4-9.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g546" fill="#FFF">
|
||||||
|
<path id="path548" d="m149.2,118.6c-0.43,2.14-2.1,2.94-4,3.6-1.92-0.96-4.51-4.06-6.4-2-0.47-0.48-1.25-0.54-1.6-1.2-0.46-0.9-0.19-1.94-0.53-2.74-0.55-1.28-1.25-2.64-1.07-4.06,1.81-0.71,2.4-2.62,1.93-4.38-0.07-0.26-0.5-0.45-0.3-0.8,0.19-0.33,0.5-0.55,0.77-0.82-0.13,0.14-0.28,0.37-0.39,0.35-0.61-0.11-0.49-0.75-0.36-1.13,0.59-1.75,2.6-2.01,3.95-0.82,0.26-0.56,0.77-0.37,1.2-0.4-0.05-0.58,0.36-1.11,0.56-1.53,0.52-1.09,2.14,0.01,2.94-0.6,1.08-0.83,2.14-1.52,3.22-0.92,1.81,1.01,3.52,2.22,4.72,3.97,0.57,0.83,0.81,2.11,0.75,3.07-0.04,0.65-1.42,0.29-1.76,1.22-0.65,1.75,1.19,2.27,1.94,3.61,0.2,0.35-0.06,0.65-0.38,0.75-0.41,0.13-1.19-0.06-1.06,0.39,0.98,3.19-1.78,3.87-4.13,4.44z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g550" fill="#FFF">
|
||||||
|
<path id="path552" d="m139.6,138.2c-0.01-1.74-1.61-3.49-0.4-5.2,0.14,0.14,0.27,0.36,0.4,0.36,0.14,0,0.27-0.22,0.4-0.36,1.5,2.22,5.15,3.14,5.01,5.99-0.03,0.45-1.11,1.37-0.21,2.01-1.81,1.35-1.87,3.72-2.8,5.6-1.24-0.28-2.45-0.65-3.6-1.2,0.35-1.48,0.24-3.17,1.06-4.49,0.43-0.7,0.14-1.78,0.14-2.71z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g554" fill="#CCC">
|
||||||
|
<path id="path556" d="m-26.6,129.2s-16.858,10.14-2.8-5.2c8.8-9.6,18.8-15.2,18.8-15.2s10.4-4.4,14-5.6,18.8-6.4,22-6.8,12.8-4.4,19.6-0.4,14.8,8.4,14.8,8.4-16.4-8.4-20-6-10.8,2-16.8,5.2c0,0-14.8,4.4-18,6.4s-13.6,13.6-15.2,12.8,0.4-1.2,1.6-4-0.8-4.4-8.8,2-9.2,8.4-9.2,8.4z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g558" fill="#000">
|
||||||
|
<path id="path560" d="m-19.195,123.23s1.41-13.04,9.888-11.37c0,0,8.226-4.17,10.948-6.14,0,0,8.139-1.7,9.449-2.32,18.479-8.698,33.198-4.179,33.745-5.299,0.546-1.119,20.171,5.999,23.78,10.079,0.391,0.45-10.231-5.59-19.929-7.48-8.273-1.617-29.875,0.24-40.781,5.78-2.973,1.51-11.918,7.29-14.449,7.18s-12.651,9.57-12.651,9.57z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g562" fill="#CCC">
|
||||||
|
<path id="path564" d="m-23,148.8s-15.2-2.4,1.6-4c0,0,18-2,22-7.2,0,0,13.6-9.2,16.4-9.6s32.8-7.6,33.2-10,6-2.4,7.6-1.6,0.8,2-2,2.8-34,17.2-40.4,18.4-18,8.8-22.8,10-15.6,1.2-15.6,1.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g566" fill="#000">
|
||||||
|
<path id="path568" d="m-3.48,141.4s-8.582-0.83,0.019-1.64c0,0,8.816-3.43,10.864-6.09,0,0,6.964-4.71,8.397-4.92,1.434-0.2,15.394-3.89,15.599-5.12s34.271-13.81,38.691-10.62c2.911,2.1-6.99,0.43-16.624,4.84-1.355,0.62-35.208,15.2-38.485,15.82-3.277,0.61-9.216,4.5-11.674,5.12-2.457,0.61-6.787,2.61-6.787,2.61z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g570" fill="#000">
|
||||||
|
<path id="path572" d="m-11.4,143.6s5.2-0.4,4,1.2-3.6,0.8-3.6,0.8l-0.4-2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g574" fill="#000">
|
||||||
|
<path id="path576" d="m-18.6,145.2s5.2-0.4,4,1.2-3.6,0.8-3.6,0.8l-0.4-2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g578" fill="#000">
|
||||||
|
<path id="path580" d="m-29,146.8s5.2-0.4,4,1.2-3.6,0.8-3.6,0.8l-0.4-2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g582" fill="#000">
|
||||||
|
<path id="path584" d="m-36.6,147.6s5.2-0.4,4,1.2-3.6,0.8-3.6,0.8l-0.4-2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g586" fill="#000">
|
||||||
|
<path id="path588" d="m1.8,108,3.2,1.6c-1.2,1.6-4.4,1.2-4.4,1.2l1.2-2.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g590" fill="#000">
|
||||||
|
<path id="path592" d="m-8.2,113.6s6.506-2.14,4,1.2c-1.2,1.6-3.6,0.8-3.6,0.8l-0.4-2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g594" fill="#000">
|
||||||
|
<path id="path596" d="m-19.4,118.4s5.2-0.4,4,1.2-3.6,0.8-3.6,0.8l-0.4-2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g598" fill="#000">
|
||||||
|
<path id="path600" d="m-27,124.4s5.2-0.4,4,1.2-3.6,0.8-3.6,0.8l-0.4-2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g602" fill="#000">
|
||||||
|
<path id="path604" d="m-33.8,129.2s5.2-0.4,4,1.2-3.6,0.8-3.6,0.8l-0.4-2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g606" fill="#000">
|
||||||
|
<path id="path608" d="m5.282,135.6s6.921-0.53,5.324,1.6c-1.597,2.12-4.792,1.06-4.792,1.06l-0.532-2.66z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g610" fill="#000">
|
||||||
|
<path id="path612" d="m15.682,130.8s6.921-0.53,5.324,1.6c-1.597,2.12-4.792,1.06-4.792,1.06l-0.532-2.66z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g614" fill="#000">
|
||||||
|
<path id="path616" d="m26.482,126.4s6.921-0.53,5.324,1.6c-1.597,2.12-4.792,1.06-4.792,1.06l-0.532-2.66z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g618" fill="#000">
|
||||||
|
<path id="path620" d="m36.882,121.6s6.921-0.53,5.324,1.6c-1.597,2.12-4.792,1.06-4.792,1.06l-0.532-2.66z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g622" fill="#000">
|
||||||
|
<path id="path624" d="m9.282,103.6s6.921-0.53,5.324,1.6c-1.597,2.12-5.592,1.86-5.592,1.86l0.268-3.46z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g626" fill="#000">
|
||||||
|
<path id="path628" d="m19.282,100.4s6.921-0.534,5.324,1.6c-1.597,2.12-5.992,1.86-5.992,1.86l0.668-3.46z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g630" fill="#000">
|
||||||
|
<path id="path632" d="m-3.4,140.4s5.2-0.4,4,1.2-3.6,0.8-3.6,0.8l-0.4-2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g634" fill="#992600">
|
||||||
|
<path id="path636" d="m-76.6,41.2s-4.4,8.8-4.8,12c0,0,0.8-8.8,2-10.8s2.8-1.2,2.8-1.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g638" fill="#992600">
|
||||||
|
<path id="path640" d="m-95,55.2s-3.2,14.4-2.8,17.2c0,0-1.2-11.6-0.8-12.8s3.6-4.4,3.6-4.4z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g642" fill="#CCC">
|
||||||
|
<path id="path644" d="m-74.2-19.4-0.2,3.2-2.2,0.2s14.2,12.6,14.8,20.2c0,0,0.8-8.2-12.4-23.6z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g646" fill="#000">
|
||||||
|
<path id="path648" d="m-70.216-18.135c-0.431-0.416-0.212-1.161-0.62-1.421-0.809-0.516,1.298-0.573,1.07-1.289-0.383-1.206-0.196-1.227-0.318-2.503-0.057-0.598,0.531-2.138,0.916-2.578,1.446-1.652,0.122-4.584,1.762-6.135,0.304-0.289,0.68-0.841,0.965-1.259,0.659-0.963,1.843-1.451,2.793-2.279,0.318-0.276,0.117-1.103,0.686-1.011,0.714,0.115,1.955-0.015,1.91,0.826-0.113,2.12-1.442,3.84-2.722,5.508,0.451,0.704-0.007,1.339-0.291,1.896-1.335,2.62-1.146,5.461-1.32,8.301-0.005,0.085-0.312,0.163-0.304,0.216,0.353,2.335,0.937,4.534,1.816,6.763,0.366,0.93,0.837,1.825,0.987,2.752,0.111,0.686,0.214,1.519-0.194,2.224,2.035,2.89,0.726,5.541,1.895,9.072,0.207,0.625,1.899,2.539,1.436,2.378-2.513-0.871-2.625-1.269-2.802-2.022-0.146-0.623-0.476-2-0.713-2.602-0.064-0.164-0.235-2.048-0.313-2.17-1.513-2.382-0.155-2.206-1.525-4.564-1.428-0.68-2.394-1.784-3.517-2.946-0.198-0.204,0.945-0.928,0.764-1.141-1.092-1.289-2.245-2.056-1.909-3.549,0.155-0.69,0.292-1.747-0.452-2.467z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g650" fill="#000">
|
||||||
|
<path id="path652" d="m-73.8-16.4s0.4,6.8,2.8,8.4,1.2,0.8-2-0.4-2-2-2-2-2.8,0.4-0.4,2.4,6,4.4,4.4,4.4-9.2-4-9.2-6.8-1-6.9-1-6.9,1.1-0.8,5.9-0.7c0,0,1.4,0.7,1.5,1.6z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g654" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path656" d="m-74.6,2.2s-8.52-2.791-27,0.6c0,0,9.031-2.078,27.8,0.2,10.3,1.25-0.8-0.8-0.8-0.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g658" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path660" d="m-72.502,2.129s-8.246-3.518-26.951-1.737c0,0,9.178-1.289,27.679,2.603,10.154,2.136-0.728-0.866-0.728-0.866z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g662" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path664" d="m-70.714,2.222s-7.962-4.121-26.747-3.736c0,0,9.248-0.604,27.409,4.654,9.966,2.885-0.662-0.918-0.662-0.918z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g666" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path668" d="m-69.444,2.445s-6.824-4.307-23.698-5.405c0,0,8.339,0.17,24.22,6.279,8.716,3.353-0.522-0.874-0.522-0.874z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g670" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path672" d="m45.84,12.961s-0.93,0.644-0.716-0.537c0.215-1.181,28.423-14.351,32.037-14.101,0,0-30.248,13.206-31.321,14.638z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g674" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path676" d="m42.446,13.6s-0.876,0.715-0.755-0.479,27.208-16.539,30.83-16.573c0,0-29.117,15.541-30.075,17.052z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g678" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path680" d="m39.16,14.975s-0.828,0.772-0.786-0.428c0.042-1.199,19.859-16.696,29.671-18.57,0,0-18.03,8.127-28.885,18.998z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g682" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path684" d="m36.284,16.838s-0.745,0.694-0.707-0.385c0.038-1.08,17.872-15.027,26.703-16.713,0,0-16.226,7.314-25.996,17.098z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g686" fill="#CCC">
|
||||||
|
<path id="path688" d="m4.6,164.8s-15.2-2.4,1.6-4c0,0,18-2,22-7.2,0,0,13.6-9.2,16.4-9.6s19.2-4,19.6-6.4,6.4-4.8,8-4,1.6,10-1.2,10.8-21.6,8-28,9.2-18,8.8-22.8,10-15.6,1.2-15.6,1.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g690" fill="#000">
|
||||||
|
<path id="path692" d="m77.6,127.4s-3,1.6-4.2,4.2c0,0-6.4,10.6-20.6,13.8,0,0-23,9-30.8,11,0,0-13.4,5-20.8,4.2,0,0-7,0.2-0.8,1.8,0,0,20.2-2,23.6-3.8,0,0,15.6-5.2,18.6-7.8s21.2-7.6,23.4-9.6,12-10.4,11.6-13.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g694" fill="#000">
|
||||||
|
<path id="path696" d="m18.882,158.91s5.229-0.23,4.076,1.32-3.601,0.68-3.601,0.68l-0.475-2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g698" fill="#000">
|
||||||
|
<path id="path700" d="m11.68,160.26s5.228-0.22,4.076,1.33c-1.153,1.55-3.601,0.67-3.601,0.67l-0.475-2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g702" fill="#000">
|
||||||
|
<path id="path704" d="m1.251,161.51s5.229-0.23,4.076,1.32-3.601,0.68-3.601,0.68l-0.475-2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g706" fill="#000">
|
||||||
|
<path id="path708" d="m-6.383,162.06s5.229-0.23,4.076,1.32-3.601,0.67-3.601,0.67l-0.475-1.99z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g710" fill="#000">
|
||||||
|
<path id="path712" d="m35.415,151.51s6.96-0.3,5.425,1.76c-1.534,2.07-4.793,0.9-4.793,0.9l-0.632-2.66z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g714" fill="#000">
|
||||||
|
<path id="path716" d="m45.73,147.09s5.959-3.3,5.425,1.76c-0.27,2.55-4.793,0.9-4.793,0.9l-0.632-2.66z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g718" fill="#000">
|
||||||
|
<path id="path720" d="m54.862,144.27s7.159-3.7,5.425,1.77c-0.778,2.44-4.794,0.9-4.794,0.9l-0.631-2.67z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g722" fill="#000">
|
||||||
|
<path id="path724" d="m64.376,139.45s4.359-4.9,5.425,1.76c0.406,2.54-4.793,0.9-4.793,0.9l-0.632-2.66z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g726" fill="#000">
|
||||||
|
<path id="path728" d="m26.834,156s5.228-0.23,4.076,1.32c-1.153,1.55-3.602,0.68-3.602,0.68l-0.474-2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g730" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path732" d="m62.434,34.603s-0.726,0.665-0.727-0.406c0-1.07,17.484-14.334,26.327-15.718,0,0-16.099,6.729-25.6,16.124z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g734" fill="#000">
|
||||||
|
<path id="path736" d="m65.4,98.4s22.001,22.4,31.201,26c0,0,9.199,11.2,5.199,37.2,0,0-3.199,7.6-6.399-13.2,0,0,3.2-25.2-8-9.2,0,0-8.401-9.9-2.001-9.6,0,0,3.201,2,3.601,0.4s-7.601-15.2-24.801-29.6,1.2-2,1.2-2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g738" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path740" d="m7,137.2s-0.2-1.8,1.6-1,96,7,127.6,31c0,0-45.199-23.2-129.2-30z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g742" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path744" d="m17.4,132.8s-0.2-1.8,1.6-1,138.4-0.2,162,32.2c0,0-22-25.2-163.6-31.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g746" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path748" d="m29,128.8s-0.2-1.8,1.6-1,175.2-12.2,198.8,20.2c0,0-9.6-25.6-200.4-19.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g750" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path752" d="m39,124s-0.2-1.8,1.6-1,124-37.8,147.6-5.4c0,0-13.4-24.6-149.2,6.4z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g754" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path756" d="m-19,146.8s-0.2-1.8,1.6-1,19.6,3,21.6,41.8c0,0-7.2-42-23.2-40.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g758" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path760" d="m-27.8,148.4s-0.2-1.8,1.6-1,16-3.8,13.2,35c0,0,1.2-35.2-14.8-34z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g762" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path764" d="m-35.8,148.8s-0.2-1.8,1.6-1,17.2,1.4,4.8,23.8c0,0,9.6-24-6.4-22.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g766" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path768" d="m11.526,104.46s-0.444,2,1.105,0.79c16.068-12.628,48.51-71.53,104.2-77.164,0,0-38.312-12.11-105.3,76.374z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g770" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path772" d="m22.726,102.66s-1.363-1.19,0.505-1.81c1.868-0.63,114.31-73.13,153.6-65.164,0,0-27.11-7.51-154.1,66.974z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g774" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path776" d="m1.885,108.77s-0.509,1.6,1.202,0.62c8.975-5.12,12.59-62.331,56.167-63.586,0,0-32.411-14.714-57.369,62.966z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g778" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path780" d="m-18.038,119.79s-1.077,1.29,0.876,1.03c10.246-1.33,31.651-42.598,76.09-37.519,0,0-31.966-14.346-76.966,36.489z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g782" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path784" d="m-6.8,113.67s-0.811,1.47,1.058,0.84c9.799-3.27,22.883-47.885,67.471-51.432,0,0-34.126-7.943-68.529,50.592z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g786" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path788" d="m-25.078,124.91s-0.873,1.04,0.709,0.84c8.299-1.08,25.637-34.51,61.633-30.396,0,0-25.893-11.62-62.342,29.556z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g790" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path792" d="m-32.677,130.82s-1.005,1.05,0.586,0.93c4.168-0.31,34.806-33.39,53.274-17.89,0,0-12.015-18.721-53.86,16.96z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g794" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path796" d="m36.855,98.898s-1.201-1.355,0.731-1.74c1.932-0.384,122.63-58.097,160.59-45.231,0,0-25.94-10.874-161.32,46.971z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g798" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path800" d="m3.4,163.2s-0.2-1.8,1.6-1,17.2,1.4,4.8,23.8c0,0,9.6-24-6.4-22.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g802" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path804" d="m13.8,161.6s-0.2-1.8,1.6-1,19.6,3,21.6,41.8c0,0-7.2-42-23.2-40.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g806" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path808" d="m20.6,160s-0.2-1.8,1.6-1,26.4,4.2,50,36.6c0,0-35.6-36.8-51.6-35.6z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g810" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path812" d="m28.225,157.97s-0.437-1.76,1.453-1.2c1.89,0.55,22.324-1.35,60.421,32.83,0,0-46.175-34.94-61.874-31.63z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g814" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path816" d="m38.625,153.57s-0.437-1.76,1.453-1.2c1.89,0.55,36.724,5.05,88.422,40.03,0,0-74.176-42.14-89.875-38.83z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g818" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path820" d="m-1.8,142s-0.2-1.8,1.6-1,55.2,3.4,85.6,30.2c0,0-34.901-24.77-87.2-29.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g822" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path824" d="m-11.8,146s-0.2-1.8,1.6-1,26.4,4.2,50,36.6c0,0-35.6-36.8-51.6-35.6z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g826" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path828" d="m49.503,148.96s-0.565-1.72,1.361-1.3c1.926,0.41,36.996,2.34,91.116,33.44,0,0-77.663-34.4-92.477-32.14z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g830" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path832" d="m57.903,146.56s-0.565-1.72,1.361-1.3c1.926,0.41,36.996,2.34,91.116,33.44,0,0-77.063-34.8-92.477-32.14z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g834" stroke-width="0.1" stroke="#000" fill="#FFF">
|
||||||
|
<path id="path836" d="m67.503,141.56s-0.565-1.72,1.361-1.3c1.926,0.41,44.996,4.74,134.72,39.04,0,0-120.66-40.4-136.08-37.74z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g838" fill="#000">
|
||||||
|
<path id="path840" d="m-43.8,148.4s5.2-0.4,4,1.2-3.6,0.8-3.6,0.8l-0.4-2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g842" fill="#000">
|
||||||
|
<path id="path844" d="m-13,162.4s5.2-0.4,4,1.2-3.6,0.8-3.6,0.8l-0.4-2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g846" fill="#000">
|
||||||
|
<path id="path848" d="m-21.8,162s5.2-0.4,4,1.2-3.6,0.8-3.6,0.8l-0.4-2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g850" fill="#000">
|
||||||
|
<path id="path852" d="m-117.17,150.18s5.05,1.32,3.39,2.44-3.67-0.42-3.67-0.42l0.28-2.02z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g854" fill="#000">
|
||||||
|
<path id="path856" d="m-115.17,140.58s5.05,1.32,3.39,2.44-3.67-0.42-3.67-0.42l0.28-2.02z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g858" fill="#000">
|
||||||
|
<path id="path860" d="m-122.37,136.18s5.05,1.32,3.39,2.44-3.67-0.42-3.67-0.42l0.28-2.02z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g862" fill="#CCC">
|
||||||
|
<path id="path864" d="m-42.6,211.2-5.6,2c-2,0-13.2,3.6-18.8,13.6,0,0,12.4-9.6,24.4-15.6z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g866" fill="#CCC">
|
||||||
|
<path id="path868" d="m45.116,303.85c0.141,0.25,0.196,0.67,0.488,0.69,0.658,0.04,1.891,0.34,1.766-0.29-0.848-4.31-1.722-9.25-5.855-11.05-0.639-0.28-2.081,0.13-2.155,1.02-0.127,1.52-0.244,2.87,0.065,4.33,0.3,1.43,2.458,1.43,3.375,0.05,0.936,1.67,1.368,3.52,2.316,5.25z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g870" fill="#CCC">
|
||||||
|
<path id="path872" d="m34.038,308.58c0.748,1.41,0.621,3.27,2.036,3.84,0.74,0.29,2.59-0.68,2.172-1.76-0.802-2.06-1.19-4.3-2.579-6.11-0.2-0.26,0.04-0.79-0.12-1.12-0.594-1.22-1.739-1.96-3.147-1.63-1.115,2.2,0.033,4.33,1.555,6.04,0.136,0.15-0.03,0.53,0.083,0.74z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g874" fill="#CCC">
|
||||||
|
<path id="path876" d="m-5.564,303.39c-0.108-0.38-0.146-0.84,0.019-1.16,0.531-1.03,1.324-2.15,0.987-3.18-0.348-1.05-1.464-0.87-2.114-0.3-1.135,0.99-1.184,2.82-1.875,4.18-0.196,0.38-0.145,0.96-0.586,1.35-0.474,0.42-0.914,1.94-0.818,2.51,0.053,0.32-0.13,10.22,0.092,9.96,0.619-0.73,3.669-10.47,3.738-11.36,0.057-0.73,0.789-1.19,0.557-2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g878" fill="#CCC">
|
||||||
|
<path id="path880" d="m-31.202,296.6c2.634-2.5,5.424-5.46,4.982-9.17-0.116-0.98-1.891-0.45-2.078,0.39-0.802,3.63-2.841,6.29-5.409,8.68-2.196,2.05-4.058,8.39-4.293,8.9,3.697-5.26,5.954-8,6.798-8.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g882" fill="#CCC">
|
||||||
|
<path id="path884" d="m-44.776,290.64c0.523-0.38,0.221-0.87,0.438-1.2,0.953-1.46,2.254-2.7,2.272-4.44,0.003-0.28-0.375-0.59-0.71-0.36-0.277,0.18-0.619,0.31-0.727,0.44-2.03,2.45-3.43,5.12-4.873,7.93-0.183,0.36-1.327,4.85-1.014,4.96,0.239,0.09,1.959-4.09,2.169-4.21,1.263-0.68,1.275-2.3,2.445-3.12z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g886" fill="#CCC">
|
||||||
|
<path id="path888" d="m-28.043,310.18c0.444-0.87,2.02-2.07,1.907-2.96-0.118-0.93,0.35-2.37-0.562-1.68-1.257,0.94-4.706,2.29-4.976,8.1-0.026,0.57,2.948-2.12,3.631-3.46z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g890" fill="#CCC">
|
||||||
|
<path id="path892" d="m-13.6,293c0.4-0.67,1.108-0.19,1.567-0.46,0.648-0.37,1.259-0.93,1.551-1.58,0.97-2.14,2.739-3.96,2.882-6.36-1.491-1.4-2.17,0.64-2.8,1.6-1.323-1.65-2.322,0.23-3.622,0.75-0.07,0.03-0.283-0.32-0.358-0.29-1.177,0.44-1.857,1.52-2.855,2.3-0.171,0.13-0.576-0.05-0.723,0.09-0.652,0.6-1.625,0.93-1.905,1.61-1.11,2.7-4.25,4.8-6.137,12.34,0.381,0.91,4.512-6.64,4.999-7.34,0.836-1.2,0.954,1.66,2.23,1,0.051-0.03,0.237,0.21,0.371,0.34,0.194-0.28,0.412-0.51,0.8-0.4,0-0.4-0.134-0.96,0.067-1.11,1.237-0.98,1.153-2.05,1.933-3.29,0.458,0.79,1.519,0.07,2,0.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g894" fill="#CCC">
|
||||||
|
<path id="path896" d="m46.2,347.4s7.4-20.4,3-31.6c0,0,11.4,21.6,6.8,32.8,0,0-0.4-10.4-4.4-15.4,0,0-4,12.8-5.4,14.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g898" fill="#CCC">
|
||||||
|
<path id="path900" d="m31.4,344.8s5.4-8.8-2.6-27.2c0,0-0.8,20.4-7.6,31.4,0,0,14.2-20.2,10.2-4.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g902" fill="#CCC">
|
||||||
|
<path id="path904" d="m21.4,342.8s-0.2-20,0.2-23c0,0-3.8,16.6-14,26.2,0,0,14.4-12,13.8-3.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g906" fill="#CCC">
|
||||||
|
<path id="path908" d="m11.8,310.8s6,13.6-4,32c0,0,6.4-12.2,1.6-19.2,0,0,2.6-3.4,2.4-12.8z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g910" fill="#CCC">
|
||||||
|
<path id="path912" d="m-7.4,342.4s-1-15.6,0.8-17.8c0,0,0.2-6.4-0.2-7.4,0,0,4-6.2,4.2,1.2,0,0,1.4,7.8,4.2,12.4,0,0,3.6,5.4,3.4,11.8,0,0-10-30.2-12.4-0.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g914" fill="#CCC">
|
||||||
|
<path id="path916" d="m-11,314.8s-6.6,10.8-8.4,29.8c0,0-1.4-6.2,2.4-20.6,0,0,4.2-15.4,6-9.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g918" fill="#CCC">
|
||||||
|
<path id="path920" d="m-32.8,334.6s5-5.4,6.4-10.4c0,0,3.6-15.8-2.8-7.2,0,0,0.2,8-8,15.4,0,0,4.8-2.4,4.4,2.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g922" fill="#CCC">
|
||||||
|
<path id="path924" d="m-38.6,329.6s3.4-17.4,4.2-18.2c0,0,1.8-3.4-1-0.2,0,0-8.8,19.2-12.8,25.8,0,0,8-9.2,9.6-7.4z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g926" fill="#CCC">
|
||||||
|
<path id="path928" d="m-44.4,313s11.6-22.4-10.2,3.4c0,0,11-9.8,10.2-3.4z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g930" fill="#CCC">
|
||||||
|
<path id="path932" d="m-59.8,298.4s4.8-18.8,7.4-18.6l1.6,1.6s-6,9.6-5.4,19.4c0,0-0.6-9.6-3.6-2.4z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g934" fill="#CCC">
|
||||||
|
<path id="path936" d="m270.5,287s-12-10-14.5-13.5c0,0,13.5,18.5,13.5,25.5,0,0,2.5-7.5,1-12z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g938" fill="#CCC">
|
||||||
|
<path id="path940" d="m276,265s-21-15-24.5-22.5c0,0,26.5,29.5,26.5,34,0,0,0.5-9-2-11.5z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g942" fill="#CCC">
|
||||||
|
<path id="path944" d="m293,111s-12-8-13.5-6c0,0,10.5,6.5,13,15,0,0-1.5-9,0.5-9z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g946" fill="#CCC">
|
||||||
|
<path id="path948" d="m301.5,191.5-17.5-12s19,17,19.5,21l-2-9z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g950" stroke="#000">
|
||||||
|
<path id="path952" d="m-89.25,169,22,4.75"/>
|
||||||
|
</g>
|
||||||
|
<g id="g954" stroke="#000">
|
||||||
|
<path id="path956" d="m-39,331s-0.5-3.5-9.5,7"/>
|
||||||
|
</g>
|
||||||
|
<g id="g958" stroke="#000">
|
||||||
|
<path id="path960" d="m-33.5,336s2-6.5-4.5-2"/>
|
||||||
|
</g>
|
||||||
|
<g id="g962" stroke="#000">
|
||||||
|
<path id="path964" d="m20.5,344.5s1.5-11-10,2"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 142 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 134 KiB |
After Width: | Height: | Size: 57 KiB |
After Width: | Height: | Size: 2.9 MiB |
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
@ -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))
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)<!--([\\s\\S]*?)-->")
|
||||||
|
svgRegex = regexp.MustCompile(`(?i)^\s*(?:<\?xml[^>]*>\s*)?(?:<!doctype svg[^>]*>\s*)?<svg[^>]*>[^*]*<\/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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
package bimg
|
||||||
|
|
||||||
|
// Version represents the current package semantic version.
|
||||||
|
const Version = "1.0.9"
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,532 @@
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <vips/vips.h>
|
||||||
|
#include <vips/foreign.h>
|
||||||
|
#include <vips/vips7compat.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|