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 }