return false
}
+// ToString converts the given value to a string.
+// Note that this is a more strict version compared to cast.ToString,
+// as it will not try to convert numeric values to strings,
+// but only accept strings or fmt.Stringer.
+func ToString(v any) (string, bool) {
+ switch vv := v.(type) {
+ case string:
+ return vv, true
+ case fmt.Stringer:
+ return vv.String(), true
+ }
+ return "", false
+}
+
type Tuple struct {
First string
Second string
panic(e.ResourceError)
}
-func (e *errorResource) Colors() ([]string, error) {
+func (e *errorResource) Colors() ([]images.Color, error) {
panic(e.ResourceError)
}
meta *imageMeta
dominantColorInit sync.Once
- dominantColors []string
+ dominantColors []images.Color
baseResource
}
// Colors returns a slice of the most dominant colors in an image
// using a simple histogram method.
-func (i *imageResource) Colors() ([]string, error) {
+func (i *imageResource) Colors() ([]images.Color, error) {
var err error
i.dominantColorInit.Do(func() {
var img image.Image
}
colors := color_extractor.ExtractColors(img)
for _, c := range colors {
- i.dominantColors = append(i.dominantColors, images.ColorToHexString(c))
+ i.dominantColors = append(i.dominantColors, images.ColorGoToColor(c))
}
})
return i.dominantColors, nil
assertWidthHeight(c, img, w, h)
}
- colors, err := image.Colors()
+ gotColors, err := image.Colors()
c.Assert(err, qt.IsNil)
- c.Assert(colors, qt.DeepEquals, []string{"#2d2f33", "#a49e93", "#d39e59", "#a76936", "#737a84", "#7c838b"})
+ expectedColors := images.HexStringsToColors("#2d2f33", "#a49e93", "#d39e59", "#a76936", "#737a84", "#7c838b")
+ c.Assert(len(gotColors), qt.Equals, len(expectedColors))
+ for i := range gotColors {
+ c1, c2 := gotColors[i], expectedColors[i]
+ c.Assert(c1.ColorHex(), qt.Equals, c2.ColorHex())
+ c.Assert(c1.ColorGo(), qt.DeepEquals, c2.ColorGo())
+ c.Assert(c1.Luminance(), qt.Equals, c2.Luminance())
+ }
c.Assert(image.RelPermalink(), qt.Equals, "/a/sunset.jpg")
c.Assert(image.ResourceType(), qt.Equals, "image")
getAndCheckExif(c, image)
}
+func TestImageColorsLuminance(t *testing.T) {
+ c := qt.New(t)
+
+ _, image := fetchSunset(c)
+ c.Assert(image, qt.Not(qt.IsNil))
+ colors, err := image.Colors()
+ c.Assert(err, qt.IsNil)
+ c.Assert(len(colors), qt.Equals, 6)
+ var prevLuminance float64
+ for i, color := range colors {
+ luminance := color.Luminance()
+ c.Assert(err, qt.IsNil)
+ c.Assert(luminance > 0, qt.IsTrue)
+ c.Assert(luminance, qt.Not(qt.Equals), prevLuminance, qt.Commentf("i=%d", i))
+ prevLuminance = luminance
+ }
+}
+
func BenchmarkImageExif(b *testing.B) {
getImages := func(c *qt.C, b *testing.B, fs afero.Fs) []images.ImageResource {
spec := newTestResourceSpec(specDescriptor{fs: fs, c: c})
import (
"encoding/hex"
"fmt"
+ "hash/fnv"
"image/color"
+ "math"
"strings"
+
+ "github.com/gohugoio/hugo/common/hstrings"
)
+type colorGoProvider interface {
+ ColorGo() color.Color
+}
+
+type Color struct {
+ // The color.
+ color color.Color
+
+ // The color prefixed with a #.
+ hex string
+
+ // The relative luminance of the color.
+ luminance float64
+}
+
+// Luminance as defined by w3.org.
+// See https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
+func (c Color) Luminance() float64 {
+ return c.luminance
+}
+
+// ColorGo returns the color as a color.Color.
+// For internal use only.
+func (c Color) ColorGo() color.Color {
+ return c.color
+}
+
+// ColorHex returns the color as a hex string prefixed with a #.
+func (c Color) ColorHex() string {
+ return c.hex
+}
+
+// String returns the color as a hex string prefixed with a #.
+func (c Color) String() string {
+ return c.hex
+}
+
+// For hashstructure. This struct is used in template func options
+// that needs to be able to hash a Color.
+// For internal use only.
+func (c Color) Hash() (uint64, error) {
+ h := fnv.New64a()
+ h.Write([]byte(c.hex))
+ return h.Sum64(), nil
+}
+
+func (c *Color) init() error {
+ c.hex = ColorGoToHexString(c.color)
+ r, g, b, _ := c.color.RGBA()
+ c.luminance = 0.2126*c.toSRGB(uint8(r)) + 0.7152*c.toSRGB(uint8(g)) + 0.0722*c.toSRGB(uint8(b))
+ return nil
+}
+
+func (c Color) toSRGB(i uint8) float64 {
+ v := float64(i) / 255
+ if v <= 0.04045 {
+ return v / 12.92
+ } else {
+ return math.Pow((v+0.055)/1.055, 2.4)
+ }
+}
+
// AddColorToPalette adds c as the first color in p if not already there.
// Note that it does no additional checks, so callers must make sure
// that the palette is valid for the relevant format.
p[p.Index(c)] = c
}
-// ColorToHexString converts a color to a hex string.
-func ColorToHexString(c color.Color) string {
+// ColorGoToHexString converts a color.Color to a hex string.
+func ColorGoToHexString(c color.Color) string {
r, g, b, a := c.RGBA()
rgba := color.RGBA{uint8(r), uint8(g), uint8(b), uint8(a)}
- return fmt.Sprintf("#%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B)
+ if rgba.A == 0xff {
+ return fmt.Sprintf("#%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B)
+ }
+ return fmt.Sprintf("#%.2x%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B, rgba.A)
+}
+
+// ColorGoToColor converts a color.Color to a Color.
+func ColorGoToColor(c color.Color) Color {
+ cc := Color{color: c}
+ if err := cc.init(); err != nil {
+ panic(err)
+ }
+ return cc
+}
+
+func hexStringToColor(s string) Color {
+ c, err := hexStringToColorGo(s)
+ if err != nil {
+ panic(err)
+ }
+ return ColorGoToColor(c)
+}
+
+// HexStringsToColors converts a slice of hex strings to a slice of Colors.
+func HexStringsToColors(s ...string) []Color {
+ var colors []Color
+ for _, v := range s {
+ colors = append(colors, hexStringToColor(v))
+ }
+ return colors
+}
+
+func toColorGo(v any) (color.Color, bool, error) {
+ switch vv := v.(type) {
+ case colorGoProvider:
+ return vv.ColorGo(), true, nil
+ default:
+ s, ok := hstrings.ToString(v)
+ if !ok {
+ return nil, false, nil
+ }
+ c, err := hexStringToColorGo(s)
+ if err != nil {
+ return nil, false, err
+ }
+ return c, true, nil
+ }
}
-func hexStringToColor(s string) (color.Color, error) {
+func hexStringToColorGo(s string) (color.Color, error) {
s = strings.TrimPrefix(s, "#")
if len(s) != 3 && len(s) != 4 && len(s) != 6 && len(s) != 8 {
c.Run(test.arg, func(c *qt.C) {
c.Parallel()
- result, err := hexStringToColor(test.arg)
+ result, err := hexStringToColorGo(test.arg)
if b, ok := test.expect.(bool); ok && !b {
c.Assert(err, qt.Not(qt.IsNil))
{color.White, "#ffffff"},
{color.Black, "#000000"},
{color.RGBA{R: 0x42, G: 0x87, B: 0xf5, A: 0xff}, "#4287f5"},
+
+ // 50% opacity.
+ // Note that the .Colors (dominant colors) received from the Image resource
+ // will always have an alpha value of 0xff.
+ {color.RGBA{R: 0x42, G: 0x87, B: 0xf5, A: 0x80}, "#4287f580"},
} {
test := test
c.Run(test.expect, func(c *qt.C) {
c.Parallel()
- result := ColorToHexString(test.arg)
+ result := ColorGoToHexString(test.arg)
c.Assert(result, qt.Equals, test.expect)
})
c.Assert(AddColorToPalette(color.White, palette), qt.HasLen, 2)
- blue1, _ := hexStringToColor("34c3eb")
- blue2, _ := hexStringToColor("34c3eb")
- white, _ := hexStringToColor("fff")
+ blue1, _ := hexStringToColorGo("34c3eb")
+ blue2, _ := hexStringToColorGo("34c3eb")
+ white, _ := hexStringToColorGo("fff")
c.Assert(AddColorToPalette(white, palette), qt.HasLen, 2)
c.Assert(AddColorToPalette(blue1, palette), qt.HasLen, 3)
c := qt.New(t)
palette := color.Palette{color.White, color.Black}
- offWhite, _ := hexStringToColor("fcfcfc")
+ offWhite, _ := hexStringToColorGo("fcfcfc")
ReplaceColorInPalette(offWhite, palette)
c.Assert(palette, qt.HasLen, 2)
c.Assert(palette[0], qt.Equals, offWhite)
}
+
+func TestColorLuminance(t *testing.T) {
+ c := qt.New(t)
+ c.Assert(hexStringToColor("#000000").Luminance(), qt.Equals, 0.0)
+ c.Assert(hexStringToColor("#768a9a").Luminance(), qt.Equals, 0.24361603589088263)
+ c.Assert(hexStringToColor("#d5bc9f").Luminance(), qt.Equals, 0.5261577672685374)
+ c.Assert(hexStringToColor("#ffffff").Luminance(), qt.Equals, 1.0)
+}
return i, nil, err
}
- i.BgColor, err = hexStringToColor(i.Imaging.BgColor)
+ i.BgColor, err = hexStringToColorGo(i.Imaging.BgColor)
if err != nil {
return i, nil, err
}
c.Hint = hint
} else if part[0] == '#' {
c.BgColorStr = part[1:]
- c.BgColor, err = hexStringToColor(c.BgColorStr)
+ c.BgColor, err = hexStringToColorGo(c.BgColorStr)
if err != nil {
return c, err
}
func (i *ImagingConfigInternal) Compile(externalCfg *ImagingConfig) error {
var err error
- i.BgColor, err = hexStringToColor(externalCfg.BgColor)
+ i.BgColor, err = hexStringToColorGo(externalCfg.BgColor)
if err != nil {
return err
}
c.qualitySetForImage = quality != 75
c.Rotate = rotate
c.BgColorStr = bgColor
- c.BgColor, _ = hexStringToColor(bgColor)
+ c.BgColor, _ = hexStringToColorGo(bgColor)
if filter != "" {
filter = strings.ToLower(filter)
-// Copyright 2019 The Hugo Authors. All rights reserved.
+// 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.
func (*Filters) Text(text string, options ...any) gift.Filter {
tf := textFilter{
text: text,
- color: "#ffffff",
+ color: color.White,
size: 20,
x: 10,
y: 10,
for option, v := range opt {
switch option {
case "color":
- tf.color = cast.ToString(v)
+ if color, ok, _ := toColorGo(v); ok {
+ tf.color = color
+ }
case "size":
tf.size = cast.ToFloat64(v)
case "x":
var top, right, bottom, left int
var ccolor color.Color = color.White // canvas color
- var err error
_args := args // preserve original args for most stable hash
- if vcs, ok := (args[len(args)-1]).(string); ok {
- ccolor, err = hexStringToColor(vcs)
+ if vcs, ok, err := toColorGo(args[len(args)-1]); ok || err != nil {
if err != nil {
panic("invalid canvas color: specify RGB or RGBA using hex notation")
}
+ ccolor = vcs
args = args[:len(args)-1]
if len(args) == 0 {
panic("not enough arguments: provide one or more padding values using the CSS shorthand property syntax")
// Dither creates a filter that dithers an image.
func (*Filters) Dither(options ...any) gift.Filter {
ditherOptions := struct {
- Colors []string
+ Colors []any
Method string
Serpentine bool
Strength float32
}{
- Colors: []string{"000000ff", "ffffffff"},
Method: "floydsteinberg",
Serpentine: true,
Strength: 1.0,
}
}
+ if len(ditherOptions.Colors) == 0 {
+ ditherOptions.Colors = []any{"000000ff", "ffffffff"}
+ }
+
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 {
+ cc, ok, err := toColorGo(c)
+ if !ok || err != nil {
panic(fmt.Sprintf("%q is an invalid color: specify RGB or RGBA using hexadecimal notation", c))
}
palette = append(palette, cc)
// Colors returns a slice of the most dominant colors in an image
// using a simple histogram method.
- Colors() ([]string, error)
+ Colors() ([]Color, error)
// For internal use.
DecodeImage() (image.Image, error)
import (
"image"
+ "image/color"
"image/draw"
"io"
"strings"
var _ gift.Filter = (*textFilter)(nil)
type textFilter struct {
- text, color string
+ text string
+ color color.Color
x, y int
size float64
linespacing int
}
func (f textFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) {
- color, err := hexStringToColor(f.color)
- if err != nil {
- panic(err)
- }
-
// Load and parse font
ttf := goregular.TTF
if f.fontSource != nil {
d := font.Drawer{
Dst: dst,
- Src: image.NewUniform(color),
+ Src: image.NewUniform(f.color),
Face: face,
}
return r.getImageOps().Exif()
}
-func (r *resourceAdapter) Colors() ([]string, error) {
+func (r *resourceAdapter) Colors() ([]images.Color, error) {
return r.getImageOps().Colors()
}