github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/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 "fill": 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 it, then resize it. 218 filters = append(filters, gift.Crop(bounds)) 219 filters = append(filters, gift.Resize(conf.Width, conf.Height, conf.Filter)) 220 221 } else { 222 filters = append(filters, gift.ResizeToFill(conf.Width, conf.Height, conf.Filter, conf.Anchor)) 223 } 224 case "fit": 225 filters = append(filters, gift.ResizeToFit(conf.Width, conf.Height, conf.Filter)) 226 default: 227 return nil, errors.Errorf("unsupported action: %q", conf.Action) 228 } 229 230 img, err := p.Filter(src, filters...) 231 if err != nil { 232 return nil, err 233 } 234 235 return img, nil 236 } 237 238 func (p *ImageProcessor) Filter(src image.Image, filters ...gift.Filter) (image.Image, error) { 239 g := gift.New(filters...) 240 bounds := g.Bounds(src.Bounds()) 241 var dst draw.Image 242 switch src.(type) { 243 case *image.RGBA: 244 dst = image.NewRGBA(bounds) 245 case *image.NRGBA: 246 dst = image.NewNRGBA(bounds) 247 case *image.Gray: 248 dst = image.NewGray(bounds) 249 default: 250 dst = image.NewNRGBA(bounds) 251 } 252 g.Draw(dst, src) 253 return dst, nil 254 } 255 256 func GetDefaultImageConfig(action string, defaults ImagingConfig) ImageConfig { 257 return ImageConfig{ 258 Action: action, 259 Hint: defaults.Hint, 260 Quality: defaults.Cfg.Quality, 261 } 262 } 263 264 type Spec interface { 265 // Loads the image source. 266 ReadSeekCloser() (hugio.ReadSeekCloser, error) 267 } 268 269 // Format is an image file format. 270 type Format int 271 272 const ( 273 JPEG Format = iota + 1 274 PNG 275 GIF 276 TIFF 277 BMP 278 WEBP 279 ) 280 281 // RequiresDefaultQuality returns if the default quality needs to be applied to 282 // images of this format. 283 func (f Format) RequiresDefaultQuality() bool { 284 return f == JPEG || f == WEBP 285 } 286 287 // SupportsTransparency reports whether it supports transparency in any form. 288 func (f Format) SupportsTransparency() bool { 289 return f != JPEG 290 } 291 292 // DefaultExtension returns the default file extension of this format, starting with a dot. 293 // For example: .jpg for JPEG 294 func (f Format) DefaultExtension() string { 295 return f.MediaType().FirstSuffix.FullSuffix 296 } 297 298 // MediaType returns the media type of this image, e.g. image/jpeg for JPEG 299 func (f Format) MediaType() media.Type { 300 switch f { 301 case JPEG: 302 return media.JPEGType 303 case PNG: 304 return media.PNGType 305 case GIF: 306 return media.GIFType 307 case TIFF: 308 return media.TIFFType 309 case BMP: 310 return media.BMPType 311 case WEBP: 312 return media.WEBPType 313 default: 314 panic(fmt.Sprintf("%d is not a valid image format", f)) 315 } 316 } 317 318 type imageConfig struct { 319 config image.Config 320 configInit sync.Once 321 configLoaded bool 322 } 323 324 func imageConfigFromImage(img image.Image) image.Config { 325 b := img.Bounds() 326 return image.Config{Width: b.Max.X, Height: b.Max.Y} 327 } 328 329 func ToFilters(in interface{}) []gift.Filter { 330 switch v := in.(type) { 331 case []gift.Filter: 332 return v 333 case []filter: 334 vv := make([]gift.Filter, len(v)) 335 for i, f := range v { 336 vv[i] = f 337 } 338 return vv 339 case gift.Filter: 340 return []gift.Filter{v} 341 default: 342 panic(fmt.Sprintf("%T is not an image filter", in)) 343 } 344 } 345 346 // IsOpaque returns false if the image has alpha channel and there is at least 1 347 // pixel that is not (fully) opaque. 348 func IsOpaque(img image.Image) bool { 349 if oim, ok := img.(interface { 350 Opaque() bool 351 }); ok { 352 return oim.Opaque() 353 } 354 355 return false 356 } 357 358 // ImageSource identifies and decodes an image. 359 type ImageSource interface { 360 DecodeImage() (image.Image, error) 361 Key() string 362 }