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