Files
opencloud/vendor/github.com/kovidgoyal/imaging/io.go
dependabot[bot] 85361fca67 build(deps): bump github.com/kovidgoyal/imaging from 1.7.2 to 1.8.17
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>
2025-11-26 14:46:30 +01:00

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
}