mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-07 20:30:36 -05:00
parent
0672b5c766
commit
21d9057dbf
6 changed files with 668 additions and 0 deletions
160
docs/content/en/functions/images/Dither.md
Normal file
160
docs/content/en/functions/images/Dither.md
Normal 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
|
381
docs/layouts/shortcodes/img.html
Normal file
381
docs/layouts/shortcodes/img.html
Normal 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
1
go.mod
|
@ -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
2
go.sum
|
@ -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=
|
||||
|
|
71
resources/images/dither.go
Normal file
71
resources/images/dither.go
Normal 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())
|
||||
}
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue