diff options
author | Joe Mooring <[email protected]> | 2024-02-07 15:42:27 -0800 |
---|---|---|
committer | Bjørn Erik Pedersen <[email protected]> | 2024-02-11 22:51:11 +0200 |
commit | 21d9057dbfe64f668d5fc6a7f458e0984fbf7e56 (patch) | |
tree | f2908e8ee594eb908375f11dd85edc595be8aeb7 /resources | |
parent | 0672b5c76605132475ff18b8c526f1cf0d6affc3 (diff) | |
download | hugo-21d9057dbfe64f668d5fc6a7f458e0984fbf7e56.tar.gz hugo-21d9057dbfe64f668d5fc6a7f458e0984fbf7e56.zip |
Add images.Dither filter
Closes #8598
Diffstat (limited to 'resources')
-rw-r--r-- | resources/images/dither.go | 71 | ||||
-rw-r--r-- | resources/images/filters.go | 53 |
2 files changed, 124 insertions, 0 deletions
diff --git a/resources/images/dither.go b/resources/images/dither.go new file mode 100644 index 000000000..19d7e088d --- /dev/null +++ b/resources/images/dither.go @@ -0,0 +1,71 @@ +// Copyright 2024 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package images + +import ( + "image" + "image/draw" + + "github.com/disintegration/gift" + "github.com/makeworld-the-better-one/dither/v2" +) + +var _ gift.Filter = (*ditherFilter)(nil) + +type ditherFilter struct { + ditherer *dither.Ditherer +} + +var ditherMethodsErrorDiffusion = map[string]dither.ErrorDiffusionMatrix{ + "atkinson": dither.Atkinson, + "burkes": dither.Burkes, + "falsefloydsteinberg": dither.FalseFloydSteinberg, + "floydsteinberg": dither.FloydSteinberg, + "jarvisjudiceninke": dither.JarvisJudiceNinke, + "sierra": dither.Sierra, + "sierra2": dither.Sierra2, + "sierra2_4a": dither.Sierra2_4A, + "sierra3": dither.Sierra3, + "sierralite": dither.SierraLite, + "simple2d": dither.Simple2D, + "stevenpigeon": dither.StevenPigeon, + "stucki": dither.Stucki, + "tworowsierra": dither.TwoRowSierra, +} + +var ditherMethodsOrdered = map[string]dither.OrderedDitherMatrix{ + "clustereddot4x4": dither.ClusteredDot4x4, + "clustereddot6x6": dither.ClusteredDot6x6, + "clustereddot6x6_2": dither.ClusteredDot6x6_2, + "clustereddot6x6_3": dither.ClusteredDot6x6_3, + "clustereddot8x8": dither.ClusteredDot8x8, + "clustereddotdiagonal16x16": dither.ClusteredDotDiagonal16x16, + "clustereddotdiagonal6x6": dither.ClusteredDotDiagonal6x6, + "clustereddotdiagonal8x8": dither.ClusteredDotDiagonal8x8, + "clustereddotdiagonal8x8_2": dither.ClusteredDotDiagonal8x8_2, + "clustereddotdiagonal8x8_3": dither.ClusteredDotDiagonal8x8_3, + "clustereddothorizontalline": dither.ClusteredDotHorizontalLine, + "clustereddotspiral5x5": dither.ClusteredDotSpiral5x5, + "clustereddotverticalline": dither.ClusteredDotVerticalLine, + "horizontal3x5": dither.Horizontal3x5, + "vertical5x3": dither.Vertical5x3, +} + +func (f ditherFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) { + gift.New().Draw(dst, f.ditherer.Dither(src)) +} + +func (f ditherFilter) Bounds(srcBounds image.Rectangle) image.Rectangle { + return image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy()) +} diff --git a/resources/images/filters.go b/resources/images/filters.go index 572a10d71..53818c97d 100644 --- a/resources/images/filters.go +++ b/resources/images/filters.go @@ -17,10 +17,13 @@ package images import ( "fmt" "image/color" + "strings" "github.com/gohugoio/hugo/common/hugio" "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/resources/resource" + "github.com/makeworld-the-better-one/dither/v2" + "github.com/mitchellh/mapstructure" "github.com/disintegration/gift" "github.com/spf13/cast" @@ -174,6 +177,56 @@ func (*Filters) Padding(args ...any) gift.Filter { } } +// Dither creates a filter that dithers an image. +func (*Filters) Dither(options ...any) gift.Filter { + ditherOptions := struct { + Colors []string + Method string + Serpentine bool + Strength float32 + }{ + Colors: []string{"000000ff", "ffffffff"}, + Method: "floydsteinberg", + Serpentine: true, + Strength: 1.0, + } + + if len(options) != 0 { + err := mapstructure.WeakDecode(options[0], &ditherOptions) + if err != nil { + panic(fmt.Sprintf("failed to decode options: %s", err)) + } + } + + if len(ditherOptions.Colors) < 2 { + panic("palette must have at least two colors") + } + + var palette []color.Color + for _, c := range ditherOptions.Colors { + cc, err := hexStringToColor(c) + if err != nil { + panic(fmt.Sprintf("%q is an invalid color: specify RGB or RGBA using hexadecimal notation", c)) + } + palette = append(palette, cc) + } + + d := dither.NewDitherer(palette) + if method, ok := ditherMethodsErrorDiffusion[strings.ToLower(ditherOptions.Method)]; ok { + d.Matrix = dither.ErrorDiffusionStrength(method, ditherOptions.Strength) + d.Serpentine = ditherOptions.Serpentine + } else if method, ok := ditherMethodsOrdered[strings.ToLower(ditherOptions.Method)]; ok { + d.Mapper = dither.PixelMapperFromMatrix(method, ditherOptions.Strength) + } else { + panic(fmt.Sprintf("%q is an invalid dithering method: see documentation", ditherOptions.Method)) + } + + return filter{ + Options: newFilterOpts(ditherOptions), + Filter: ditherFilter{ditherer: d}, + } +} + // AutoOrient creates a filter that rotates and flips an image as needed per // its EXIF orientation tag. func (*Filters) AutoOrient() gift.Filter { |