github.com/neohugo/neohugo@v0.123.8/resources/images/filters.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 provides template functions for manipulating images.
    15  package images
    16  
    17  import (
    18  	"fmt"
    19  	"image/color"
    20  	"strings"
    21  
    22  	"github.com/makeworld-the-better-one/dither/v2"
    23  	"github.com/mitchellh/mapstructure"
    24  	"github.com/neohugo/neohugo/common/hugio"
    25  	"github.com/neohugo/neohugo/common/maps"
    26  	"github.com/neohugo/neohugo/resources/resource"
    27  
    28  	"github.com/disintegration/gift"
    29  	"github.com/spf13/cast"
    30  )
    31  
    32  // Increment for re-generation of images using these filters.
    33  const filterAPIVersion = 0
    34  
    35  type Filters struct{}
    36  
    37  // Process creates a filter that processes an image using the given specification.
    38  func (*Filters) Process(spec any) gift.Filter {
    39  	return filter{
    40  		Options: newFilterOpts(spec),
    41  		Filter: processFilter{
    42  			spec: cast.ToString(spec),
    43  		},
    44  	}
    45  }
    46  
    47  // Overlay creates a filter that overlays src at position x y.
    48  func (*Filters) Overlay(src ImageSource, x, y any) gift.Filter {
    49  	return filter{
    50  		Options: newFilterOpts(src.Key(), x, y),
    51  		Filter:  overlayFilter{src: src, x: cast.ToInt(x), y: cast.ToInt(y)},
    52  	}
    53  }
    54  
    55  // Opacity creates a filter that changes the opacity of an image.
    56  // The opacity parameter must be in range (0, 1).
    57  func (*Filters) Opacity(opacity any) gift.Filter {
    58  	return filter{
    59  		Options: newFilterOpts(opacity),
    60  		Filter:  opacityFilter{opacity: cast.ToFloat32(opacity)},
    61  	}
    62  }
    63  
    64  // Text creates a filter that draws text with the given options.
    65  func (*Filters) Text(text string, options ...any) gift.Filter {
    66  	tf := textFilter{
    67  		text:        text,
    68  		color:       "#ffffff",
    69  		size:        20,
    70  		x:           10,
    71  		y:           10,
    72  		linespacing: 2,
    73  	}
    74  
    75  	var opt maps.Params
    76  	if len(options) > 0 {
    77  		opt = maps.MustToParamsAndPrepare(options[0])
    78  		for option, v := range opt {
    79  			switch option {
    80  			case "color":
    81  				tf.color = cast.ToString(v)
    82  			case "size":
    83  				tf.size = cast.ToFloat64(v)
    84  			case "x":
    85  				tf.x = cast.ToInt(v)
    86  			case "y":
    87  				tf.y = cast.ToInt(v)
    88  			case "linespacing":
    89  				tf.linespacing = cast.ToInt(v)
    90  			case "font":
    91  				if err, ok := v.(error); ok {
    92  					panic(fmt.Sprintf("invalid font source: %s", err))
    93  				}
    94  				fontSource, ok1 := v.(hugio.ReadSeekCloserProvider)
    95  				identifier, ok2 := v.(resource.Identifier)
    96  
    97  				if !(ok1 && ok2) {
    98  					panic(fmt.Sprintf("invalid text font source: %T", v))
    99  				}
   100  
   101  				tf.fontSource = fontSource
   102  
   103  				// The input value isn't hashable and will not make a stable key.
   104  				// Replace it with a string in the map used as basis for the
   105  				// hash string.
   106  				opt["font"] = identifier.Key()
   107  
   108  			}
   109  		}
   110  	}
   111  
   112  	return filter{
   113  		Options: newFilterOpts(text, opt),
   114  		Filter:  tf,
   115  	}
   116  }
   117  
   118  // Padding creates a filter that resizes the image canvas without resizing the
   119  // image. The last argument is the canvas color, expressed as an RGB or RGBA
   120  // hexadecimal color. The default value is `ffffffff` (opaque white). The
   121  // preceding arguments are the padding values, in pixels, using the CSS
   122  // shorthand property syntax. Negative padding values will crop the image. The
   123  // signature is images.Padding V1 [V2] [V3] [V4] [COLOR].
   124  func (*Filters) Padding(args ...any) gift.Filter {
   125  	if len(args) < 1 || len(args) > 5 {
   126  		panic("the padding filter requires between 1 and 5 arguments")
   127  	}
   128  
   129  	var top, right, bottom, left int
   130  	var ccolor color.Color = color.White // canvas color
   131  	var err error
   132  
   133  	_args := args // preserve original args for most stable hash
   134  
   135  	if vcs, ok := (args[len(args)-1]).(string); ok {
   136  		ccolor, err = hexStringToColor(vcs)
   137  		if err != nil {
   138  			panic("invalid canvas color: specify RGB or RGBA using hex notation")
   139  		}
   140  		args = args[:len(args)-1]
   141  		if len(args) == 0 {
   142  			panic("not enough arguments: provide one or more padding values using the CSS shorthand property syntax")
   143  		}
   144  	}
   145  
   146  	var vals []int
   147  	for _, v := range args {
   148  		vi := cast.ToInt(v)
   149  		if vi > 5000 {
   150  			panic("padding values must not exceed 5000 pixels")
   151  		}
   152  		vals = append(vals, vi)
   153  	}
   154  
   155  	switch len(args) {
   156  	case 1:
   157  		top, right, bottom, left = vals[0], vals[0], vals[0], vals[0]
   158  	case 2:
   159  		top, right, bottom, left = vals[0], vals[1], vals[0], vals[1]
   160  	case 3:
   161  		top, right, bottom, left = vals[0], vals[1], vals[2], vals[1]
   162  	case 4:
   163  		top, right, bottom, left = vals[0], vals[1], vals[2], vals[3]
   164  	default:
   165  		panic(fmt.Sprintf("too many padding values: received %d, expected maximum of 4", len(args)))
   166  	}
   167  
   168  	return filter{
   169  		Options: newFilterOpts(_args...),
   170  		Filter: paddingFilter{
   171  			top:    top,
   172  			right:  right,
   173  			bottom: bottom,
   174  			left:   left,
   175  			ccolor: ccolor,
   176  		},
   177  	}
   178  }
   179  
   180  // Dither creates a filter that dithers an image.
   181  func (*Filters) Dither(options ...any) gift.Filter {
   182  	ditherOptions := struct {
   183  		Colors     []string
   184  		Method     string
   185  		Serpentine bool
   186  		Strength   float32
   187  	}{
   188  		Colors:     []string{"000000ff", "ffffffff"},
   189  		Method:     "floydsteinberg",
   190  		Serpentine: true,
   191  		Strength:   1.0,
   192  	}
   193  
   194  	if len(options) != 0 {
   195  		err := mapstructure.WeakDecode(options[0], &ditherOptions)
   196  		if err != nil {
   197  			panic(fmt.Sprintf("failed to decode options: %s", err))
   198  		}
   199  	}
   200  
   201  	if len(ditherOptions.Colors) < 2 {
   202  		panic("palette must have at least two colors")
   203  	}
   204  
   205  	var palette []color.Color
   206  	for _, c := range ditherOptions.Colors {
   207  		cc, err := hexStringToColor(c)
   208  		if err != nil {
   209  			panic(fmt.Sprintf("%q is an invalid color: specify RGB or RGBA using hexadecimal notation", c))
   210  		}
   211  		palette = append(palette, cc)
   212  	}
   213  
   214  	d := dither.NewDitherer(palette)
   215  	if method, ok := ditherMethodsErrorDiffusion[strings.ToLower(ditherOptions.Method)]; ok {
   216  		d.Matrix = dither.ErrorDiffusionStrength(method, ditherOptions.Strength)
   217  		d.Serpentine = ditherOptions.Serpentine
   218  	} else if method, ok := ditherMethodsOrdered[strings.ToLower(ditherOptions.Method)]; ok {
   219  		d.Mapper = dither.PixelMapperFromMatrix(method, ditherOptions.Strength)
   220  	} else {
   221  		panic(fmt.Sprintf("%q is an invalid dithering method: see documentation", ditherOptions.Method))
   222  	}
   223  
   224  	return filter{
   225  		Options: newFilterOpts(ditherOptions),
   226  		Filter:  ditherFilter{ditherer: d},
   227  	}
   228  }
   229  
   230  // AutoOrient creates a filter that rotates and flips an image as needed per
   231  // its EXIF orientation tag.
   232  func (*Filters) AutoOrient() gift.Filter {
   233  	return filter{
   234  		Filter: autoOrientFilter{},
   235  	}
   236  }
   237  
   238  // Brightness creates a filter that changes the brightness of an image.
   239  // The percentage parameter must be in range (-100, 100).
   240  func (*Filters) Brightness(percentage any) gift.Filter {
   241  	return filter{
   242  		Options: newFilterOpts(percentage),
   243  		Filter:  gift.Brightness(cast.ToFloat32(percentage)),
   244  	}
   245  }
   246  
   247  // ColorBalance creates a filter that changes the color balance of an image.
   248  // The percentage parameters for each color channel (red, green, blue) must be in range (-100, 500).
   249  func (*Filters) ColorBalance(percentageRed, percentageGreen, percentageBlue any) gift.Filter {
   250  	return filter{
   251  		Options: newFilterOpts(percentageRed, percentageGreen, percentageBlue),
   252  		Filter:  gift.ColorBalance(cast.ToFloat32(percentageRed), cast.ToFloat32(percentageGreen), cast.ToFloat32(percentageBlue)),
   253  	}
   254  }
   255  
   256  // Colorize creates a filter that produces a colorized version of an image.
   257  // The hue parameter is the angle on the color wheel, typically in range (0, 360).
   258  // The saturation parameter must be in range (0, 100).
   259  // The percentage parameter specifies the strength of the effect, it must be in range (0, 100).
   260  func (*Filters) Colorize(hue, saturation, percentage any) gift.Filter {
   261  	return filter{
   262  		Options: newFilterOpts(hue, saturation, percentage),
   263  		Filter:  gift.Colorize(cast.ToFloat32(hue), cast.ToFloat32(saturation), cast.ToFloat32(percentage)),
   264  	}
   265  }
   266  
   267  // Contrast creates a filter that changes the contrast of an image.
   268  // The percentage parameter must be in range (-100, 100).
   269  func (*Filters) Contrast(percentage any) gift.Filter {
   270  	return filter{
   271  		Options: newFilterOpts(percentage),
   272  		Filter:  gift.Contrast(cast.ToFloat32(percentage)),
   273  	}
   274  }
   275  
   276  // Gamma creates a filter that performs a gamma correction on an image.
   277  // The gamma parameter must be positive. Gamma = 1 gives the original image.
   278  // Gamma less than 1 darkens the image and gamma greater than 1 lightens it.
   279  func (*Filters) Gamma(gamma any) gift.Filter {
   280  	return filter{
   281  		Options: newFilterOpts(gamma),
   282  		Filter:  gift.Gamma(cast.ToFloat32(gamma)),
   283  	}
   284  }
   285  
   286  // GaussianBlur creates a filter that applies a gaussian blur to an image.
   287  func (*Filters) GaussianBlur(sigma any) gift.Filter {
   288  	return filter{
   289  		Options: newFilterOpts(sigma),
   290  		Filter:  gift.GaussianBlur(cast.ToFloat32(sigma)),
   291  	}
   292  }
   293  
   294  // Grayscale creates a filter that produces a grayscale version of an image.
   295  func (*Filters) Grayscale() gift.Filter {
   296  	return filter{
   297  		Filter: gift.Grayscale(),
   298  	}
   299  }
   300  
   301  // Hue creates a filter that rotates the hue of an image.
   302  // The hue angle shift is typically in range -180 to 180.
   303  func (*Filters) Hue(shift any) gift.Filter {
   304  	return filter{
   305  		Options: newFilterOpts(shift),
   306  		Filter:  gift.Hue(cast.ToFloat32(shift)),
   307  	}
   308  }
   309  
   310  // Invert creates a filter that negates the colors of an image.
   311  func (*Filters) Invert() gift.Filter {
   312  	return filter{
   313  		Filter: gift.Invert(),
   314  	}
   315  }
   316  
   317  // Pixelate creates a filter that applies a pixelation effect to an image.
   318  func (*Filters) Pixelate(size any) gift.Filter {
   319  	return filter{
   320  		Options: newFilterOpts(size),
   321  		Filter:  gift.Pixelate(cast.ToInt(size)),
   322  	}
   323  }
   324  
   325  // Saturation creates a filter that changes the saturation of an image.
   326  func (*Filters) Saturation(percentage any) gift.Filter {
   327  	return filter{
   328  		Options: newFilterOpts(percentage),
   329  		Filter:  gift.Saturation(cast.ToFloat32(percentage)),
   330  	}
   331  }
   332  
   333  // Sepia creates a filter that produces a sepia-toned version of an image.
   334  func (*Filters) Sepia(percentage any) gift.Filter {
   335  	return filter{
   336  		Options: newFilterOpts(percentage),
   337  		Filter:  gift.Sepia(cast.ToFloat32(percentage)),
   338  	}
   339  }
   340  
   341  // Sigmoid creates a filter that changes the contrast of an image using a sigmoidal function and returns the adjusted image.
   342  // It's a non-linear contrast change useful for photo adjustments as it preserves highlight and shadow detail.
   343  func (*Filters) Sigmoid(midpoint, factor any) gift.Filter {
   344  	return filter{
   345  		Options: newFilterOpts(midpoint, factor),
   346  		Filter:  gift.Sigmoid(cast.ToFloat32(midpoint), cast.ToFloat32(factor)),
   347  	}
   348  }
   349  
   350  // UnsharpMask creates a filter that sharpens an image.
   351  // The sigma parameter is used in a gaussian function and affects the radius of effect.
   352  // Sigma must be positive. Sharpen radius roughly equals 3 * sigma.
   353  // The amount parameter controls how much darker and how much lighter the edge borders become. Typically between 0.5 and 1.5.
   354  // The threshold parameter controls the minimum brightness change that will be sharpened. Typically between 0 and 0.05.
   355  func (*Filters) UnsharpMask(sigma, amount, threshold any) gift.Filter {
   356  	return filter{
   357  		Options: newFilterOpts(sigma, amount, threshold),
   358  		Filter:  gift.UnsharpMask(cast.ToFloat32(sigma), cast.ToFloat32(amount), cast.ToFloat32(threshold)),
   359  	}
   360  }
   361  
   362  type filter struct {
   363  	Options filterOpts
   364  	gift.Filter
   365  }
   366  
   367  // For cache-busting.
   368  type filterOpts struct {
   369  	Version int
   370  	Vals    any
   371  }
   372  
   373  func newFilterOpts(vals ...any) filterOpts {
   374  	return filterOpts{
   375  		Version: filterAPIVersion,
   376  		Vals:    vals,
   377  	}
   378  }