github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/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/gohugoio/hugo/config" 30 "github.com/gohugoio/hugo/resources/images/webp" 31 32 "github.com/gohugoio/hugo/media" 33 "github.com/gohugoio/hugo/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/gohugoio/hugo/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 i.initConfig() 118 return i.config.Height 119 } 120 121 // Width returns i's width. 122 func (i *Image) Width() int { 123 i.initConfig() 124 return i.config.Width 125 } 126 127 func (i Image) WithImage(img image.Image) *Image { 128 i.Spec = nil 129 i.imageConfig = &imageConfig{ 130 config: imageConfigFromImage(img), 131 configLoaded: true, 132 } 133 134 return &i 135 } 136 137 func (i Image) WithSpec(s Spec) *Image { 138 i.Spec = s 139 i.imageConfig = &imageConfig{} 140 return &i 141 } 142 143 // InitConfig reads the image config from the given reader. 144 func (i *Image) InitConfig(r io.Reader) error { 145 var err error 146 i.configInit.Do(func() { 147 i.config, _, err = image.DecodeConfig(r) 148 }) 149 return err 150 } 151 152 func (i *Image) initConfig() error { 153 var err error 154 i.configInit.Do(func() { 155 if i.configLoaded { 156 return 157 } 158 159 var f hugio.ReadSeekCloser 160 161 f, err = i.Spec.ReadSeekCloser() 162 if err != nil { 163 return 164 } 165 defer f.Close() 166 167 i.config, _, err = image.DecodeConfig(f) 168 }) 169 170 if err != nil { 171 return fmt.Errorf("failed to load image config: %w", err) 172 } 173 174 return nil 175 } 176 177 func NewImageProcessor(cfg *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) (*ImageProcessor, error) { 178 e := cfg.Config.Imaging.Exif 179 exifDecoder, err := exif.NewDecoder( 180 exif.WithDateDisabled(e.DisableDate), 181 exif.WithLatLongDisabled(e.DisableLatLong), 182 exif.ExcludeFields(e.ExcludeFields), 183 exif.IncludeFields(e.IncludeFields), 184 ) 185 if err != nil { 186 return nil, err 187 } 188 189 return &ImageProcessor{ 190 Cfg: cfg, 191 exifDecoder: exifDecoder, 192 }, nil 193 } 194 195 type ImageProcessor struct { 196 Cfg *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal] 197 exifDecoder *exif.Decoder 198 } 199 200 func (p *ImageProcessor) DecodeExif(r io.Reader) (*exif.ExifInfo, error) { 201 return p.exifDecoder.Decode(r) 202 } 203 204 func (p *ImageProcessor) FiltersFromConfig(src image.Image, conf ImageConfig) ([]gift.Filter, error) { 205 var filters []gift.Filter 206 207 if conf.Rotate != 0 { 208 // Apply any rotation before any resize. 209 filters = append(filters, gift.Rotate(float32(conf.Rotate), color.Transparent, gift.NearestNeighborInterpolation)) 210 } 211 212 switch conf.Action { 213 case "resize": 214 filters = append(filters, gift.Resize(conf.Width, conf.Height, conf.Filter)) 215 case "crop": 216 if conf.AnchorStr == smartCropIdentifier { 217 bounds, err := p.smartCrop(src, conf.Width, conf.Height, conf.Filter) 218 if err != nil { 219 return nil, err 220 } 221 222 // First crop using the bounds returned by smartCrop. 223 filters = append(filters, gift.Crop(bounds)) 224 // Then center crop the image to get an image the desired size without resizing. 225 filters = append(filters, gift.CropToSize(conf.Width, conf.Height, gift.CenterAnchor)) 226 227 } else { 228 filters = append(filters, gift.CropToSize(conf.Width, conf.Height, conf.Anchor)) 229 } 230 case "fill": 231 if conf.AnchorStr == smartCropIdentifier { 232 bounds, err := p.smartCrop(src, conf.Width, conf.Height, conf.Filter) 233 if err != nil { 234 return nil, err 235 } 236 237 // First crop it, then resize it. 238 filters = append(filters, gift.Crop(bounds)) 239 filters = append(filters, gift.Resize(conf.Width, conf.Height, conf.Filter)) 240 241 } else { 242 filters = append(filters, gift.ResizeToFill(conf.Width, conf.Height, conf.Filter, conf.Anchor)) 243 } 244 case "fit": 245 filters = append(filters, gift.ResizeToFit(conf.Width, conf.Height, conf.Filter)) 246 default: 247 248 } 249 return filters, nil 250 } 251 252 func (p *ImageProcessor) ApplyFiltersFromConfig(src image.Image, conf ImageConfig) (image.Image, error) { 253 filters, err := p.FiltersFromConfig(src, conf) 254 if err != nil { 255 return nil, err 256 } 257 258 if len(filters) == 0 { 259 return p.resolveSrc(src, conf.TargetFormat), nil 260 } 261 262 img, err := p.doFilter(src, conf.TargetFormat, filters...) 263 if err != nil { 264 return nil, err 265 } 266 267 return img, nil 268 } 269 270 func (p *ImageProcessor) Filter(src image.Image, filters ...gift.Filter) (image.Image, error) { 271 return p.doFilter(src, 0, filters...) 272 } 273 274 func (p *ImageProcessor) resolveSrc(src image.Image, targetFormat Format) image.Image { 275 if giph, ok := src.(Giphy); ok { 276 g := giph.GIF() 277 if len(g.Image) < 2 || (targetFormat == 0 || targetFormat != GIF) { 278 src = g.Image[0] 279 } 280 } 281 return src 282 } 283 284 func (p *ImageProcessor) doFilter(src image.Image, targetFormat Format, filters ...gift.Filter) (image.Image, error) { 285 filter := gift.New(filters...) 286 287 if giph, ok := src.(Giphy); ok { 288 g := giph.GIF() 289 if len(g.Image) < 2 || (targetFormat == 0 || targetFormat != GIF) { 290 src = g.Image[0] 291 } else { 292 var bounds image.Rectangle 293 firstFrame := g.Image[0] 294 tmp := image.NewNRGBA(firstFrame.Bounds()) 295 for i := range g.Image { 296 gift.New().DrawAt(tmp, g.Image[i], g.Image[i].Bounds().Min, gift.OverOperator) 297 bounds = filter.Bounds(tmp.Bounds()) 298 dst := image.NewPaletted(bounds, g.Image[i].Palette) 299 filter.Draw(dst, tmp) 300 g.Image[i] = dst 301 } 302 g.Config.Width = bounds.Dx() 303 g.Config.Height = bounds.Dy() 304 305 return giph, nil 306 } 307 308 } 309 310 bounds := filter.Bounds(src.Bounds()) 311 312 var dst draw.Image 313 switch src.(type) { 314 case *image.RGBA: 315 dst = image.NewRGBA(bounds) 316 case *image.NRGBA: 317 dst = image.NewNRGBA(bounds) 318 case *image.Gray: 319 dst = image.NewGray(bounds) 320 default: 321 dst = image.NewNRGBA(bounds) 322 } 323 filter.Draw(dst, src) 324 325 return dst, nil 326 } 327 328 func GetDefaultImageConfig(action string, defaults *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) ImageConfig { 329 if defaults == nil { 330 defaults = defaultImageConfig 331 } 332 return ImageConfig{ 333 Action: action, 334 Hint: defaults.Config.Hint, 335 Quality: defaults.Config.Imaging.Quality, 336 } 337 } 338 339 type Spec interface { 340 // Loads the image source. 341 ReadSeekCloser() (hugio.ReadSeekCloser, error) 342 } 343 344 // Format is an image file format. 345 type Format int 346 347 const ( 348 JPEG Format = iota + 1 349 PNG 350 GIF 351 TIFF 352 BMP 353 WEBP 354 ) 355 356 // RequiresDefaultQuality returns if the default quality needs to be applied to 357 // images of this format. 358 func (f Format) RequiresDefaultQuality() bool { 359 return f == JPEG || f == WEBP 360 } 361 362 // SupportsTransparency reports whether it supports transparency in any form. 363 func (f Format) SupportsTransparency() bool { 364 return f != JPEG 365 } 366 367 // DefaultExtension returns the default file extension of this format, starting with a dot. 368 // For example: .jpg for JPEG 369 func (f Format) DefaultExtension() string { 370 return f.MediaType().FirstSuffix.FullSuffix 371 } 372 373 // MediaType returns the media type of this image, e.g. image/jpeg for JPEG 374 func (f Format) MediaType() media.Type { 375 switch f { 376 case JPEG: 377 return media.Builtin.JPEGType 378 case PNG: 379 return media.Builtin.PNGType 380 case GIF: 381 return media.Builtin.GIFType 382 case TIFF: 383 return media.Builtin.TIFFType 384 case BMP: 385 return media.Builtin.BMPType 386 case WEBP: 387 return media.Builtin.WEBPType 388 default: 389 panic(fmt.Sprintf("%d is not a valid image format", f)) 390 } 391 } 392 393 type imageConfig struct { 394 config image.Config 395 configInit sync.Once 396 configLoaded bool 397 } 398 399 func imageConfigFromImage(img image.Image) image.Config { 400 if giphy, ok := img.(Giphy); ok { 401 return giphy.GIF().Config 402 } 403 b := img.Bounds() 404 return image.Config{Width: b.Max.X, Height: b.Max.Y} 405 } 406 407 // UnwrapFilter unwraps the given filter if it is a filter wrapper. 408 func UnwrapFilter(in gift.Filter) gift.Filter { 409 if f, ok := in.(filter); ok { 410 return f.Filter 411 } 412 return in 413 } 414 415 // ToFilters converts the given input to a slice of gift.Filter. 416 func ToFilters(in any) []gift.Filter { 417 switch v := in.(type) { 418 case []gift.Filter: 419 return v 420 case []filter: 421 vv := make([]gift.Filter, len(v)) 422 for i, f := range v { 423 vv[i] = f 424 } 425 return vv 426 case gift.Filter: 427 return []gift.Filter{v} 428 default: 429 panic(fmt.Sprintf("%T is not an image filter", in)) 430 } 431 } 432 433 // IsOpaque returns false if the image has alpha channel and there is at least 1 434 // pixel that is not (fully) opaque. 435 func IsOpaque(img image.Image) bool { 436 if oim, ok := img.(interface { 437 Opaque() bool 438 }); ok { 439 return oim.Opaque() 440 } 441 442 return false 443 } 444 445 // ImageSource identifies and decodes an image. 446 type ImageSource interface { 447 DecodeImage() (image.Image, error) 448 Key() string 449 } 450 451 // Giphy represents a GIF Image that may be animated. 452 type Giphy interface { 453 image.Image // The first frame. 454 GIF() *gif.GIF // All frames. 455 }