mirror of
				https://github.com/gohugoio/hugo.git
				synced 2024-05-11 05:54:58 +00:00 
			
		
		
		
	resources/images: Add $image.Colors
Which returns the most dominant colors of an image using a simple histogram method. Fixes #10307
This commit is contained in:
		| @@ -163,6 +163,19 @@ Sometimes it can be useful to create the filter chain once and then reuse it. | |||||||
| {{ $image2 := $image2.Filter $filters }} | {{ $image2 := $image2.Filter $filters }} | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | ### Colors | ||||||
|  |  | ||||||
|  | {{< new-in "0.104.0" >}} | ||||||
|  |  | ||||||
|  | `.Colors` returns a slice of hex string with the dominant colors in the image using a simple histogram method. | ||||||
|  |  | ||||||
|  | ```go-html-template | ||||||
|  | {{ $colors := $image.Colors }} | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | This method is fast, but if you also scale down your images, it would be good for performance to extract the colors from the scaled down image. | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Exif | ### Exif | ||||||
|  |  | ||||||
| Provides an [Exif] object containing image metadata. | Provides an [Exif] object containing image metadata. | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							| @@ -91,6 +91,7 @@ require ( | |||||||
| 	github.com/aws/aws-sdk-go-v2/service/sso v1.4.0 // indirect | 	github.com/aws/aws-sdk-go-v2/service/sso v1.4.0 // indirect | ||||||
| 	github.com/aws/aws-sdk-go-v2/service/sts v1.7.0 // indirect | 	github.com/aws/aws-sdk-go-v2/service/sts v1.7.0 // indirect | ||||||
| 	github.com/aws/smithy-go v1.8.0 // indirect | 	github.com/aws/smithy-go v1.8.0 // indirect | ||||||
|  | 	github.com/cenkalti/dominantcolor v1.0.0 // indirect | ||||||
| 	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect | 	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect | ||||||
| 	github.com/dlclark/regexp2 v1.4.0 // indirect | 	github.com/dlclark/regexp2 v1.4.0 // indirect | ||||||
| 	github.com/go-openapi/jsonpointer v0.19.5 // indirect | 	github.com/go-openapi/jsonpointer v0.19.5 // indirect | ||||||
| @@ -108,6 +109,7 @@ require ( | |||||||
| 	github.com/kr/pretty v0.3.0 // indirect | 	github.com/kr/pretty v0.3.0 // indirect | ||||||
| 	github.com/kr/text v0.2.0 // indirect | 	github.com/kr/text v0.2.0 // indirect | ||||||
| 	github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e // indirect | 	github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e // indirect | ||||||
|  | 	github.com/marekm4/color-extractor v1.2.0 // indirect | ||||||
| 	github.com/mattn/go-ieproxy v0.0.1 // indirect | 	github.com/mattn/go-ieproxy v0.0.1 // indirect | ||||||
| 	github.com/mattn/go-runewidth v0.0.9 // indirect | 	github.com/mattn/go-runewidth v0.0.9 // indirect | ||||||
| 	github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect | 	github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							| @@ -184,6 +184,8 @@ github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI= | |||||||
| github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0= | github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0= | ||||||
| github.com/bep/workers v1.0.0 h1:U+H8YmEaBCEaFZBst7GcRVEoqeRC9dzH2dWOwGmOchg= | github.com/bep/workers v1.0.0 h1:U+H8YmEaBCEaFZBst7GcRVEoqeRC9dzH2dWOwGmOchg= | ||||||
| github.com/bep/workers v1.0.0/go.mod h1:7kIESOB86HfR2379pwoMWNy8B50D7r99fRLUyPSNyCs= | github.com/bep/workers v1.0.0/go.mod h1:7kIESOB86HfR2379pwoMWNy8B50D7r99fRLUyPSNyCs= | ||||||
|  | github.com/cenkalti/dominantcolor v1.0.0 h1:MFLKUzcxQf65GRQdCcpcMlEFYvvy4Y51+eJ4bLpe4bM= | ||||||
|  | github.com/cenkalti/dominantcolor v1.0.0/go.mod h1:/fauwSWvIFhvyrHSOhqRwdnjZLETEl5ocyxCkakCI/Q= | ||||||
| github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= | ||||||
| github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= | github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= | ||||||
| github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= | ||||||
| @@ -437,6 +439,8 @@ github.com/magefile/mage v1.13.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXq | |||||||
| github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= | ||||||
| github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= | ||||||
| github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= | ||||||
|  | github.com/marekm4/color-extractor v1.2.0 h1:DCU/FXg3PlAwig7W5PRZshiX5x38k0aNPTxYZ6/fZb0= | ||||||
|  | github.com/marekm4/color-extractor v1.2.0/go.mod h1:90VjmiHI6M8ez9eYUaXLdcKnS+BAOp7w+NpwBdkJmpA= | ||||||
| github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= | github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= | ||||||
| github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI= | github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI= | ||||||
| github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= | github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= | ||||||
|   | |||||||
| @@ -123,6 +123,10 @@ func (e *errorResource) Exif() *exif.ExifInfo { | |||||||
| 	panic(e.ResourceError) | 	panic(e.ResourceError) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (e *errorResource) Colors() ([]string, error) { | ||||||
|  | 	panic(e.ResourceError) | ||||||
|  | } | ||||||
|  |  | ||||||
| func (e *errorResource) DecodeImage() (image.Image, error) { | func (e *errorResource) DecodeImage() (image.Image, error) { | ||||||
| 	panic(e.ResourceError) | 	panic(e.ResourceError) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -30,6 +30,8 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
|  |  | ||||||
|  | 	color_extractor "github.com/marekm4/color-extractor" | ||||||
|  |  | ||||||
| 	"github.com/gohugoio/hugo/common/paths" | 	"github.com/gohugoio/hugo/common/paths" | ||||||
|  |  | ||||||
| 	"github.com/disintegration/gift" | 	"github.com/disintegration/gift" | ||||||
| @@ -64,6 +66,9 @@ type imageResource struct { | |||||||
| 	metaInitErr error | 	metaInitErr error | ||||||
| 	meta        *imageMeta | 	meta        *imageMeta | ||||||
|  |  | ||||||
|  | 	dominantColorInit sync.Once | ||||||
|  | 	dominantColors    []string | ||||||
|  |  | ||||||
| 	baseResource | 	baseResource | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -135,6 +140,24 @@ func (i *imageResource) getExif() *exif.ExifInfo { | |||||||
| 	return i.meta.Exif | 	return i.meta.Exif | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Colors returns a slice of the most dominant colors in an image | ||||||
|  | // using a simple histogram method. | ||||||
|  | func (i *imageResource) Colors() ([]string, error) { | ||||||
|  | 	var err error | ||||||
|  | 	i.dominantColorInit.Do(func() { | ||||||
|  | 		var img image.Image | ||||||
|  | 		img, err = i.DecodeImage() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		colors := color_extractor.ExtractColors(img) | ||||||
|  | 		for _, c := range colors { | ||||||
|  | 			i.dominantColors = append(i.dominantColors, images.ColorToHexString(c)) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | 	return i.dominantColors, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| // Clone is for internal use. | // Clone is for internal use. | ||||||
| func (i *imageResource) Clone() resource.Resource { | func (i *imageResource) Clone() resource.Resource { | ||||||
| 	gr := i.baseResource.Clone().(baseResource) | 	gr := i.baseResource.Clone().(baseResource) | ||||||
|   | |||||||
| @@ -84,6 +84,10 @@ func TestImageTransformBasic(t *testing.T) { | |||||||
| 		c.Assert(img.Height(), qt.Equals, h) | 		c.Assert(img.Height(), qt.Equals, h) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	colors, err := image.Colors() | ||||||
|  | 	c.Assert(err, qt.IsNil) | ||||||
|  | 	c.Assert(colors, qt.DeepEquals, []string{"#2d2f33", "#a49e93", "#d39e59", "#a76936", "#737a84", "#7c838b"}) | ||||||
|  |  | ||||||
| 	c.Assert(image.RelPermalink(), qt.Equals, "/a/sunset.jpg") | 	c.Assert(image.RelPermalink(), qt.Equals, "/a/sunset.jpg") | ||||||
| 	c.Assert(image.ResourceType(), qt.Equals, "image") | 	c.Assert(image.ResourceType(), qt.Equals, "image") | ||||||
| 	assertWidthHeight(image, 900, 562) | 	assertWidthHeight(image, 900, 562) | ||||||
|   | |||||||
| @@ -45,6 +45,14 @@ func ReplaceColorInPalette(c color.Color, p color.Palette) { | |||||||
| 	p[p.Index(c)] = c | 	p[p.Index(c)] = c | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // ColorToHexString converts a color to a hex string. | ||||||
|  | func ColorToHexString(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) | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
| func hexStringToColor(s string) (color.Color, error) { | func hexStringToColor(s string) (color.Color, error) { | ||||||
| 	s = strings.TrimPrefix(s, "#") | 	s = strings.TrimPrefix(s, "#") | ||||||
|  |  | ||||||
|   | |||||||
| @@ -60,6 +60,30 @@ func TestHexStringToColor(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestColorToHexString(t *testing.T) { | ||||||
|  | 	c := qt.New(t) | ||||||
|  |  | ||||||
|  | 	for _, test := range []struct { | ||||||
|  | 		arg    color.Color | ||||||
|  | 		expect string | ||||||
|  | 	}{ | ||||||
|  | 		{color.White, "#ffffff"}, | ||||||
|  | 		{color.Black, "#000000"}, | ||||||
|  | 		{color.RGBA{R: 0x42, G: 0x87, B: 0xf5, A: 0xff}, "#4287f5"}, | ||||||
|  | 	} { | ||||||
|  |  | ||||||
|  | 		test := test | ||||||
|  | 		c.Run(test.expect, func(c *qt.C) { | ||||||
|  | 			c.Parallel() | ||||||
|  |  | ||||||
|  | 			result := ColorToHexString(test.arg) | ||||||
|  |  | ||||||
|  | 			c.Assert(result, qt.Equals, test.expect) | ||||||
|  | 		}) | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| func TestAddColorToPalette(t *testing.T) { | func TestAddColorToPalette(t *testing.T) { | ||||||
| 	c := qt.New(t) | 	c := qt.New(t) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -48,6 +48,10 @@ type ImageResourceOps interface { | |||||||
| 	// Exif returns an ExifInfo object containing Image metadata. | 	// Exif returns an ExifInfo object containing Image metadata. | ||||||
| 	Exif() *exif.ExifInfo | 	Exif() *exif.ExifInfo | ||||||
|  |  | ||||||
|  | 	// Colors returns a slice of the most dominant colors in an image | ||||||
|  | 	// using a simple histogram method. | ||||||
|  | 	Colors() ([]string, error) | ||||||
|  |  | ||||||
| 	// Internal | 	// Internal | ||||||
| 	DecodeImage() (image.Image, error) | 	DecodeImage() (image.Image, error) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -213,6 +213,10 @@ func (r *resourceAdapter) Exif() *exif.ExifInfo { | |||||||
| 	return r.getImageOps().Exif() | 	return r.getImageOps().Exif() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (r *resourceAdapter) Colors() ([]string, error) { | ||||||
|  | 	return r.getImageOps().Colors() | ||||||
|  | } | ||||||
|  |  | ||||||
| func (r *resourceAdapter) Key() string { | func (r *resourceAdapter) Key() string { | ||||||
| 	r.init(false, false) | 	r.init(false, false) | ||||||
| 	return r.target.(resource.Identifier).Key() | 	return r.target.(resource.Identifier).Key() | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user