aboutsummaryrefslogtreecommitdiffhomepage
path: root/resources
diff options
context:
space:
mode:
authorJoe Mooring <[email protected]>2024-02-07 15:42:27 -0800
committerBjørn Erik Pedersen <[email protected]>2024-02-11 22:51:11 +0200
commit21d9057dbfe64f668d5fc6a7f458e0984fbf7e56 (patch)
treef2908e8ee594eb908375f11dd85edc595be8aeb7 /resources
parent0672b5c76605132475ff18b8c526f1cf0d6affc3 (diff)
downloadhugo-21d9057dbfe64f668d5fc6a7f458e0984fbf7e56.tar.gz
hugo-21d9057dbfe64f668d5fc6a7f458e0984fbf7e56.zip
Add images.Dither filter
Closes #8598
Diffstat (limited to 'resources')
-rw-r--r--resources/images/dither.go71
-rw-r--r--resources/images/filters.go53
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 {