github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/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 "github.com/gohugoio/hugo/common/hugio" 38 "github.com/pkg/errors" 39 ) 40 41 func NewImage(f Format, proc *ImageProcessor, img image.Image, s Spec) *Image { 42 if img != nil { 43 return &Image{ 44 Format: f, 45 Proc: proc, 46 Spec: s, 47 imageConfig: &imageConfig{ 48 config: imageConfigFromImage(img), 49 configLoaded: true, 50 }, 51 } 52 } 53 return &Image{Format: f, Proc: proc, Spec: s, imageConfig: &imageConfig{}} 54 } 55 56 type Image struct { 57 Format Format 58 Proc *ImageProcessor 59 Spec Spec 60 *imageConfig 61 } 62 63 func (i *Image) EncodeTo(conf ImageConfig, img image.Image, w io.Writer) error { 64 switch conf.TargetFormat { 65 case JPEG: 66 67 var rgba *image.RGBA 68 quality := conf.Quality 69 70 if nrgba, ok := img.(*image.NRGBA); ok { 71 if nrgba.Opaque() { 72 rgba = &image.RGBA{ 73 Pix: nrgba.Pix, 74 Stride: nrgba.Stride, 75 Rect: nrgba.Rect, 76 } 77 } 78 } 79 if rgba != nil { 80 return jpeg.Encode(w, rgba, &jpeg.Options{Quality: quality}) 81 } 82 return jpeg.Encode(w, img, &jpeg.Options{Quality: quality}) 83 case PNG: 84 encoder := png.Encoder{CompressionLevel: png.DefaultCompression} 85 return encoder.Encode(w, img) 86 87 case GIF: 88 return gif.Encode(w, img, &gif.Options{ 89 NumColors: 256, 90 }) 91 case TIFF: 92 return tiff.Encode(w, img, &tiff.Options{Compression: tiff.Deflate, Predictor: true}) 93 94 case BMP: 95 return bmp.Encode(w, img) 96 case WEBP: 97 return webp.Encode( 98 w, 99 img, webpoptions.EncodingOptions{ 100 Quality: conf.Quality, 101 EncodingPreset: webpoptions.EncodingPreset(conf.Hint), 102 UseSharpYuv: true, 103 }, 104 ) 105 default: 106 return errors.New("format not supported") 107 } 108 } 109 110 // Height returns i's height. 111 func (i *Image) Height() int { 112 i.initConfig() 113 return i.config.Height 114 } 115 116 // Width returns i's width. 117 func (i *Image) Width() int { 118 i.initConfig() 119 return i.config.Width 120 } 121 122 func (i Image) WithImage(img image.Image) *Image { 123 i.Spec = nil 124 i.imageConfig = &imageConfig{ 125 config: imageConfigFromImage(img), 126 configLoaded: true, 127 } 128 129 return &i 130 } 131 132 func (i Image) WithSpec(s Spec) *Image { 133 i.Spec = s 134 i.imageConfig = &imageConfig{} 135 return &i 136 } 137 138 // InitConfig reads the image config from the given reader. 139 func (i *Image) InitConfig(r io.Reader) error { 140 var err error 141 i.configInit.Do(func() { 142 i.config, _, err = image.DecodeConfig(r) 143 }) 144 return err 145 } 146 147 func (i *Image) initConfig() error { 148 var err error 149 i.configInit.Do(func() { 150 if i.configLoaded { 151 return 152 } 153 154 var f hugio.ReadSeekCloser 155 156 f, err = i.Spec.ReadSeekCloser() 157 if err != nil { 158 return 159 } 160 defer f.Close() 161 162 i.config, _, err = image.DecodeConfig(f) 163 }) 164 165 if err != nil { 166 return errors.Wrap(err, "failed to load image config") 167 } 168 169 return nil 170 } 171 172 func NewImageProcessor(cfg ImagingConfig) (*ImageProcessor, error) { 173 e := cfg.Cfg.Exif 174 exifDecoder, err := exif.NewDecoder( 175 exif.WithDateDisabled(e.DisableDate), 176 exif.WithLatLongDisabled(e.DisableLatLong), 177 exif.ExcludeFields(e.ExcludeFields), 178 exif.IncludeFields(e.IncludeFields), 179 ) 180 if err != nil { 181 return nil, err 182 } 183 184 return &ImageProcessor{ 185 Cfg: cfg, 186 exifDecoder: exifDecoder, 187 }, nil 188 } 189 190 type ImageProcessor struct { 191 Cfg ImagingConfig 192 exifDecoder *exif.Decoder 193 } 194 195 func (p *ImageProcessor) DecodeExif(r io.Reader) (*exif.Exif, error) { 196 return p.exifDecoder.Decode(r) 197 } 198 199 func (p *ImageProcessor) ApplyFiltersFromConfig(src image.Image, conf ImageConfig) (image.Image, error) { 200 var filters []gift.Filter 201 202 if conf.Rotate != 0 { 203 // Apply any rotation before any resize. 204 filters = append(filters, gift.Rotate(float32(conf.Rotate), color.Transparent, gift.NearestNeighborInterpolation)) 205 } 206 207 switch conf.Action { 208 case "resize": 209 filters = append(filters, gift.Resize(conf.Width, conf.Height, conf.Filter)) 210 case "crop": 211 if conf.AnchorStr == smartCropIdentifier { 212 bounds, err := p.smartCrop(src, conf.Width, conf.Height, conf.Filter) 213 if err != nil { 214 return nil, err 215 } 216 217 // First crop using the bounds returned by smartCrop. 218 filters = append(filters, gift.Crop(bounds)) 219 // Then center crop the image to get an image the desired size without resizing. 220 filters = append(filters, gift.CropToSize(conf.Width, conf.Height, gift.CenterAnchor)) 221 222 } else { 223 filters = append(filters, gift.CropToSize(conf.Width, conf.Height, conf.Anchor)) 224 } 225 case "fill": 226 if conf.AnchorStr == smartCropIdentifier { 227 bounds, err := p.smartCrop(src, conf.Width, conf.Height, conf.Filter) 228 if err != nil { 229 return nil, err 230 } 231 232 // First crop it, then resize it. 233 filters = append(filters, gift.Crop(bounds)) 234 filters = append(filters, gift.Resize(conf.Width, conf.Height, conf.Filter)) 235 236 } else { 237 filters = append(filters, gift.ResizeToFill(conf.Width, conf.Height, conf.Filter, conf.Anchor)) 238 } 239 case "fit": 240 filters = append(filters, gift.ResizeToFit(conf.Width, conf.Height, conf.Filter)) 241 default: 242 return nil, errors.Errorf("unsupported action: %q", conf.Action) 243 } 244 245 img, err := p.Filter(src, filters...) 246 if err != nil { 247 return nil, err 248 } 249 250 return img, nil 251 } 252 253 func (p *ImageProcessor) Filter(src image.Image, filters ...gift.Filter) (image.Image, error) { 254 g := gift.New(filters...) 255 bounds := g.Bounds(src.Bounds()) 256 var dst draw.Image 257 switch src.(type) { 258 case *image.RGBA: 259 dst = image.NewRGBA(bounds) 260 case *image.NRGBA: 261 dst = image.NewNRGBA(bounds) 262 case *image.Gray: 263 dst = image.NewGray(bounds) 264 default: 265 dst = image.NewNRGBA(bounds) 266 } 267 g.Draw(dst, src) 268 return dst, nil 269 } 270 271 func GetDefaultImageConfig(action string, defaults ImagingConfig) ImageConfig { 272 return ImageConfig{ 273 Action: action, 274 Hint: defaults.Hint, 275 Quality: defaults.Cfg.Quality, 276 } 277 } 278 279 type Spec interface { 280 // Loads the image source. 281 ReadSeekCloser() (hugio.ReadSeekCloser, error) 282 } 283 284 // Format is an image file format. 285 type Format int 286 287 const ( 288 JPEG Format = iota + 1 289 PNG 290 GIF 291 TIFF 292 BMP 293 WEBP 294 ) 295 296 // RequiresDefaultQuality returns if the default quality needs to be applied to 297 // images of this format. 298 func (f Format) RequiresDefaultQuality() bool { 299 return f == JPEG || f == WEBP 300 } 301 302 // SupportsTransparency reports whether it supports transparency in any form. 303 func (f Format) SupportsTransparency() bool { 304 return f != JPEG 305 } 306 307 // DefaultExtension returns the default file extension of this format, starting with a dot. 308 // For example: .jpg for JPEG 309 func (f Format) DefaultExtension() string { 310 return f.MediaType().FirstSuffix.FullSuffix 311 } 312 313 // MediaType returns the media type of this image, e.g. image/jpeg for JPEG 314 func (f Format) MediaType() media.Type { 315 switch f { 316 case JPEG: 317 return media.JPEGType 318 case PNG: 319 return media.PNGType 320 case GIF: 321 return media.GIFType 322 case TIFF: 323 return media.TIFFType 324 case BMP: 325 return media.BMPType 326 case WEBP: 327 return media.WEBPType 328 default: 329 panic(fmt.Sprintf("%d is not a valid image format", f)) 330 } 331 } 332 333 type imageConfig struct { 334 config image.Config 335 configInit sync.Once 336 configLoaded bool 337 } 338 339 func imageConfigFromImage(img image.Image) image.Config { 340 b := img.Bounds() 341 return image.Config{Width: b.Max.X, Height: b.Max.Y} 342 } 343 344 func ToFilters(in interface{}) []gift.Filter { 345 switch v := in.(type) { 346 case []gift.Filter: 347 return v 348 case []filter: 349 vv := make([]gift.Filter, len(v)) 350 for i, f := range v { 351 vv[i] = f 352 } 353 return vv 354 case gift.Filter: 355 return []gift.Filter{v} 356 default: 357 panic(fmt.Sprintf("%T is not an image filter", in)) 358 } 359 } 360 361 // IsOpaque returns false if the image has alpha channel and there is at least 1 362 // pixel that is not (fully) opaque. 363 func IsOpaque(img image.Image) bool { 364 if oim, ok := img.(interface { 365 Opaque() bool 366 }); ok { 367 return oim.Opaque() 368 } 369 370 return false 371 } 372 373 // ImageSource identifies and decodes an image. 374 type ImageSource interface { 375 DecodeImage() (image.Image, error) 376 Key() string 377 }