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 README
This commit is contained in:
Robert Swain 2017-06-07 01:12:49 +02:00 committed by GitHub
parent def49400bc
commit 2d202cec07
73 changed files with 10027 additions and 83 deletions

View file

@ -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

View file

@ -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, "LOG_DIR": logDir,
"DATABASE": dataSource, "CONFIG_PATH": configPath,
"LOG_DIR": logDir, "ServerName": cfg.ServerName,
"MAX_FILE_SIZE_BYTES": maxFileSizeBytes, "AbsBasePath": cfg.AbsBasePath,
"SERVER_NAME": serverName, "MaxFileSizeBytes": *cfg.MaxFileSizeBytes,
}).Info("Starting mediaapi") "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
}

View file

@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View file

@ -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"`
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

@ -35,28 +35,40 @@ 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),
req = util.RequestWithLogging(req) )
r0mux.Handle("/thumbnail/{serverName}/{mediaId}",
// Set common headers returned regardless of the outcome of the request makeDownloadAPI("thumbnail", cfg, db, activeRemoteRequests, activeThumbnailGeneration),
util.SetCORSHeaders(w)
// Content-Type will be overridden in case of returning file data, else we respond with JSON-formatted errors
w.Header().Set("Content-Type", "application/json")
vars := mux.Vars(req)
writers.Download(w, req, gomatrixserverlib.ServerName(vars["serverName"]), types.MediaID(vars["mediaId"]), cfg, db, activeRemoteRequests)
})),
) )
servMux.Handle("/metrics", prometheus.Handler()) servMux.Handle("/metrics", prometheus.Handler())
servMux.Handle("/api/", http.StripPrefix("/api", apiMux)) servMux.Handle("/api/", http.StripPrefix("/api", apiMux))
} }
func makeDownloadAPI(name string, cfg *config.MediaAPI, db *storage.Database, activeRemoteRequests *types.ActiveRemoteRequests, activeThumbnailGeneration *types.ActiveThumbnailGeneration) http.HandlerFunc {
return prometheus.InstrumentHandler(name, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req = util.RequestWithLogging(req)
// Set common headers returned regardless of the outcome of the request
util.SetCORSHeaders(w)
// Content-Type will be overridden in case of returning file data, else we respond with JSON-formatted errors
w.Header().Set("Content-Type", "application/json")
vars := mux.Vars(req)
writers.Download(w, req, gomatrixserverlib.ServerName(vars["serverName"]), types.MediaID(vars["mediaId"]), cfg, db, activeRemoteRequests, activeThumbnailGeneration, name == "thumbnail")
}))
}

View file

@ -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
} }

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
Logger *log.Entry IsThumbnailRequest bool
ThumbnailSize types.ThumbnailSize
Logger *log.Entry
} }
// Download implements /download // Download implements /download amd /thumbnail
// Files from this server (i.e. origin == cfg.ServerName) are served directly // Files from 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
} }
r.Logger.WithFields(log.Fields{ var responseFile *os.File
"UploadName": r.MediaMetadata.UploadName, var responseMetadata *types.MediaMetadata
"Base64Hash": r.MediaMetadata.Base64Hash, if r.IsThumbnailRequest {
"FileSizeBytes": r.MediaMetadata.FileSizeBytes, thumbFile, thumbMetadata, resErr := r.getThumbnailFile(types.Path(filePath), activeThumbnailGeneration, maxThumbnailGenerators, db, dynamicThumbnails, thumbnailSizes)
"Content-Type": r.MediaMetadata.ContentType, if thumbFile != nil {
}).Info("Responding with file") defer thumbFile.Close()
}
if resErr != nil {
return resErr
}
if thumbFile == nil {
r.Logger.WithFields(log.Fields{
"UploadName": r.MediaMetadata.UploadName,
"Base64Hash": r.MediaMetadata.Base64Hash,
"FileSizeBytes": r.MediaMetadata.FileSizeBytes,
"ContentType": r.MediaMetadata.ContentType,
}).Info("No good thumbnail found. Responding with original file.")
responseFile = file
responseMetadata = r.MediaMetadata
} else {
r.Logger.Info("Responding with thumbnail")
responseFile = thumbFile
responseMetadata = thumbMetadata.MediaMetadata
}
} else {
r.Logger.WithFields(log.Fields{
"UploadName": r.MediaMetadata.UploadName,
"Base64Hash": r.MediaMetadata.Base64Hash,
"FileSizeBytes": r.MediaMetadata.FileSizeBytes,
"ContentType": r.MediaMetadata.ContentType,
}).Info("Responding with file")
responseFile = file
responseMetadata = r.MediaMetadata
}
w.Header().Set("Content-Type", string(r.MediaMetadata.ContentType)) w.Header().Set("Content-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

View file

@ -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
} }

18
vendor/manifest vendored
View file

@ -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",

View file

@ -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.

View file

@ -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.

View file

@ -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)
}
}
}

View file

@ -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()
}
}
}

View file

@ -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
}

View file

@ -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)
}
}
}

View file

@ -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()
}
}
}

View file

@ -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))
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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,
)
}
}
}

227
vendor/src/github.com/nfnt/resize/ycc.go vendored Normal file
View file

@ -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
}

View file

@ -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
}
}
}
}
}
}
}
}

View file

@ -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

View file

@ -0,0 +1,8 @@
test:
@go test
bench:
@go test -bench=.
.PHONY: bench test

View file

@ -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

View file

@ -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
}

View file

@ -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")
}
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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.

View file

@ -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.

View file

@ -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)

View file

@ -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)
}

View file

@ -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)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 KiB

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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))
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}
}
}

View file

@ -0,0 +1,4 @@
package bimg
// Version represents the current package semantic version.
const Version = "1.0.9"

View file

@ -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
}

532
vendor/src/gopkg.in/h2non/bimg.v1/vips.h vendored Normal file
View file

@ -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
}

View file

@ -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
}