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,160 @@
---
title: images.Dither
description: Returns an image filter that dithers an image.
categories: []
keywords: []
action:
aliases: []
related:
- functions/images/Filter
- functions/images/Process
- methods/resource/Colors
- methods/resource/Filter
returnType: images.filter
signatures: ['images.Dither [OPTIONS]']
toc: true
---
## Options
colors
: (`string array`) A slice of two or more colors that make up the dithering palette, each expressed as an RGB or RGBA [hexadecimal] value, with or without a leading hash mark. The default values are opaque black (`000000ff`) and opaque white (`ffffffff`).
[hexadecimal]: https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color
method
: (`string`) The dithering method. See the [dithering methods](#dithering-methods) section below for a list of the available methods. Default is `FloydSteinberg`.
serpentine
: (`bool`) Applicable to error diffusion dithering methods, serpentine controls whether the error diffusion matrix is applied in a serpentine manner, meaning that it goes right-to-left every other line. This greatly reduces line-type artifacts. Default is `true`.
strength
: (`float`) The strength at which to apply the dithering matrix, typically a value in the range [0, 1]. A value of `1.0` applies the dithering matrix at 100% strength (no modifification of the dither matrix). The `strength` is inversely proportional to contrast; reducing the strength increases the contrast. Setting `strength` to a value such as `0.8` can be useful to reduce noise in the dithered image. Default is `1.0`.
## Usage
Create the options map:
```go-html-template
{{ $opts := dict
"colors" (slice "222222" "808080" "dddddd")
"method" "ClusteredDot4x4"
"strength" 0.85
}}
```
Create the filter:
```go-html-template
{{ $filter := images.Dither $opts }}
```
Or create the filter using the default settings:
```go-html-template
{{ $filter := images.Dither }}
```
{{% include "functions/images/_common/apply-image-filter.md" %}}
## Dithering methods
See the [Go documentation] for descriptions of each of the dithering methods below.
[Go documentation]: https://pkg.go.dev/github.com/makeworld-the-better-one/dither/v2#pkg-variables
Error diffusion dithering methods:
- Atkinson
- Burkes
- FalseFloydSteinberg
- FloydSteinberg
- JarvisJudiceNinke
- Sierra
- Sierra2
- Sierra2_4A
- Sierra3
- SierraLite
- Simple2D
- StevenPigeon
- Stucki
- TwoRowSierra
Ordered dithering methods:
- ClusteredDot4x4
- ClusteredDot6x6
- ClusteredDot6x6_2
- ClusteredDot6x6_3
- ClusteredDot8x8
- ClusteredDotDiagonal16x16
- ClusteredDotDiagonal6x6
- ClusteredDotDiagonal8x8
- ClusteredDotDiagonal8x8_2
- ClusteredDotDiagonal8x8_3
- ClusteredDotHorizontalLine
- ClusteredDotSpiral5x5
- ClusteredDotVerticalLine
- Horizontal3x5
- Vertical5x3
## Example
This example uses the default dithering options.
{{< img
src="images/examples/zion-national-park.jpg"
alt="Zion National Park"
filter="Dither"
filterArgs=""
example=true
>}}
## Recommendations
Regardless of dithering method, do both of the following to obtain the best results:
1. Scale the image _before_ dithering
2. Output the image to a lossless format such as GIF or PNG
The example below does both of these, and it sets the dithering palette to the three most dominant colors in the image.
```go-html-template
{{ with resources.Get "original.jpg" }}
{{ $opts := dict
"method" "ClusteredDotSpiral5x5"
"colors" (first 3 .Colors)
}}
{{ $filters := slice
(images.Process "resize 800x")
(images.Dither $opts)
(images.Process "png")
}}
{{ with . | images.Filter $filters }}
<img src="{{ .RelPermalink }}" width="{{ .Width }}" height="{{ .Height }}" alt="">
{{ end }}
{{ end }}
```
For best results, if the dithering palette is grayscale, convert the image to grayscale before dithering.
```go-html-template
{{ $opts := dict "colors" (slice "222" "808080" "ddd") }}
{{ $filters := slice
(images.Process "resize 800x")
(images.Grayscale)
(images.Dither $opts)
(images.Process "png")
}}
{{ with images.Filter $filters . }}
<img src="{{ .RelPermalink }}" width="{{ .Width }}" height="{{ .Height }}" alt="">
{{ end }}
```
The example above:
1. Resizes the image to be 800 px wide
2. Converts the image to grayscale
3. Dithers the image using the default (`FloydSteinberg`) dithering method with a grayscale palette
4. Converts the image to the PNG format

View File

@ -0,0 +1,381 @@
{{- /*
Renders the given image using the given filter, if any.
@param {string} src The path to the image which must be a remote, page, or global resource.
@param {string} [filter] The filter to apply to the image (case-insensitive).
@param {string} [filterArgs] A comma-delimited list of arguments to pass to the filter.
@param {bool} [example=false] If true, renders a before/after example.
@param {int} [exampleWidth=384] Image width, in pixels, when rendering a before/after example.
@returns {template.HTML}
@examples
{{< img src="zion-national-park.jpg" >}}
{{< img src="zion-national-park.jpg" alt="Zion National Park" >}}
{{< img
src="zion-national-park.jpg"
alt="Zion National Park"
filter="grayscale"
>}}
{{< img
src="zion-national-park.jpg"
alt="Zion National Park"
filter="process"
filterArgs="resize 400x webp"
>}}
{{< img
src="zion-national-park.jpg"
alt="Zion National Park"
filter="colorize"
filterArgs="180,50,20"
>}}
{{< img
src="zion-national-park.jpg"
alt="Zion National Park"
filter="grayscale"
example=true
>}}
{{< img
src="zion-national-park.jpg"
alt="Zion National Park"
filter="grayscale"
example=true
exampleWidth=400
>}}
When using the text filter, provide the arguments in this order:
0. The text
1. The horizontal offset, in pixels, relative to the left of the image (default 20)
2. The vertical offset, in pixels, relative to the top of the image (default 20)
3. The font size in pixels (default 64)
4. The line height (default 1.2)
5. The font color (default #ffffff)
{{< img
src="images/examples/zion-national-park.jpg"
alt="Zion National Park"
filter="Text"
filterArgs="Zion National Park,25,250,56"
example=true
>}}
When using the padding filter, provide all arguments in this order:
0. Padding top
1. Padding right
2. Padding bottom
3. Padding right
4. Canvas color
{{< img
src="images/examples/zion-national-park.jpg"
alt="Zion National Park"
filter="Padding"
filterArgs="20,50,20,50,#0705"
example=true
>}}
*/}}
{{- /* Initialize. */}}
{{- $alt := "" }}
{{- $src := "" }}
{{- $filter := "" }}
{{- $filterArgs := slice }}
{{- $example := false }}
{{- $exampleWidth := 384 }}
{{- /* Default values to use with the text filter. */}}
{{ $textFilterOpts := dict
"xOffset" 20
"yOffset" 20
"fontSize" 64
"lineHeight" 1.2
"fontColor" "#ffffff"
"fontPath" "https://github.com/google/fonts/raw/main/ofl/lato/Lato-Regular.ttf"
}}
{{- /* Get and validate parameters. */}}
{{- with .Get "alt" }}
{{- $alt = .}}
{{- end }}
{{- with .Get "src" }}
{{- $src = . }}
{{- else }}
{{- errorf "The %q shortcode requires a file parameter. See %s" .Name .Position }}
{{- end }}
{{- with .Get "filter" }}
{{- $filter = . | lower }}
{{- end }}
{{- $validFilters := slice
"autoorient" "brightness" "colorbalance" "colorize" "contrast" "dither"
"gamma" "gaussianblur" "grayscale" "hue" "invert" "none" "opacity" "overlay"
"padding" "pixelate" "process" "saturation" "sepia" "sigmoid" "text"
"unsharpmask"
}}
{{- with $filter }}
{{- if not (in $validFilters .) }}
{{- errorf "The filter passed to the %q shortcode is invalid. The filter must be one of %s. See %s" $.Name (delimit $validFilters ", " ", or ") $.Position }}
{{- end }}
{{- end }}
{{- with .Get "filterArgs" }}
{{- $filterArgs = split . "," }}
{{- $filterArgs = apply $filterArgs "trim" "." " " }}
{{- end }}
{{- if in (slice "false" false 0) (.Get "example") }}
{{- $example = false }}
{{- else if in (slice "true" true 1) (.Get "example")}}
{{- $example = true }}
{{- end }}
{{- with .Get "exampleWidth" }}
{{- $exampleWidth = . | int }}
{{- end }}
{{- /* Get image. */}}
{{- $ctx := dict "page" .Page "src" $src "name" .Name "position" .Position }}
{{- $i := partial "inline/get-resource.html" $ctx }}
{{- /* Resize if rendering before/after examples. */}}
{{- if $example }}
{{- $i = $i.Resize (printf "%dx" $exampleWidth) }}
{{- end }}
{{- /* Create filter. */}}
{{- $f := "" }}
{{- $ctx := dict "filter" $filter "args" $filterArgs "name" .Name "position" .Position }}
{{- if eq $filter "autoorient" }}
{{- $ctx = merge $ctx (dict "argsRequired" 0) }}
{{- template "validate-arg-count" $ctx }}
{{- $f = images.AutoOrient }}
{{- else if eq $filter "brightness" }}
{{- $ctx = merge $ctx (dict "argsRequired" 1) }}
{{- template "validate-arg-count" $ctx }}
{{- $filterArgs = apply $filterArgs "float" "." }}
{{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 0) "min" -100 "max" 100) }}
{{- template "validate-arg-value" $ctx }}
{{- $f = images.Brightness (index $filterArgs 0) }}
{{- else if eq $filter "colorbalance" }}
{{- $ctx = merge $ctx (dict "argsRequired" 3) }}
{{- template "validate-arg-count" $ctx }}
{{- $filterArgs = apply $filterArgs "float" "." }}
{{- $ctx = merge $ctx (dict "argName" "percentage red" "argValue" (index $filterArgs 0) "min" -100 "max" 500) }}
{{- template "validate-arg-value" $ctx }}
{{- $ctx = merge $ctx (dict "argName" "percentage green" "argValue" (index $filterArgs 1) "min" -100 "max" 500) }}
{{- template "validate-arg-value" $ctx }}
{{- $ctx = merge $ctx (dict "argName" "percentage blue" "argValue" (index $filterArgs 2) "min" -100 "max" 500) }}
{{- template "validate-arg-value" $ctx }}
{{- $f = images.ColorBalance (index $filterArgs 0) (index $filterArgs 1) (index $filterArgs 2) }}
{{- else if eq $filter "colorize" }}
{{- $ctx = merge $ctx (dict "argsRequired" 3) }}
{{- template "validate-arg-count" $ctx }}
{{- $filterArgs = apply $filterArgs "float" "." }}
{{- $ctx = merge $ctx (dict "argName" "hue" "argValue" (index $filterArgs 0) "min" 0 "max" 360) }}
{{- template "validate-arg-value" $ctx }}
{{- $ctx = merge $ctx (dict "argName" "saturation" "argValue" (index $filterArgs 1) "min" 0 "max" 100) }}
{{- template "validate-arg-value" $ctx }}
{{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 2) "min" 0 "max" 100) }}
{{- template "validate-arg-value" $ctx }}
{{- $f = images.Colorize (index $filterArgs 0) (index $filterArgs 1) (index $filterArgs 2) }}
{{- else if eq $filter "contrast" }}
{{- $ctx = merge $ctx (dict "argsRequired" 1) }}
{{- template "validate-arg-count" $ctx }}
{{- $filterArgs = apply $filterArgs "float" "." }}
{{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 0) "min" -100 "max" 100) }}
{{- template "validate-arg-value" $ctx }}
{{- $f = images.Contrast (index $filterArgs 0) }}
{{- else if eq $filter "dither" }}
{{- $f = images.Dither }}
{{- else if eq $filter "gamma" }}
{{- $ctx = merge $ctx (dict "argsRequired" 1) }}
{{- template "validate-arg-count" $ctx }}
{{- $filterArgs = apply $filterArgs "float" "." }}
{{- $ctx = merge $ctx (dict "argName" "gamma" "argValue" (index $filterArgs 0) "min" 0 "max" 100) }}
{{- template "validate-arg-value" $ctx }}
{{- $f = images.Gamma (index $filterArgs 0) }}
{{- else if eq $filter "gaussianblur" }}
{{- $ctx = merge $ctx (dict "argsRequired" 1) }}
{{- template "validate-arg-count" $ctx }}
{{- $filterArgs = apply $filterArgs "float" "." }}
{{- $ctx = merge $ctx (dict "argName" "sigma" "argValue" (index $filterArgs 0) "min" 0 "max" 1000) }}
{{- template "validate-arg-value" $ctx }}
{{- $f = images.GaussianBlur (index $filterArgs 0) }}
{{- else if eq $filter "grayscale" }}
{{- $ctx = merge $ctx (dict "argsRequired" 0) }}
{{- template "validate-arg-count" $ctx }}
{{- $f = images.Grayscale }}
{{- else if eq $filter "hue" }}
{{- $ctx = merge $ctx (dict "argsRequired" 1) }}
{{- template "validate-arg-count" $ctx }}
{{- $filterArgs = apply $filterArgs "float" "." }}
{{- $ctx = merge $ctx (dict "argName" "shift" "argValue" (index $filterArgs 0) "min" -180 "max" 180) }}
{{- template "validate-arg-value" $ctx }}
{{- $f = images.Hue (index $filterArgs 0) }}
{{- else if eq $filter "invert" }}
{{- $ctx = merge $ctx (dict "argsRequired" 0) }}
{{- template "validate-arg-count" $ctx }}
{{- $f = images.Invert }}
{{- else if eq $filter "opacity" }}
{{- $ctx = merge $ctx (dict "argsRequired" 1) }}
{{- template "validate-arg-count" $ctx }}
{{- $filterArgs = apply $filterArgs "float" "." }}
{{- $ctx = merge $ctx (dict "argName" "opacity" "argValue" (index $filterArgs 0) "min" 0 "max" 1) }}
{{- template "validate-arg-value" $ctx }}
{{- $f = images.Opacity (index $filterArgs 0) }}
{{- else if eq $filter "overlay" }}
{{- $ctx = merge $ctx (dict "argsRequired" 3) }}
{{- template "validate-arg-count" $ctx }}
{{- $ctx := dict "src" (index $filterArgs 0) "name" .Name "position" .Position }}
{{- $overlayImg := partial "inline/get-resource.html" $ctx }}
{{- $f = images.Overlay $overlayImg (index $filterArgs 1 | float ) (index $filterArgs 2 | float) }}
{{- else if eq $filter "padding" }}
{{- $ctx = merge $ctx (dict "argsRequired" 5) }}
{{- template "validate-arg-count" $ctx }}
{{- $f = images.Padding
(index $filterArgs 0 | int)
(index $filterArgs 1 | int)
(index $filterArgs 2 | int)
(index $filterArgs 3 | int)
(index $filterArgs 4)
}}
{{- else if eq $filter "pixelate" }}
{{- $ctx = merge $ctx (dict "argsRequired" 1) }}
{{- template "validate-arg-count" $ctx }}
{{- $filterArgs = apply $filterArgs "float" "." }}
{{- $ctx = merge $ctx (dict "argName" "size" "argValue" (index $filterArgs 0) "min" 0 "max" 1000) }}
{{- template "validate-arg-value" $ctx }}
{{- $f = images.Pixelate (index $filterArgs 0) }}
{{- else if eq $filter "process" }}
{{- $ctx = merge $ctx (dict "argsRequired" 1) }}
{{- template "validate-arg-count" $ctx }}
{{- $f = images.Process (index $filterArgs 0) }}
{{- else if eq $filter "saturation" }}
{{- $ctx = merge $ctx (dict "argsRequired" 1) }}
{{- template "validate-arg-count" $ctx }}
{{- $filterArgs = apply $filterArgs "float" "." }}
{{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 0) "min" -100 "max" 500) }}
{{- template "validate-arg-value" $ctx }}
{{- $f = images.Saturation (index $filterArgs 0) }}
{{- else if eq $filter "sepia" }}
{{- $ctx = merge $ctx (dict "argsRequired" 1) }}
{{- template "validate-arg-count" $ctx }}
{{- $filterArgs = apply $filterArgs "float" "." }}
{{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 0) "min" 0 "max" 100) }}
{{- template "validate-arg-value" $ctx }}
{{- $f = images.Sepia (index $filterArgs 0) }}
{{- else if eq $filter "sigmoid" }}
{{- $ctx = merge $ctx (dict "argsRequired" 2) }}
{{- template "validate-arg-count" $ctx }}
{{- $filterArgs = apply $filterArgs "float" "." }}
{{- $ctx = merge $ctx (dict "argName" "midpoint" "argValue" (index $filterArgs 0) "min" 0 "max" 1) }}
{{- template "validate-arg-value" $ctx }}
{{- $ctx = merge $ctx (dict "argName" "factor" "argValue" (index $filterArgs 1) "min" -10 "max" 10) }}
{{- template "validate-arg-value" $ctx }}
{{- $f = images.Sigmoid (index $filterArgs 0) (index $filterArgs 1) }}
{{- else if eq $filter "text" }}
{{- $ctx = merge $ctx (dict "argsRequired" 1) }}
{{- template "validate-arg-count" $ctx }}
{{- $ctx := dict "src" $textFilterOpts.fontPath "name" .Name "position" .Position }}
{{- $font := or (partial "inline/get-resource.html" $ctx) }}
{{- $fontSize := or (index $filterArgs 3 | int) $textFilterOpts.fontSize }}
{{- $lineHeight := math.Max (or (index $filterArgs 4 | float) $textFilterOpts.lineHeight) 1 }}
{{- $opts := dict
"x" (or (index $filterArgs 1 | int) $textFilterOpts.xOffset)
"y" (or (index $filterArgs 2 | int) $textFilterOpts.yOffset)
"size" $fontSize
"linespacing" (mul (sub $lineHeight 1) $fontSize)
"color" (or (index $filterArgs 5) $textFilterOpts.fontColor)
"font" $font
}}
{{- $f = images.Text (index $filterArgs 0) $opts }}
{{- else if eq $filter "unsharpmask" }}
{{- $ctx = merge $ctx (dict "argsRequired" 3) }}
{{- template "validate-arg-count" $ctx }}
{{- $filterArgs = apply $filterArgs "float" "." }}
{{- $ctx = merge $ctx (dict "argName" "sigma" "argValue" (index $filterArgs 0) "min" 0 "max" 500) }}
{{- template "validate-arg-value" $ctx }}
{{- $ctx = merge $ctx (dict "argName" "amount" "argValue" (index $filterArgs 1) "min" 0 "max" 100) }}
{{- template "validate-arg-value" $ctx }}
{{- $ctx = merge $ctx (dict "argName" "threshold" "argValue" (index $filterArgs 2) "min" 0 "max" 1) }}
{{- template "validate-arg-value" $ctx }}
{{- $f = images.UnsharpMask (index $filterArgs 0) (index $filterArgs 1) (index $filterArgs 2) }}
{{- end }}
{{- /* Apply filter. */}}
{{- $fi := $i }}
{{- with $f }}
{{- $fi = $i.Filter . }}
{{- end }}
{{- /* Render. */}}
{{- if $example }}
<p>Original</p>
<img class='di ba b--black-20' style="width: initial;" src="{{ $i.RelPermalink }}" alt="{{ $alt }}">
<p>Processed</p>
<img class='di ba b--black-20' style="width: initial;" src="{{ $fi.RelPermalink }}" alt="{{ $alt }}">
{{- else -}}
<img class='di' style="width: initial;" src="{{ $fi.RelPermalink }}" alt="{{ $alt }}">
{{- end }}
{{- define "validate-arg-count" }}
{{- $msg := "When using the %q filter, the %q shortcode requires an args parameter with %d %s. See %s" }}
{{- if lt (len .args) .argsRequired }}
{{- $text := "values" }}
{{- if eq 1 .argsRequired }}
{{- $text = "value" }}
{{- end }}
{{- errorf $msg .filter .name .argsRequired $text .position }}
{{- end }}
{{- end }}
{{- define "validate-arg-value" }}
{{- $msg := "The %q argument passed to the %q shortcode is invalid. Expected a value in the range [%v,%v], but received %v. See %s" }}
{{- if or (lt .argValue .min) (gt .argValue .max) }}
{{- errorf $msg .argName .name .min .max .argValue .position }}
{{- end }}
{{- end }}
{{- define "partials/inline/get-resource.html" }}
{{- $r := "" }}
{{- $u := urls.Parse .src }}
{{- $msg := "The %q shortcode was unable to resolve %s. See %s" }}
{{- if $u.IsAbs }}
{{- with resources.GetRemote $u.String }}
{{- with .Err }}
{{- errorf "%s" }}
{{- else }}
{{- /* This is a remote resource. */}}
{{- $r = . }}
{{- end }}
{{- else }}
{{- errorf $msg $.name $u.String $.position }}
{{- end }}
{{- else }}
{{- with .page.Resources.Get (strings.TrimPrefix "./" $u.Path) }}
{{- /* This is a page resource. */}}
{{- $r = . }}
{{- else }}
{{- with resources.Get $u.Path }}
{{- /* This is a global resource. */}}
{{- $r = . }}
{{- else }}
{{- errorf $msg $.name $u.Path $.position }}
{{- end }}
{{- end }}
{{- end }}
{{- return $r}}
{{- end -}}

1
go.mod
View File

@ -46,6 +46,7 @@ require (
github.com/kylelemons/godebug v1.1.0
github.com/kyokomi/emoji/v2 v2.2.12
github.com/magefile/mage v1.15.0
github.com/makeworld-the-better-one/dither/v2 v2.4.0
github.com/marekm4/color-extractor v1.2.1
github.com/mattn/go-isatty v0.0.20
github.com/mitchellh/hashstructure v1.1.0

2
go.sum
View File

@ -347,6 +347,8 @@ github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE=
github.com/makeworld-the-better-one/dither/v2 v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc=
github.com/marekm4/color-extractor v1.2.1 h1:3Zb2tQsn6bITZ8MBVhc33Qn1k5/SEuZ18mrXGUqIwn0=
github.com/marekm4/color-extractor v1.2.1/go.mod h1:90VjmiHI6M8ez9eYUaXLdcKnS+BAOp7w+NpwBdkJmpA=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=

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 {