github.com/neohugo/neohugo@v0.123.8/resources/images/image.go (about) 1 // Copyright 2019 The Hugo Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package images 15 16 import ( 17 "errors" 18 "fmt" 19 "image" 20 "image/color" 21 "image/draw" 22 "image/gif" 23 "image/jpeg" 24 "image/png" 25 "io" 26 "sync" 27 28 "github.com/bep/gowebp/libwebp/webpoptions" 29 "github.com/neohugo/neohugo/config" 30 "github.com/neohugo/neohugo/resources/images/webp" 31 32 "github.com/neohugo/neohugo/media" 33 "github.com/neohugo/neohugo/resources/images/exif" 34 35 "github.com/disintegration/gift" 36 "golang.org/x/image/bmp" 37 "golang.org/x/image/tiff" 38 39 "github.com/neohugo/neohugo/common/hugio" 40 ) 41 42 func NewImage(f Format, proc *ImageProcessor, img image.Image, s Spec) *Image { 43 if img != nil { 44 return &Image{ 45 Format: f, 46 Proc: proc, 47 Spec: s, 48 imageConfig: &imageConfig{ 49 config: imageConfigFromImage(img), 50 configLoaded: true, 51 }, 52 } 53 } 54 return &Image{Format: f, Proc: proc, Spec: s, imageConfig: &imageConfig{}} 55 } 56 57 type Image struct { 58 Format Format 59 Proc *ImageProcessor 60 Spec Spec 61 *imageConfig 62 } 63 64 func (i *Image) EncodeTo(conf ImageConfig, img image.Image, w io.Writer) error { 65 switch conf.TargetFormat { 66 case JPEG: 67 68 var rgba *image.RGBA 69 quality := conf.Quality 70 71 if nrgba, ok := img.(*image.NRGBA); ok { 72 if nrgba.Opaque() { 73 rgba = &image.RGBA{ 74 Pix: nrgba.Pix, 75 Stride: nrgba.Stride, 76 Rect: nrgba.Rect, 77 } 78 } 79 } 80 if rgba != nil { 81 return jpeg.Encode(w, rgba, &jpeg.Options{Quality: quality}) 82 } 83 return jpeg.Encode(w, img, &jpeg.Options{Quality: quality}) 84 case PNG: 85 encoder := png.Encoder{CompressionLevel: png.DefaultCompression} 86 return encoder.Encode(w, img) 87 88 case GIF: 89 if giphy, ok := img.(Giphy); ok { 90 g := giphy.GIF() 91 return gif.EncodeAll(w, g) 92 } 93 return gif.Encode(w, img, &gif.Options{ 94 NumColors: 256, 95 }) 96 case TIFF: 97 return tiff.Encode(w, img, &tiff.Options{Compression: tiff.Deflate, Predictor: true}) 98 99 case BMP: 100 return bmp.Encode(w, img) 101 case WEBP: 102 return webp.Encode( 103 w, 104 img, webpoptions.EncodingOptions{ 105 Quality: conf.Quality, 106 EncodingPreset: webpoptions.EncodingPreset(conf.Hint), 107 UseSharpYuv: true, 108 }, 109 ) 110 default: 111 return errors.New("format not supported") 112 } 113 } 114 115 // Height returns i's height. 116 func (i *Image) Height() int { 117 //nolint 118 i.initConfig() 119 return i.config.Height 120 } 121 122 // Width returns i's width. 123 func (i *Image) Width() int { 124 //nolint 125 i.initConfig() 126 return i.config.Width 127 } 128 129 func (i Image) WithImage(img image.Image) *Image { 130 i.Spec = nil 131 i.imageConfig = &imageConfig{ 132 config: imageConfigFromImage(img), 133 configLoaded: true, 134 } 135 136 return &i 137 } 138 139 func (i Image) WithSpec(s Spec) *Image { 140 i.Spec = s 141 i.imageConfig = &imageConfig{} 142 return &i 143 } 144 145 // InitConfig reads the image config from the given reader. 146 func (i *Image) InitConfig(r io.Reader) error { 147 var err error 148 i.configInit.Do(func() { 149 i.config, _, err = image.DecodeConfig(r) 150 }) 151 return err 152 } 153 154 func (i *Image) initConfig() error { 155 var err error 156 i.configInit.Do(func() { 157 if i.configLoaded { 158 return 159 } 160 161 var f hugio.ReadSeekCloser 162 163 f, err = i.Spec.ReadSeekCloser() 164 if err != nil { 165 return 166 } 167 defer f.Close() 168 169 i.config, _, err = image.DecodeConfig(f) 170 }) 171 172 if err != nil { 173 return fmt.Errorf("failed to load image config: %w", err) 174 } 175 176 return nil 177 } 178 179 func NewImageProcessor(cfg *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) (*ImageProcessor, error) { 180 e := cfg.Config.Imaging.Exif 181 exifDecoder, err := exif.NewDecoder( 182 exif.WithDateDisabled(e.DisableDate), 183 exif.WithLatLongDisabled(e.DisableLatLong), 184 exif.ExcludeFields(e.ExcludeFields), 185 exif.IncludeFields(e.IncludeFields), 186 ) 187 if err != nil { 188 return nil, err 189 } 190 191 return &ImageProcessor{ 192 Cfg: cfg, 193 exifDecoder: exifDecoder, 194 }, nil 195 } 196 197 type ImageProcessor struct { 198 Cfg *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal] 199 exifDecoder *exif.Decoder 200 } 201 202 func (p *ImageProcessor) DecodeExif(r io.Reader) (*exif.ExifInfo, error) { 203 return p.exifDecoder.Decode(r) 204 } 205 206 func (p *ImageProcessor) FiltersFromConfig(src image.Image, conf ImageConfig) ([]gift.Filter, error) { 207 var filters []gift.Filter 208 209 if conf.Rotate != 0 { 210 // Apply any rotation before any resize. 211 filters = append(filters, gift.Rotate(float32(conf.Rotate), color.Transparent, gift.NearestNeighborInterpolation)) 212 } 213 214 switch conf.Action { 215 case "resize": 216 filters = append(filters, gift.Resize(conf.Width, conf.Height, conf.Filter)) 217 case "crop": 218 if conf.AnchorStr == smartCropIdentifier { 219 bounds, err := p.smartCrop(src, conf.Width, conf.Height, conf.Filter) 220 if err != nil { 221 return nil, err 222 } 223 224 // First crop using the bounds returned by smartCrop. 225 filters = append(filters, gift.Crop(bounds)) 226 // Then center crop the image to get an image the desired size without resizing. 227 filters = append(filters, gift.CropToSize(conf.Width, conf.Height, gift.CenterAnchor)) 228 229 } else { 230 filters = append(filters, gift.CropToSize(conf.Width, conf.Height, conf.Anchor)) 231 } 232 case "fill": 233 if conf.AnchorStr == smartCropIdentifier { 234 bounds, err := p.smartCrop(src, conf.Width, conf.Height, conf.Filter) 235 if err != nil { 236 return nil, err 237 } 238 239 // First crop it, then resize it. 240 filters = append(filters, gift.Crop(bounds)) 241 filters = append(filters, gift.Resize(conf.Width, conf.Height, conf.Filter)) 242 243 } else { 244 filters = append(filters, gift.ResizeToFill(conf.Width, conf.Height, conf.Filter, conf.Anchor)) 245 } 246 case "fit": 247 filters = append(filters, gift.ResizeToFit(conf.Width, conf.Height, conf.Filter)) 248 default: 249 250 } 251 return filters, nil 252 } 253 254 func (p *ImageProcessor) ApplyFiltersFromConfig(src image.Image, conf ImageConfig) (image.Image, error) { 255 filters, err := p.FiltersFromConfig(src, conf) 256 if err != nil { 257 return nil, err 258 } 259 260 if len(filters) == 0 { 261 return p.resolveSrc(src, conf.TargetFormat), nil 262 } 263 264 img, err := p.doFilter(src, conf.TargetFormat, filters...) 265 if err != nil { 266 return nil, err 267 } 268 269 return img, nil 270 } 271 272 func (p *ImageProcessor) Filter(src image.Image, filters ...gift.Filter) (image.Image, error) { 273 return p.doFilter(src, 0, filters...) 274 } 275 276 func (p *ImageProcessor) resolveSrc(src image.Image, targetFormat Format) image.Image { 277 if giph, ok := src.(Giphy); ok { 278 g := giph.GIF() 279 if len(g.Image) < 2 || (targetFormat == 0 || targetFormat != GIF) { 280 src = g.Image[0] 281 } 282 } 283 return src 284 } 285 286 func (p *ImageProcessor) doFilter(src image.Image, targetFormat Format, filters ...gift.Filter) (image.Image, error) { 287 filter := gift.New(filters...) 288 289 if giph, ok := src.(Giphy); ok { 290 g := giph.GIF() 291 if len(g.Image) < 2 || (targetFormat == 0 || targetFormat != GIF) { 292 src = g.Image[0] 293 } else { 294 var bounds image.Rectangle 295 firstFrame := g.Image[0] 296 tmp := image.NewNRGBA(firstFrame.Bounds()) 297 for i := range g.Image { 298 gift.New().DrawAt(tmp, g.Image[i], g.Image[i].Bounds().Min, gift.OverOperator) 299 bounds = filter.Bounds(tmp.Bounds()) 300 dst := image.NewPaletted(bounds, g.Image[i].Palette) 301 filter.Draw(dst, tmp) 302 g.Image[i] = dst 303 } 304 g.Config.Width = bounds.Dx() 305 g.Config.Height = bounds.Dy() 306 307 return giph, nil 308 } 309 } 310 311 bounds := filter.Bounds(src.Bounds()) 312 313 var dst draw.Image 314 switch src.(type) { 315 case *image.RGBA: 316 dst = image.NewRGBA(bounds) 317 case *image.NRGBA: 318 dst = image.NewNRGBA(bounds) 319 case *image.Gray: 320 dst = image.NewGray(bounds) 321 default: 322 dst = image.NewNRGBA(bounds) 323 } 324 filter.Draw(dst, src) 325 326 return dst, nil 327 } 328 329 func GetDefaultImageConfig(action string, defaults *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) ImageConfig { 330 if defaults == nil { 331 defaults = defaultImageConfig 332 } 333 return ImageConfig{ 334 Action: action, 335 Hint: defaults.Config.Hint, 336 Quality: defaults.Config.Imaging.Quality, 337 } 338 } 339 340 type Spec interface { 341 // Loads the image source. 342 ReadSeekCloser() (hugio.ReadSeekCloser, error) 343 } 344 345 // Format is an image file format. 346 type Format int 347 348 const ( 349 JPEG Format = iota + 1 350 PNG 351 GIF 352 TIFF 353 BMP 354 WEBP 355 ) 356 357 // RequiresDefaultQuality returns if the default quality needs to be applied to 358 // images of this format. 359 func (f Format) RequiresDefaultQuality() bool { 360 return f == JPEG || f == WEBP 361 } 362 363 // SupportsTransparency reports whether it supports transparency in any form. 364 func (f Format) SupportsTransparency() bool { 365 return f != JPEG 366 } 367 368 // DefaultExtension returns the default file extension of this format, starting with a dot. 369 // For example: .jpg for JPEG 370 func (f Format) DefaultExtension() string { 371 return f.MediaType().FirstSuffix.FullSuffix 372 } 373 374 // MediaType returns the media type of this image, e.g. image/jpeg for JPEG 375 func (f Format) MediaType() media.Type { 376 switch f { 377 case JPEG: 378 return media.Builtin.JPEGType 379 case PNG: 380 return media.Builtin.PNGType 381 case GIF: 382 return media.Builtin.GIFType 383 case TIFF: 384 return media.Builtin.TIFFType 385 case BMP: 386 return media.Builtin.BMPType 387 case WEBP: 388 return media.Builtin.WEBPType 389 default: 390 panic(fmt.Sprintf("%d is not a valid image format", f)) 391 } 392 } 393 394 type imageConfig struct { 395 config image.Config 396 configInit sync.Once //nolint 397 configLoaded bool 398 } 399 400 func imageConfigFromImage(img image.Image) image.Config { 401 if giphy, ok := img.(Giphy); ok { 402 return giphy.GIF().Config 403 } 404 b := img.Bounds() 405 return image.Config{Width: b.Max.X, Height: b.Max.Y} 406 } 407 408 // UnwrapFilter unwraps the given filter if it is a filter wrapper. 409 func UnwrapFilter(in gift.Filter) gift.Filter { 410 if f, ok := in.(filter); ok { 411 return f.Filter 412 } 413 return in 414 } 415 416 // ToFilters converts the given input to a slice of gift.Filter. 417 func ToFilters(in any) []gift.Filter { 418 switch v := in.(type) { 419 case []gift.Filter: 420 return v 421 case []filter: 422 vv := make([]gift.Filter, len(v)) 423 for i, f := range v { 424 vv[i] = f 425 } 426 return vv 427 case gift.Filter: 428 return []gift.Filter{v} 429 default: 430 panic(fmt.Sprintf("%T is not an image filter", in)) 431 } 432 } 433 434 // IsOpaque returns false if the image has alpha channel and there is at least 1 435 // pixel that is not (fully) opaque. 436 func IsOpaque(img image.Image) bool { 437 if oim, ok := img.(interface { 438 Opaque() bool 439 }); ok { 440 return oim.Opaque() 441 } 442 443 return false 444 } 445 446 // ImageSource identifies and decodes an image. 447 type ImageSource interface { 448 DecodeImage() (image.Image, error) 449 Key() string 450 } 451 452 // Giphy represents a GIF Image that may be animated. 453 type Giphy interface { 454 image.Image // The first frame. 455 GIF() *gif.GIF // All frames. 456 }