Add images.Dither filter

Closes #8598
This commit is contained in:
Joe Mooring
2024-02-07 15:42:27 -08:00
committed by Bjørn Erik Pedersen
parent 0672b5c766
commit 21d9057dbf
6 changed files with 668 additions and 0 deletions

View File

@@ -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())
}

View File

@@ -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 {