image.go (9500B)
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 "fmt" 18 "image" 19 "image/color" 20 "image/draw" 21 "image/gif" 22 "image/jpeg" 23 "image/png" 24 "io" 25 "sync" 26 27 "github.com/bep/gowebp/libwebp/webpoptions" 28 "github.com/gohugoio/hugo/resources/images/webp" 29 30 "github.com/gohugoio/hugo/media" 31 "github.com/gohugoio/hugo/resources/images/exif" 32 33 "github.com/disintegration/gift" 34 "golang.org/x/image/bmp" 35 "golang.org/x/image/tiff" 36 37 "errors" 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 ImagingConfig) (*ImageProcessor, error) { 178 e := cfg.Cfg.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 ImagingConfig 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) ApplyFiltersFromConfig(src image.Image, conf ImageConfig) (image.Image, 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 return nil, fmt.Errorf("unsupported action: %q", conf.Action) 248 } 249 250 img, err := p.Filter(src, filters...) 251 if err != nil { 252 return nil, err 253 } 254 255 return img, nil 256 } 257 258 func (p *ImageProcessor) Filter(src image.Image, filters ...gift.Filter) (image.Image, error) { 259 260 filter := gift.New(filters...) 261 262 if giph, ok := src.(Giphy); ok && len(giph.GIF().Image) > 1 { 263 g := giph.GIF() 264 var bounds image.Rectangle 265 firstFrame := g.Image[0] 266 tmp := image.NewNRGBA(firstFrame.Bounds()) 267 for i := range g.Image { 268 gift.New().DrawAt(tmp, g.Image[i], g.Image[i].Bounds().Min, gift.OverOperator) 269 bounds = filter.Bounds(tmp.Bounds()) 270 dst := image.NewPaletted(bounds, g.Image[i].Palette) 271 filter.Draw(dst, tmp) 272 g.Image[i] = dst 273 } 274 g.Config.Width = bounds.Dx() 275 g.Config.Height = bounds.Dy() 276 277 return giph, nil 278 } 279 280 bounds := filter.Bounds(src.Bounds()) 281 282 var dst draw.Image 283 switch src.(type) { 284 case *image.RGBA: 285 dst = image.NewRGBA(bounds) 286 case *image.NRGBA: 287 dst = image.NewNRGBA(bounds) 288 case *image.Gray: 289 dst = image.NewGray(bounds) 290 default: 291 dst = image.NewNRGBA(bounds) 292 } 293 filter.Draw(dst, src) 294 295 return dst, nil 296 } 297 298 func GetDefaultImageConfig(action string, defaults ImagingConfig) ImageConfig { 299 return ImageConfig{ 300 Action: action, 301 Hint: defaults.Hint, 302 Quality: defaults.Cfg.Quality, 303 } 304 } 305 306 type Spec interface { 307 // Loads the image source. 308 ReadSeekCloser() (hugio.ReadSeekCloser, error) 309 } 310 311 // Format is an image file format. 312 type Format int 313 314 const ( 315 JPEG Format = iota + 1 316 PNG 317 GIF 318 TIFF 319 BMP 320 WEBP 321 ) 322 323 // RequiresDefaultQuality returns if the default quality needs to be applied to 324 // images of this format. 325 func (f Format) RequiresDefaultQuality() bool { 326 return f == JPEG || f == WEBP 327 } 328 329 // SupportsTransparency reports whether it supports transparency in any form. 330 func (f Format) SupportsTransparency() bool { 331 return f != JPEG 332 } 333 334 // DefaultExtension returns the default file extension of this format, starting with a dot. 335 // For example: .jpg for JPEG 336 func (f Format) DefaultExtension() string { 337 return f.MediaType().FirstSuffix.FullSuffix 338 } 339 340 // MediaType returns the media type of this image, e.g. image/jpeg for JPEG 341 func (f Format) MediaType() media.Type { 342 switch f { 343 case JPEG: 344 return media.JPEGType 345 case PNG: 346 return media.PNGType 347 case GIF: 348 return media.GIFType 349 case TIFF: 350 return media.TIFFType 351 case BMP: 352 return media.BMPType 353 case WEBP: 354 return media.WEBPType 355 default: 356 panic(fmt.Sprintf("%d is not a valid image format", f)) 357 } 358 } 359 360 type imageConfig struct { 361 config image.Config 362 configInit sync.Once 363 configLoaded bool 364 } 365 366 func imageConfigFromImage(img image.Image) image.Config { 367 b := img.Bounds() 368 return image.Config{Width: b.Max.X, Height: b.Max.Y} 369 } 370 371 func ToFilters(in any) []gift.Filter { 372 switch v := in.(type) { 373 case []gift.Filter: 374 return v 375 case []filter: 376 vv := make([]gift.Filter, len(v)) 377 for i, f := range v { 378 vv[i] = f 379 } 380 return vv 381 case gift.Filter: 382 return []gift.Filter{v} 383 default: 384 panic(fmt.Sprintf("%T is not an image filter", in)) 385 } 386 } 387 388 // IsOpaque returns false if the image has alpha channel and there is at least 1 389 // pixel that is not (fully) opaque. 390 func IsOpaque(img image.Image) bool { 391 if oim, ok := img.(interface { 392 Opaque() bool 393 }); ok { 394 return oim.Opaque() 395 } 396 397 return false 398 } 399 400 // ImageSource identifies and decodes an image. 401 type ImageSource interface { 402 DecodeImage() (image.Image, error) 403 Key() string 404 } 405 406 // Giphy represents a GIF Image that may be animated. 407 type Giphy interface { 408 image.Image // The first frame. 409 GIF() *gif.GIF // All frames. 410 }