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 }