mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-02-14 16:21:18 -05:00
Bumps [github.com/kovidgoyal/imaging](https://github.com/kovidgoyal/imaging) from 1.7.2 to 1.8.17. - [Release notes](https://github.com/kovidgoyal/imaging/releases) - [Changelog](https://github.com/kovidgoyal/imaging/blob/master/.goreleaser.yaml) - [Commits](https://github.com/kovidgoyal/imaging/compare/v1.7.2...v1.8.17) --- updated-dependencies: - dependency-name: github.com/kovidgoyal/imaging dependency-version: 1.8.17 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
695 lines
18 KiB
Go
695 lines
18 KiB
Go
package imaging
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"image/draw"
|
|
"image/gif"
|
|
"image/jpeg"
|
|
"image/png"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/kovidgoyal/imaging/apng"
|
|
myjpeg "github.com/kovidgoyal/imaging/jpeg"
|
|
"github.com/kovidgoyal/imaging/magick"
|
|
_ "github.com/kovidgoyal/imaging/netpbm"
|
|
"github.com/kovidgoyal/imaging/prism/meta"
|
|
"github.com/kovidgoyal/imaging/prism/meta/autometa"
|
|
"github.com/kovidgoyal/imaging/prism/meta/icc"
|
|
"github.com/kovidgoyal/imaging/prism/meta/tiffmeta"
|
|
"github.com/kovidgoyal/imaging/streams"
|
|
"github.com/kovidgoyal/imaging/types"
|
|
"github.com/kovidgoyal/imaging/webp"
|
|
|
|
"github.com/rwcarlsen/goexif/exif"
|
|
exif_tiff "github.com/rwcarlsen/goexif/tiff"
|
|
|
|
"golang.org/x/image/bmp"
|
|
"golang.org/x/image/tiff"
|
|
_ "golang.org/x/image/webp"
|
|
)
|
|
|
|
type fileSystem interface {
|
|
Create(string) (io.WriteCloser, error)
|
|
Open(string) (*os.File, error)
|
|
}
|
|
|
|
type localFS struct{}
|
|
|
|
func (localFS) Create(name string) (io.WriteCloser, error) { return os.Create(name) }
|
|
func (localFS) Open(name string) (*os.File, error) { return os.Open(name) }
|
|
|
|
var mockable_fs fileSystem = localFS{}
|
|
|
|
type Backend int
|
|
|
|
const (
|
|
GO_IMAGE Backend = iota
|
|
MAGICK_IMAGE
|
|
)
|
|
|
|
func (b Backend) String() string {
|
|
switch b {
|
|
case GO_IMAGE:
|
|
return "GO_IMAGE"
|
|
case MAGICK_IMAGE:
|
|
return "MAGICK_IMAGE"
|
|
}
|
|
return fmt.Sprintf("UNKNOWN_IMAGE_TYPE_%d", b)
|
|
}
|
|
|
|
type ColorSpaceType int
|
|
|
|
const (
|
|
NO_CHANGE_OF_COLORSPACE ColorSpaceType = iota
|
|
SRGB_COLORSPACE
|
|
)
|
|
const (
|
|
Relative icc.RenderingIntent = icc.RelativeColorimetricRenderingIntent
|
|
Perceptual = icc.PerceptualRenderingIntent
|
|
Saturation = icc.SaturationRenderingIntent
|
|
Absolute = icc.AbsoluteColorimetricRenderingIntent
|
|
)
|
|
|
|
type ResizeCallbackFunction func(w, h int) (nw, nh int)
|
|
|
|
type decodeConfig struct {
|
|
autoOrientation bool
|
|
outputColorspace ColorSpaceType
|
|
transform types.TransformType
|
|
resize ResizeCallbackFunction
|
|
background *color.RGBA64
|
|
backends []Backend
|
|
rendering_intent icc.RenderingIntent
|
|
use_blackpoint_compensation bool
|
|
}
|
|
|
|
// DecodeOption sets an optional parameter for the Decode and Open functions.
|
|
type DecodeOption func(*decodeConfig)
|
|
|
|
// AutoOrientation returns a DecodeOption that sets the auto-orientation mode.
|
|
// If auto-orientation is enabled, the image will be transformed after decoding
|
|
// according to the EXIF orientation tag (if present). By default it's enabled.
|
|
func AutoOrientation(enabled bool) DecodeOption {
|
|
return func(c *decodeConfig) {
|
|
c.autoOrientation = enabled
|
|
}
|
|
}
|
|
|
|
// ColorSpace returns a DecodeOption that sets the colorspace that the
|
|
// opened image will be in. Defaults to sRGB. If the image has an embedded ICC
|
|
// color profile it is automatically used to convert colors to sRGB if needed.
|
|
func ColorSpace(cs ColorSpaceType) DecodeOption {
|
|
return func(c *decodeConfig) {
|
|
c.outputColorspace = cs
|
|
}
|
|
}
|
|
|
|
// Specify a transform to perform on the image when loading it
|
|
func Transform(t types.TransformType) DecodeOption {
|
|
return func(c *decodeConfig) {
|
|
c.transform = t
|
|
}
|
|
}
|
|
|
|
// Specify a background color onto which the image should be composed when loading it
|
|
func Background(bg color.Color) DecodeOption {
|
|
return func(c *decodeConfig) {
|
|
nbg := color.RGBA64Model.Convert(bg).(color.RGBA64)
|
|
c.background = &nbg
|
|
}
|
|
}
|
|
|
|
// Specify a callback function to decide a new size for the image when loading it
|
|
func ResizeCallback(f ResizeCallbackFunction) DecodeOption {
|
|
return func(c *decodeConfig) {
|
|
c.resize = f
|
|
}
|
|
}
|
|
|
|
// Specify which backends to use to try to load the image, successively. If no backends are specified, the default
|
|
// set are used.
|
|
func Backends(backends ...Backend) DecodeOption {
|
|
return func(c *decodeConfig) {
|
|
c.backends = backends
|
|
}
|
|
}
|
|
|
|
// Set the rendering intent to use for ICC profile based color conversions
|
|
func RenderingIntent(intent icc.RenderingIntent) DecodeOption {
|
|
return func(c *decodeConfig) {
|
|
c.rendering_intent = intent
|
|
}
|
|
}
|
|
|
|
// Set whether to use blackpoint compensation during ICC profile color conversions
|
|
func BlackpointCompensation(enable bool) DecodeOption {
|
|
return func(c *decodeConfig) {
|
|
c.use_blackpoint_compensation = enable
|
|
}
|
|
}
|
|
|
|
func NewDecodeConfig(opts ...DecodeOption) (cfg *decodeConfig) {
|
|
cfg = &decodeConfig{
|
|
autoOrientation: true,
|
|
outputColorspace: SRGB_COLORSPACE,
|
|
transform: types.NoTransform,
|
|
backends: []Backend{GO_IMAGE, MAGICK_IMAGE},
|
|
// These settings match ImageMagick defaults as of v6
|
|
rendering_intent: Relative,
|
|
use_blackpoint_compensation: true,
|
|
}
|
|
default_backends := cfg.backends
|
|
for _, option := range opts {
|
|
option(cfg)
|
|
}
|
|
if len(cfg.backends) == 0 {
|
|
cfg.backends = default_backends
|
|
}
|
|
return
|
|
}
|
|
|
|
func (cfg *decodeConfig) magick_callback(w, h int) (ro magick.RenderOptions) {
|
|
ro.AutoOrient = cfg.autoOrientation
|
|
if cfg.resize != nil {
|
|
nw, nh := cfg.resize(w, h)
|
|
if nw != w || nh != h {
|
|
ro.ResizeTo.X, ro.ResizeTo.Y = nw, nh
|
|
}
|
|
}
|
|
ro.Background = cfg.background
|
|
ro.ToSRGB = cfg.outputColorspace == SRGB_COLORSPACE
|
|
ro.Transform = cfg.transform
|
|
ro.RenderingIntent = cfg.rendering_intent
|
|
ro.BlackpointCompensation = cfg.use_blackpoint_compensation
|
|
return
|
|
}
|
|
|
|
// orientation is an EXIF flag that specifies the transformation
|
|
// that should be applied to image to display it correctly.
|
|
type orientation int
|
|
|
|
const (
|
|
orientationUnspecified = 0
|
|
orientationNormal = 1
|
|
orientationFlipH = 2
|
|
orientationRotate180 = 3
|
|
orientationFlipV = 4
|
|
orientationTranspose = 5
|
|
orientationRotate270 = 6
|
|
orientationTransverse = 7
|
|
orientationRotate90 = 8
|
|
)
|
|
|
|
func fix_colors(images []*Frame, md *meta.Data, cfg *decodeConfig) error {
|
|
var err error
|
|
if md == nil || cfg.outputColorspace != SRGB_COLORSPACE {
|
|
return nil
|
|
}
|
|
if md.CICP.IsSet && !md.CICP.IsSRGB() {
|
|
p := md.CICP.PipelineToSRGB()
|
|
if p == nil {
|
|
return fmt.Errorf("cannot convert colorspace, unknown %s", md.CICP)
|
|
}
|
|
for _, f := range images {
|
|
if f.Image, err = convert(p, f.Image); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
profile, err := md.ICCProfile()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if profile != nil {
|
|
for _, f := range images {
|
|
if f.Image, err = ConvertToSRGB(profile, cfg.rendering_intent, cfg.use_blackpoint_compensation, f.Image); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fix_orientation(ans *Image, md *meta.Data, cfg *decodeConfig) error {
|
|
if md == nil || !cfg.autoOrientation {
|
|
return nil
|
|
}
|
|
exif_data, err := md.Exif()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var oval orientation = orientationUnspecified
|
|
if exif_data != nil {
|
|
orient, err := exif_data.Get(exif.Orientation)
|
|
if err == nil && orient != nil && orient.Format() == exif_tiff.IntVal {
|
|
if x, err := orient.Int(0); err == nil && x > 0 && x < 9 {
|
|
oval = orientation(x)
|
|
}
|
|
}
|
|
}
|
|
switch oval {
|
|
case orientationNormal, orientationUnspecified:
|
|
case orientationFlipH:
|
|
ans.FlipH()
|
|
case orientationFlipV:
|
|
ans.FlipV()
|
|
case orientationRotate90:
|
|
ans.Rotate90()
|
|
case orientationRotate180:
|
|
ans.Rotate180()
|
|
case orientationRotate270:
|
|
ans.Rotate270()
|
|
case orientationTranspose:
|
|
ans.Transpose()
|
|
case orientationTransverse:
|
|
ans.Transverse()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (img *Image) Transform(t types.TransformType) {
|
|
switch t {
|
|
case types.TransverseTransform:
|
|
img.Transverse()
|
|
case types.TransposeTransform:
|
|
img.Transpose()
|
|
case types.FlipHTransform:
|
|
img.FlipH()
|
|
case types.FlipVTransform:
|
|
img.FlipV()
|
|
case types.Rotate90Transform:
|
|
img.Rotate90()
|
|
case types.Rotate180Transform:
|
|
img.Rotate180()
|
|
case types.Rotate270Transform:
|
|
img.Rotate270()
|
|
}
|
|
}
|
|
|
|
const (
|
|
NoTransform = types.NoTransform
|
|
FlipHTransform = types.FlipHTransform
|
|
FlipVTransform = types.FlipVTransform
|
|
Rotate90Transform = types.Rotate90Transform
|
|
Rotate180Transform = types.Rotate180Transform
|
|
Rotate270Transform = types.Rotate270Transform
|
|
TransverseTransform = types.TransverseTransform
|
|
TransposeTransform = types.TransposeTransform
|
|
)
|
|
|
|
func format_from_decode_result(x string) Format {
|
|
switch x {
|
|
case "BMP":
|
|
return BMP
|
|
case "TIFF", "TIF":
|
|
return TIFF
|
|
}
|
|
return UNKNOWN
|
|
}
|
|
|
|
func decode_all_go(r io.Reader, md *meta.Data, cfg *decodeConfig) (ans *Image, err error) {
|
|
defer func() {
|
|
if ans == nil || err != nil || ans.Metadata == nil {
|
|
return
|
|
}
|
|
if cfg.outputColorspace != NO_CHANGE_OF_COLORSPACE {
|
|
if err = fix_colors(ans.Frames, ans.Metadata, cfg); err != nil {
|
|
return
|
|
}
|
|
}
|
|
if cfg.autoOrientation {
|
|
if err = fix_orientation(ans, ans.Metadata, cfg); err != nil {
|
|
return
|
|
}
|
|
}
|
|
if cfg.background != nil {
|
|
ans.PasteOntoBackground(*cfg.background)
|
|
}
|
|
if cfg.transform != types.NoTransform {
|
|
ans.Transform(cfg.transform)
|
|
}
|
|
if cfg.resize != nil {
|
|
w, h := ans.Bounds().Dx(), ans.Bounds().Dy()
|
|
nw, nh := cfg.resize(w, h)
|
|
if nw != w || nh != h {
|
|
ans.Resize(nw, nh, Lanczos)
|
|
}
|
|
}
|
|
ans.Metadata.PixelWidth = uint32(ans.Bounds().Dx())
|
|
ans.Metadata.PixelHeight = uint32(ans.Bounds().Dy())
|
|
}()
|
|
if md == nil {
|
|
img, imgf, err := image.Decode(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
m := meta.Data{
|
|
PixelWidth: uint32(img.Bounds().Dx()),
|
|
PixelHeight: uint32(img.Bounds().Dy()),
|
|
Format: format_from_decode_result(imgf),
|
|
BitsPerComponent: tiffmeta.BitsPerComponent(img.ColorModel()),
|
|
}
|
|
f := Frame{Image: img}
|
|
return &Image{Metadata: &m, Frames: []*Frame{&f}}, nil
|
|
}
|
|
ans = &Image{Metadata: md}
|
|
if md.HasFrames {
|
|
switch md.Format {
|
|
case GIF:
|
|
g, err := gif.DecodeAll(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ans.populate_from_gif(g)
|
|
case PNG:
|
|
png, err := apng.DecodeAll(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ans.populate_from_apng(&png)
|
|
case WEBP:
|
|
wp, err := webp.DecodeAnimated(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ans.populate_from_webp(wp)
|
|
}
|
|
ans.Metadata.NumFrames = len(ans.Frames)
|
|
ans.Metadata.NumPlays = int(ans.LoopCount)
|
|
} else {
|
|
var img image.Image
|
|
switch md.Format {
|
|
case JPEG:
|
|
img, err = myjpeg.Decode(r)
|
|
case PNG:
|
|
img, err = apng.Decode(r)
|
|
default:
|
|
img, _, err = image.Decode(r)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ans.Metadata.PixelWidth = uint32(img.Bounds().Dx())
|
|
ans.Metadata.PixelHeight = uint32(img.Bounds().Dy())
|
|
ans.Frames = append(ans.Frames, &Frame{Image: img})
|
|
}
|
|
return
|
|
}
|
|
|
|
func decode_all_magick(inp *types.Input, md *meta.Data, cfg *decodeConfig) (ans *Image, err error) {
|
|
mi, err := magick.OpenAll(inp, md, cfg.magick_callback)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ans = &Image{Metadata: md}
|
|
for _, f := range mi.Frames {
|
|
fr := &Frame{
|
|
Number: uint(f.Number), TopLeft: image.Pt(f.Left, f.Top), Image: f.Img,
|
|
Delay: time.Millisecond * time.Duration(f.Delay_ms), ComposeOnto: uint(f.Compose_onto),
|
|
Replace: f.Replace,
|
|
}
|
|
ans.Frames = append(ans.Frames, fr)
|
|
}
|
|
if md != nil {
|
|
// in case of transforms/auto-orient
|
|
b := ans.Bounds()
|
|
md.PixelWidth, md.PixelHeight = uint32(b.Dx()), uint32(b.Dy())
|
|
}
|
|
return
|
|
}
|
|
|
|
func decode_all(inp *types.Input, opts []DecodeOption) (ans *Image, err error) {
|
|
cfg := NewDecodeConfig(opts...)
|
|
if !magick.HasMagick() {
|
|
cfg.backends = slices.DeleteFunc(cfg.backends, func(b Backend) bool { return b == MAGICK_IMAGE })
|
|
}
|
|
if len(cfg.backends) == 0 {
|
|
return nil, fmt.Errorf("the magick command was not found in PATH")
|
|
}
|
|
|
|
if inp.Reader == nil {
|
|
var f *os.File
|
|
f, err = mockable_fs.Open(inp.Path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer f.Close()
|
|
inp.Reader = f
|
|
}
|
|
var md *meta.Data
|
|
md, inp.Reader, err = autometa.Load(inp.Reader)
|
|
if err != nil {
|
|
return
|
|
}
|
|
var backend_err error
|
|
for _, backend := range cfg.backends {
|
|
switch backend {
|
|
case GO_IMAGE:
|
|
inp.Reader, backend_err = streams.CallbackWithSeekable(inp.Reader, func(r io.Reader) (err error) {
|
|
ans, err = decode_all_go(r, md, cfg)
|
|
return
|
|
})
|
|
case MAGICK_IMAGE:
|
|
inp.Reader, backend_err = streams.CallbackWithSeekable(inp.Reader, func(r io.Reader) (err error) {
|
|
i := *inp
|
|
i.Reader = r
|
|
ans, err = decode_all_magick(&i, md, cfg)
|
|
return
|
|
})
|
|
}
|
|
if backend_err == nil && ans != nil {
|
|
return
|
|
}
|
|
}
|
|
if ans == nil && backend_err == nil {
|
|
backend_err = fmt.Errorf("unrecognised image format")
|
|
}
|
|
return ans, backend_err
|
|
}
|
|
|
|
// Decode image from r including all animation frames if its an animated image.
|
|
// Returns nil with no error when no supported image is found in r.
|
|
// Also returns a reader that will yield all bytes from r so that this API does
|
|
// not exhaust r.
|
|
func DecodeAll(r io.Reader, opts ...DecodeOption) (ans *Image, s io.Reader, err error) {
|
|
inp := &types.Input{Reader: r}
|
|
ans, err = decode_all(inp, opts)
|
|
s = inp.Reader
|
|
return
|
|
}
|
|
|
|
func (ans *Image) SingleFrame() image.Image {
|
|
if ans.DefaultImage != nil {
|
|
return ans.DefaultImage
|
|
}
|
|
return ans.Frames[0].Image
|
|
|
|
}
|
|
|
|
// Decode reads an image from r.
|
|
func Decode(r io.Reader, opts ...DecodeOption) (image.Image, error) {
|
|
ans, _, err := DecodeAll(r, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ans.SingleFrame(), nil
|
|
}
|
|
|
|
// Open loads an image from file.
|
|
//
|
|
// Examples:
|
|
//
|
|
// // Load an image from file.
|
|
// img, err := imaging.Open("test.jpg")
|
|
func Open(filename string, opts ...DecodeOption) (image.Image, error) {
|
|
ans, err := OpenAll(filename, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ans.SingleFrame(), nil
|
|
}
|
|
|
|
func OpenAll(filename string, opts ...DecodeOption) (*Image, error) {
|
|
return decode_all(&types.Input{Path: filename}, opts)
|
|
}
|
|
|
|
func OpenConfig(filename string) (ans image.Config, format_name string, err error) {
|
|
file, err := mockable_fs.Open(filename)
|
|
if err != nil {
|
|
return ans, "", err
|
|
}
|
|
defer file.Close()
|
|
return image.DecodeConfig(file)
|
|
}
|
|
|
|
type Format = types.Format
|
|
|
|
const (
|
|
UNKNOWN = types.UNKNOWN
|
|
JPEG = types.JPEG
|
|
PNG = types.PNG
|
|
GIF = types.GIF
|
|
TIFF = types.TIFF
|
|
WEBP = types.WEBP
|
|
BMP = types.BMP
|
|
PBM = types.PBM
|
|
PGM = types.PGM
|
|
PPM = types.PPM
|
|
PAM = types.PAM
|
|
)
|
|
|
|
// ErrUnsupportedFormat means the given image format is not supported.
|
|
var ErrUnsupportedFormat = errors.New("imaging: unsupported image format")
|
|
|
|
// FormatFromExtension parses image format from filename extension:
|
|
// "jpg" (or "jpeg"), "png", "gif", "tif" (or "tiff") and "bmp" are supported.
|
|
func FormatFromExtension(ext string) (Format, error) {
|
|
if f, ok := types.FormatExts[strings.ToLower(strings.TrimPrefix(ext, "."))]; ok {
|
|
return f, nil
|
|
}
|
|
return -1, ErrUnsupportedFormat
|
|
}
|
|
|
|
// FormatFromFilename parses image format from filename:
|
|
// "jpg" (or "jpeg"), "png", "gif", "tif" (or "tiff") and "bmp" are supported.
|
|
func FormatFromFilename(filename string) (Format, error) {
|
|
ext := filepath.Ext(filename)
|
|
return FormatFromExtension(ext)
|
|
}
|
|
|
|
type encodeConfig struct {
|
|
jpegQuality int
|
|
gifNumColors int
|
|
gifQuantizer draw.Quantizer
|
|
gifDrawer draw.Drawer
|
|
pngCompressionLevel png.CompressionLevel
|
|
}
|
|
|
|
var defaultEncodeConfig = encodeConfig{
|
|
jpegQuality: 95,
|
|
gifNumColors: 256,
|
|
gifQuantizer: nil,
|
|
gifDrawer: nil,
|
|
pngCompressionLevel: png.DefaultCompression,
|
|
}
|
|
|
|
// EncodeOption sets an optional parameter for the Encode and Save functions.
|
|
type EncodeOption func(*encodeConfig)
|
|
|
|
// JPEGQuality returns an EncodeOption that sets the output JPEG quality.
|
|
// Quality ranges from 1 to 100 inclusive, higher is better. Default is 95.
|
|
func JPEGQuality(quality int) EncodeOption {
|
|
return func(c *encodeConfig) {
|
|
c.jpegQuality = quality
|
|
}
|
|
}
|
|
|
|
// GIFNumColors returns an EncodeOption that sets the maximum number of colors
|
|
// used in the GIF-encoded image. It ranges from 1 to 256. Default is 256.
|
|
func GIFNumColors(numColors int) EncodeOption {
|
|
return func(c *encodeConfig) {
|
|
c.gifNumColors = numColors
|
|
}
|
|
}
|
|
|
|
// GIFQuantizer returns an EncodeOption that sets the quantizer that is used to produce
|
|
// a palette of the GIF-encoded image.
|
|
func GIFQuantizer(quantizer draw.Quantizer) EncodeOption {
|
|
return func(c *encodeConfig) {
|
|
c.gifQuantizer = quantizer
|
|
}
|
|
}
|
|
|
|
// GIFDrawer returns an EncodeOption that sets the drawer that is used to convert
|
|
// the source image to the desired palette of the GIF-encoded image.
|
|
func GIFDrawer(drawer draw.Drawer) EncodeOption {
|
|
return func(c *encodeConfig) {
|
|
c.gifDrawer = drawer
|
|
}
|
|
}
|
|
|
|
// PNGCompressionLevel returns an EncodeOption that sets the compression level
|
|
// of the PNG-encoded image. Default is png.DefaultCompression.
|
|
func PNGCompressionLevel(level png.CompressionLevel) EncodeOption {
|
|
return func(c *encodeConfig) {
|
|
c.pngCompressionLevel = level
|
|
}
|
|
}
|
|
|
|
// Encode writes the image img to w in the specified format (JPEG, PNG, GIF, TIFF or BMP).
|
|
func Encode(w io.Writer, img image.Image, format Format, opts ...EncodeOption) error {
|
|
cfg := defaultEncodeConfig
|
|
for _, option := range opts {
|
|
option(&cfg)
|
|
}
|
|
|
|
switch format {
|
|
case JPEG:
|
|
if nrgba, ok := img.(*image.NRGBA); ok && IsOpaque(nrgba) {
|
|
rgba := &image.RGBA{
|
|
Pix: nrgba.Pix,
|
|
Stride: nrgba.Stride,
|
|
Rect: nrgba.Rect,
|
|
}
|
|
return jpeg.Encode(w, rgba, &jpeg.Options{Quality: cfg.jpegQuality})
|
|
}
|
|
return jpeg.Encode(w, img, &jpeg.Options{Quality: cfg.jpegQuality})
|
|
|
|
case PNG:
|
|
encoder := png.Encoder{CompressionLevel: cfg.pngCompressionLevel}
|
|
return encoder.Encode(w, img)
|
|
|
|
case GIF:
|
|
return gif.Encode(w, img, &gif.Options{
|
|
NumColors: cfg.gifNumColors,
|
|
Quantizer: cfg.gifQuantizer,
|
|
Drawer: cfg.gifDrawer,
|
|
})
|
|
|
|
case TIFF:
|
|
return tiff.Encode(w, img, &tiff.Options{Compression: tiff.Deflate, Predictor: true})
|
|
|
|
case BMP:
|
|
return bmp.Encode(w, img)
|
|
}
|
|
|
|
return ErrUnsupportedFormat
|
|
}
|
|
|
|
// Save saves the image to file with the specified filename.
|
|
// The format is determined from the filename extension:
|
|
// "jpg" (or "jpeg"), "png", "gif", "tif" (or "tiff") and "bmp" are supported.
|
|
//
|
|
// Examples:
|
|
//
|
|
// // Save the image as PNG.
|
|
// err := imaging.Save(img, "out.png")
|
|
//
|
|
// // Save the image as JPEG with optional quality parameter set to 80.
|
|
// err := imaging.Save(img, "out.jpg", imaging.JPEGQuality(80))
|
|
func Save(img image.Image, filename string, opts ...EncodeOption) (err error) {
|
|
f, err := FormatFromFilename(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
file, err := mockable_fs.Create(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = Encode(file, img, f, opts...)
|
|
errc := file.Close()
|
|
if err == nil {
|
|
err = errc
|
|
}
|
|
return err
|
|
}
|