21
vendor/github.com/mat/besticon/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Matthias Lüdtke, Hamburg - https://github.com/mat
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
4610
vendor/github.com/mat/besticon/NOTICES
generated
vendored
Normal file
561
vendor/github.com/mat/besticon/besticon/besticon.go
generated
vendored
Normal file
@@ -0,0 +1,561 @@
|
||||
// Package besticon includes functions
|
||||
// finding icons for a given web site.
|
||||
package besticon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"image/color"
|
||||
|
||||
// Load supported image formats.
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
|
||||
_ "github.com/mat/besticon/ico"
|
||||
|
||||
"github.com/mat/besticon/colorfinder"
|
||||
|
||||
"golang.org/x/net/html/charset"
|
||||
"golang.org/x/net/idna"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
var defaultFormats []string
|
||||
|
||||
const MinIconSize = 0
|
||||
|
||||
// TODO: Turn into env var: https://github.com/rendomnet/besticon/commit/c85867cc80c00c898053ce8daf40d51a93b9d39f#diff-37b57e3fdbe4246771791e86deb4d69dL41
|
||||
const MaxIconSize = 500
|
||||
|
||||
// Icon holds icon information.
|
||||
type Icon struct {
|
||||
URL string `json:"url"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Format string `json:"format"`
|
||||
Bytes int `json:"bytes"`
|
||||
Error error `json:"error"`
|
||||
Sha1sum string `json:"sha1sum"`
|
||||
ImageData []byte `json:",omitempty"`
|
||||
}
|
||||
|
||||
type IconFinder struct {
|
||||
FormatsAllowed []string
|
||||
HostOnlyDomains []string
|
||||
KeepImageBytes bool
|
||||
icons []Icon
|
||||
}
|
||||
|
||||
func (f *IconFinder) FetchIcons(url string) ([]Icon, error) {
|
||||
url = strings.TrimSpace(url)
|
||||
if !strings.HasPrefix(url, "http:") && !strings.HasPrefix(url, "https:") {
|
||||
url = "http://" + url
|
||||
}
|
||||
|
||||
url = f.stripIfNecessary(url)
|
||||
|
||||
var err error
|
||||
|
||||
if CacheEnabled() {
|
||||
f.icons, err = resultFromCache(url)
|
||||
} else {
|
||||
f.icons, err = fetchIcons(url)
|
||||
}
|
||||
|
||||
return f.Icons(), err
|
||||
}
|
||||
|
||||
// stripIfNecessary removes everything from URL but the Scheme and Host
|
||||
// part if URL.Host is found in HostOnlyDomains.
|
||||
// This can be used for very popular domains like youtube.com where throttling is
|
||||
// an issue.
|
||||
func (f *IconFinder) stripIfNecessary(URL string) string {
|
||||
u, e := url.Parse(URL)
|
||||
if e != nil {
|
||||
return URL
|
||||
}
|
||||
|
||||
for _, h := range f.HostOnlyDomains {
|
||||
if h == u.Host || h == "*" {
|
||||
domainOnlyURL := url.URL{Scheme: u.Scheme, Host: u.Host}
|
||||
return domainOnlyURL.String()
|
||||
}
|
||||
}
|
||||
|
||||
return URL
|
||||
}
|
||||
|
||||
func (f *IconFinder) IconInSizeRange(r SizeRange) *Icon {
|
||||
icons := f.Icons()
|
||||
|
||||
// 1. SVG always wins
|
||||
for _, ico := range icons {
|
||||
if ico.Format == "svg" {
|
||||
return &ico
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Try to return smallest in range perfect..max
|
||||
sortIcons(icons, false)
|
||||
for _, ico := range icons {
|
||||
if (ico.Width >= r.Perfect && ico.Height >= r.Perfect) && (ico.Width <= r.Max && ico.Height <= r.Max) {
|
||||
return &ico
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Try to return biggest in range perfect..min
|
||||
sortIcons(icons, true)
|
||||
for _, ico := range icons {
|
||||
if (ico.Width >= r.Min && ico.Height >= r.Min) && (ico.Width <= r.Perfect && ico.Height <= r.Perfect) {
|
||||
return &ico
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *IconFinder) MainColorForIcons() *color.RGBA {
|
||||
return MainColorForIcons(f.icons)
|
||||
}
|
||||
|
||||
func (f *IconFinder) Icons() []Icon {
|
||||
return discardUnwantedFormats(f.icons, f.FormatsAllowed)
|
||||
}
|
||||
|
||||
func (ico *Icon) Image() (*image.Image, error) {
|
||||
img, _, err := image.Decode(bytes.NewReader(ico.ImageData))
|
||||
return &img, err
|
||||
}
|
||||
|
||||
func discardUnwantedFormats(icons []Icon, wantedFormats []string) []Icon {
|
||||
formats := defaultFormats
|
||||
if len(wantedFormats) > 0 {
|
||||
formats = wantedFormats
|
||||
}
|
||||
|
||||
return filterIcons(icons, func(ico Icon) bool {
|
||||
return includesString(formats, ico.Format)
|
||||
})
|
||||
}
|
||||
|
||||
type iconPredicate func(Icon) bool
|
||||
|
||||
func filterIcons(icons []Icon, pred iconPredicate) []Icon {
|
||||
var result []Icon
|
||||
for _, ico := range icons {
|
||||
if pred(ico) {
|
||||
result = append(result, ico)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func includesString(arr []string, str string) bool {
|
||||
for _, e := range arr {
|
||||
if e == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func fetchIcons(siteURL string) ([]Icon, error) {
|
||||
var links []string
|
||||
|
||||
html, urlAfterRedirect, e := fetchHTML(siteURL)
|
||||
if e == nil {
|
||||
// Search HTML for icons
|
||||
links, e = findIconLinks(urlAfterRedirect, html)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
} else {
|
||||
// Unable to fetch the response or got a bad HTTP status code. Try default
|
||||
// icon paths. https://github.com/mat/besticon/discussions/47
|
||||
links, e = defaultIconURLs(siteURL)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
}
|
||||
|
||||
icons := fetchAllIcons(links)
|
||||
icons = rejectBrokenIcons(icons)
|
||||
sortIcons(icons, true)
|
||||
|
||||
return icons, nil
|
||||
}
|
||||
|
||||
const maxResponseBodySize = 10485760 // 10MB
|
||||
|
||||
func fetchHTML(url string) ([]byte, *url.URL, error) {
|
||||
r, e := Get(url)
|
||||
if e != nil {
|
||||
return nil, nil, e
|
||||
}
|
||||
|
||||
if !(r.StatusCode >= 200 && r.StatusCode < 300) {
|
||||
return nil, nil, errors.New("besticon: not found")
|
||||
}
|
||||
|
||||
b, e := GetBodyBytes(r)
|
||||
if e != nil {
|
||||
return nil, nil, e
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil, nil, errors.New("besticon: empty response")
|
||||
}
|
||||
|
||||
reader := bytes.NewReader(b)
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
utf8reader, e := charset.NewReader(reader, contentType)
|
||||
if e != nil {
|
||||
return nil, nil, e
|
||||
}
|
||||
utf8bytes, e := ioutil.ReadAll(utf8reader)
|
||||
if e != nil {
|
||||
return nil, nil, e
|
||||
}
|
||||
|
||||
return utf8bytes, r.Request.URL, nil
|
||||
}
|
||||
|
||||
func MainColorForIcons(icons []Icon) *color.RGBA {
|
||||
if len(icons) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var icon *Icon
|
||||
// Prefer gif, jpg, png
|
||||
for _, ico := range icons {
|
||||
if ico.Format == "gif" || ico.Format == "jpg" || ico.Format == "png" {
|
||||
icon = &ico
|
||||
break
|
||||
}
|
||||
}
|
||||
// Try .ico else
|
||||
if icon == nil {
|
||||
for _, ico := range icons {
|
||||
if ico.Format == "ico" {
|
||||
icon = &ico
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if icon == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
img, err := icon.Image()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cf := colorfinder.ColorFinder{}
|
||||
mainColor, err := cf.FindMainColor(*img)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &mainColor
|
||||
}
|
||||
|
||||
// Construct default icon URLs. A fallback if we can't fetch the HTML.
|
||||
func defaultIconURLs(siteURL string) ([]string, error) {
|
||||
baseURL, e := url.Parse(siteURL)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
var links []string
|
||||
for _, path := range iconPaths {
|
||||
absoluteURL, e := absoluteURL(baseURL, path)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
links = append(links, absoluteURL)
|
||||
}
|
||||
|
||||
return links, nil
|
||||
}
|
||||
|
||||
func fetchAllIcons(urls []string) []Icon {
|
||||
ch := make(chan Icon)
|
||||
|
||||
for _, u := range urls {
|
||||
go func(u string) { ch <- fetchIconDetails(u) }(u)
|
||||
}
|
||||
|
||||
var icons []Icon
|
||||
for range urls {
|
||||
icon := <-ch
|
||||
icons = append(icons, icon)
|
||||
}
|
||||
return icons
|
||||
}
|
||||
|
||||
func fetchIconDetails(url string) Icon {
|
||||
i := Icon{URL: url}
|
||||
|
||||
response, e := Get(url)
|
||||
if e != nil {
|
||||
i.Error = e
|
||||
return i
|
||||
}
|
||||
|
||||
b, e := GetBodyBytes(response)
|
||||
if e != nil {
|
||||
i.Error = e
|
||||
return i
|
||||
}
|
||||
|
||||
if isSVG(b) {
|
||||
// Special handling for svg, which golang can't decode with
|
||||
// image.DecodeConfig. Fill in an absurdly large width/height so SVG always
|
||||
// wins size contests.
|
||||
i.Format = "svg"
|
||||
i.Width = 9999
|
||||
i.Height = 9999
|
||||
} else {
|
||||
cfg, format, e := image.DecodeConfig(bytes.NewReader(b))
|
||||
if e != nil {
|
||||
i.Error = fmt.Errorf("besticon: unknown image format: %s", e)
|
||||
return i
|
||||
}
|
||||
|
||||
// jpeg => jpg
|
||||
if format == "jpeg" {
|
||||
format = "jpg"
|
||||
}
|
||||
|
||||
i.Width = cfg.Width
|
||||
i.Height = cfg.Height
|
||||
i.Format = format
|
||||
}
|
||||
|
||||
i.Bytes = len(b)
|
||||
i.Sha1sum = sha1Sum(b)
|
||||
if keepImageBytes {
|
||||
i.ImageData = b
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// SVG detector. We can't use image.RegisterFormat, since RegisterFormat is
|
||||
// limited to a simple magic number check. It's easy to confuse the first few
|
||||
// bytes of HTML with SVG.
|
||||
func isSVG(body []byte) bool {
|
||||
// is it long enough?
|
||||
if len(body) < 10 {
|
||||
return false
|
||||
}
|
||||
|
||||
// does it start with something reasonable?
|
||||
switch {
|
||||
case bytes.Equal(body[0:2], []byte("<!")):
|
||||
case bytes.Equal(body[0:2], []byte("<?")):
|
||||
case bytes.Equal(body[0:4], []byte("<svg")):
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
// is there an <svg in the first 300 bytes?
|
||||
if off := bytes.Index(body, []byte("<svg")); off == -1 || off > 300 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func Get(urlstring string) (*http.Response, error) {
|
||||
u, e := url.Parse(urlstring)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
// Maybe we can get rid of this conversion someday
|
||||
// https://github.com/golang/go/issues/13835
|
||||
u.Host, e = idna.ToASCII(u.Host)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
req, e := http.NewRequest("GET", u.String(), nil)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
setDefaultHeaders(req)
|
||||
|
||||
start := time.Now()
|
||||
resp, err := client.Do(req)
|
||||
end := time.Now()
|
||||
duration := end.Sub(start)
|
||||
|
||||
if err != nil {
|
||||
logger.Printf("Error: %s %s %s %.2fms",
|
||||
req.Method,
|
||||
req.URL,
|
||||
err,
|
||||
float64(duration)/float64(time.Millisecond),
|
||||
)
|
||||
} else {
|
||||
logger.Printf("%s %s %d %.2fms %d",
|
||||
req.Method,
|
||||
req.URL,
|
||||
resp.StatusCode,
|
||||
float64(duration)/float64(time.Millisecond),
|
||||
resp.ContentLength,
|
||||
)
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func GetBodyBytes(r *http.Response) ([]byte, error) {
|
||||
limitReader := io.LimitReader(r.Body, maxResponseBodySize)
|
||||
b, e := ioutil.ReadAll(limitReader)
|
||||
r.Body.Close()
|
||||
|
||||
if len(b) >= maxResponseBodySize {
|
||||
return nil, errors.New("body too large")
|
||||
}
|
||||
return b, e
|
||||
}
|
||||
|
||||
func setDefaultHeaders(req *http.Request) {
|
||||
req.Header.Set("Accept", "*/*")
|
||||
req.Header.Set("User-Agent", getenvOrFallback("HTTP_USER_AGENT", "Mozilla/5.0 (iPhone; CPU iPhone OS 10_0 like Mac OS X) AppleWebKit/602.1.38 (KHTML, like Gecko) Version/10.0 Mobile/14A5297c Safari/602.1"))
|
||||
}
|
||||
|
||||
func mustInitCookieJar() *cookiejar.Jar {
|
||||
options := cookiejar.Options{
|
||||
PublicSuffixList: publicsuffix.List,
|
||||
}
|
||||
jar, e := cookiejar.New(&options)
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
|
||||
return jar
|
||||
}
|
||||
|
||||
func checkRedirect(req *http.Request, via []*http.Request) error {
|
||||
setDefaultHeaders(req)
|
||||
|
||||
if len(via) >= 10 {
|
||||
return errors.New("stopped after 10 redirects")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func absoluteURL(baseURL *url.URL, path string) (string, error) {
|
||||
u, e := url.Parse(path)
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
|
||||
u.Scheme = baseURL.Scheme
|
||||
if u.Scheme == "" {
|
||||
u.Scheme = "http"
|
||||
}
|
||||
|
||||
if u.Host == "" {
|
||||
u.Host = baseURL.Host
|
||||
}
|
||||
return baseURL.ResolveReference(u).String(), nil
|
||||
}
|
||||
|
||||
func urlFromBase(baseURL *url.URL, path string) string {
|
||||
u := *baseURL
|
||||
u.Path = path
|
||||
if u.Scheme == "" {
|
||||
u.Scheme = "http"
|
||||
}
|
||||
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func rejectBrokenIcons(icons []Icon) []Icon {
|
||||
var result []Icon
|
||||
for _, img := range icons {
|
||||
if img.Error == nil && (img.Width > 1 && img.Height > 1) {
|
||||
result = append(result, img)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func sha1Sum(b []byte) string {
|
||||
hash := sha1.New()
|
||||
hash.Write(b)
|
||||
bs := hash.Sum(nil)
|
||||
return fmt.Sprintf("%x", bs)
|
||||
}
|
||||
|
||||
var client *http.Client
|
||||
var keepImageBytes bool
|
||||
|
||||
func init() {
|
||||
duration, e := time.ParseDuration(getenvOrFallback("HTTP_CLIENT_TIMEOUT", "5s"))
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
setHTTPClient(&http.Client{Timeout: duration})
|
||||
|
||||
// see
|
||||
// https://github.com/mat/besticon/pull/52/commits/208e9dcbdbdeb7ef7491bb42f1bc449e87e084a2
|
||||
// when we are ready to add support for the FORMATS env variable
|
||||
|
||||
defaultFormats = []string{"gif", "ico", "jpg", "png"}
|
||||
}
|
||||
|
||||
func setHTTPClient(c *http.Client) {
|
||||
c.Jar = mustInitCookieJar()
|
||||
c.CheckRedirect = checkRedirect
|
||||
client = c
|
||||
}
|
||||
|
||||
var logger *log.Logger
|
||||
|
||||
// SetLogOutput sets the output for the package's logger.
|
||||
func SetLogOutput(w io.Writer) {
|
||||
logger = log.New(w, "http: ", log.LstdFlags|log.Lmicroseconds)
|
||||
}
|
||||
|
||||
func init() {
|
||||
SetLogOutput(os.Stdout)
|
||||
keepImageBytes = true
|
||||
}
|
||||
|
||||
func getenvOrFallback(key string, fallbackValue string) string {
|
||||
value := strings.TrimSpace(os.Getenv(key))
|
||||
if len(value) != 0 {
|
||||
return value
|
||||
}
|
||||
return fallbackValue
|
||||
}
|
||||
|
||||
func getenvOrFallbackArray(key string, fallbackValue []string) []string {
|
||||
value := strings.TrimSpace(os.Getenv(key))
|
||||
if len(value) != 0 {
|
||||
return strings.Split(value, ",")
|
||||
}
|
||||
return fallbackValue
|
||||
}
|
||||
|
||||
var BuildDate string // set via ldflags on Make
|
||||
90
vendor/github.com/mat/besticon/besticon/caching.go
generated
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
package besticon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang/groupcache"
|
||||
)
|
||||
|
||||
var iconCache *groupcache.Group
|
||||
|
||||
const contextKeySiteURL = "siteURL"
|
||||
|
||||
type result struct {
|
||||
Icons []Icon
|
||||
Error string
|
||||
}
|
||||
|
||||
func resultFromCache(siteURL string) ([]Icon, error) {
|
||||
if iconCache == nil {
|
||||
return fetchIcons(siteURL)
|
||||
}
|
||||
|
||||
c := context.WithValue(context.Background(), contextKeySiteURL, siteURL)
|
||||
var data []byte
|
||||
err := iconCache.Get(c, cacheKey(siteURL), groupcache.AllocatingByteSliceSink(&data))
|
||||
if err != nil {
|
||||
logger.Println("ERR:", err)
|
||||
return fetchIcons(siteURL)
|
||||
}
|
||||
|
||||
res := &result{}
|
||||
err = json.Unmarshal(data, res)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if res.Error != "" {
|
||||
return res.Icons, errors.New(res.Error)
|
||||
}
|
||||
return res.Icons, nil
|
||||
}
|
||||
|
||||
func cacheKey(siteURL string) string {
|
||||
// Let results expire after a day
|
||||
now := time.Now()
|
||||
return fmt.Sprintf("%d-%02d-%02d-%s", now.Year(), now.Month(), now.Day(), siteURL)
|
||||
}
|
||||
|
||||
func generatorFunc(ctx context.Context, key string, sink groupcache.Sink) error {
|
||||
siteURL := ctx.Value(contextKeySiteURL).(string)
|
||||
icons, err := fetchIcons(siteURL)
|
||||
if err != nil {
|
||||
// Don't cache errors
|
||||
return err
|
||||
}
|
||||
|
||||
res := result{Icons: icons}
|
||||
if err != nil {
|
||||
res.Error = err.Error()
|
||||
}
|
||||
bytes, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
sink.SetBytes(bytes)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CacheEnabled() bool {
|
||||
return iconCache != nil
|
||||
}
|
||||
|
||||
// SetCacheMaxSize enables icon caching if sizeInMB > 0.
|
||||
func SetCacheMaxSize(sizeInMB int64) {
|
||||
if sizeInMB > 0 {
|
||||
iconCache = groupcache.NewGroup("icons", sizeInMB<<20, groupcache.GetterFunc(generatorFunc))
|
||||
} else {
|
||||
iconCache = nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetCacheStats returns cache statistics.
|
||||
func GetCacheStats() groupcache.CacheStats {
|
||||
return iconCache.CacheStats(groupcache.MainCache)
|
||||
}
|
||||
151
vendor/github.com/mat/besticon/besticon/extract.go
generated
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
package besticon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
var iconPaths = []string{
|
||||
"/favicon.ico",
|
||||
"/apple-touch-icon.png",
|
||||
"/apple-touch-icon-precomposed.png",
|
||||
}
|
||||
|
||||
const (
|
||||
favIcon = "icon"
|
||||
appleTouchIcon = "apple-touch-icon"
|
||||
appleTouchIconPrecomposed = "apple-touch-icon-precomposed"
|
||||
)
|
||||
|
||||
type empty struct{}
|
||||
|
||||
// Find all icons in this html. We use siteURL as the base url unless we detect
|
||||
// another base url in <head>
|
||||
func findIconLinks(siteURL *url.URL, html []byte) ([]string, error) {
|
||||
doc, e := docFromHTML(html)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
baseURL := determineBaseURL(siteURL, doc)
|
||||
|
||||
// Use a map to avoid dups
|
||||
links := make(map[string]empty)
|
||||
|
||||
// Add common, hard coded icon paths
|
||||
for _, path := range iconPaths {
|
||||
links[urlFromBase(baseURL, path)] = empty{}
|
||||
}
|
||||
|
||||
// Add icons found in page
|
||||
urls := extractIconTags(doc)
|
||||
for _, u := range urls {
|
||||
absoluteURL, e := absoluteURL(baseURL, u)
|
||||
if e == nil {
|
||||
links[absoluteURL] = empty{}
|
||||
}
|
||||
}
|
||||
|
||||
// Turn unique keys into array
|
||||
var result []string
|
||||
for u := range links {
|
||||
result = append(result, u)
|
||||
}
|
||||
sort.Strings(result)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// What is the baseURL for this doc?
|
||||
func determineBaseURL(siteURL *url.URL, doc *goquery.Document) *url.URL {
|
||||
baseTagHref := extractBaseTag(doc)
|
||||
if baseTagHref != "" {
|
||||
baseTagURL, e := url.Parse(baseTagHref)
|
||||
if e != nil {
|
||||
return siteURL
|
||||
}
|
||||
return baseTagURL
|
||||
}
|
||||
|
||||
return siteURL
|
||||
}
|
||||
|
||||
// Convert bytes => doc
|
||||
func docFromHTML(html []byte) (*goquery.Document, error) {
|
||||
doc, e := goquery.NewDocumentFromReader(bytes.NewReader(html))
|
||||
if e != nil || doc == nil {
|
||||
return nil, errParseHTML
|
||||
}
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
var errParseHTML = errors.New("besticon: could not parse html")
|
||||
|
||||
// Find <head><base href="xxx">
|
||||
func extractBaseTag(doc *goquery.Document) string {
|
||||
href := ""
|
||||
doc.Find("head base[href]").First().Each(func(i int, s *goquery.Selection) {
|
||||
href, _ = s.Attr("href")
|
||||
})
|
||||
return href
|
||||
}
|
||||
|
||||
var (
|
||||
iconTypes = []string{favIcon, appleTouchIcon, appleTouchIconPrecomposed}
|
||||
iconTypesRe = regexp.MustCompile(fmt.Sprintf("^(%s)$", strings.Join(regexpQuoteMetaArray(iconTypes), "|")))
|
||||
)
|
||||
|
||||
// Find icons from doc using goquery
|
||||
func extractIconTags(doc *goquery.Document) []string {
|
||||
var hits []string
|
||||
doc.Find("link[href][rel]").Each(func(i int, s *goquery.Selection) {
|
||||
href := extractIconTag(s)
|
||||
if href != "" {
|
||||
hits = append(hits, href)
|
||||
}
|
||||
})
|
||||
return hits
|
||||
}
|
||||
|
||||
func extractIconTag(s *goquery.Selection) string {
|
||||
// What sort of iconType is in this <rel>?
|
||||
rel, _ := s.Attr("rel")
|
||||
if rel == "" {
|
||||
return ""
|
||||
}
|
||||
rel = strings.ToLower(rel)
|
||||
|
||||
var iconType string
|
||||
for _, i := range strings.Fields(rel) {
|
||||
if iconTypesRe.MatchString(i) {
|
||||
iconType = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if iconType == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
href, _ := s.Attr("href")
|
||||
if href == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return href
|
||||
}
|
||||
|
||||
// regexp.QuoteMeta an array of strings
|
||||
func regexpQuoteMetaArray(a []string) []string {
|
||||
quoted := make([]string, len(a))
|
||||
for i, s := range a {
|
||||
quoted[i] = regexp.QuoteMeta(s)
|
||||
}
|
||||
return quoted
|
||||
}
|
||||
13
vendor/github.com/mat/besticon/besticon/popular_sites.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
package besticon
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PopularSites we might use for examples and testing.
|
||||
var PopularSites []string
|
||||
|
||||
func init() {
|
||||
PopularSites = strings.Split(os.Getenv("POPULAR_SITES"), ",")
|
||||
}
|
||||
50
vendor/github.com/mat/besticon/besticon/size_range.go
generated
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
package besticon
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SizeRange represents the desired icon dimensions
|
||||
type SizeRange struct {
|
||||
Min int
|
||||
Perfect int
|
||||
Max int
|
||||
}
|
||||
|
||||
var errBadSize = errors.New("besticon: bad size")
|
||||
|
||||
// ParseSizeRange parses a string like 60..100..200 into a SizeRange
|
||||
func ParseSizeRange(s string) (*SizeRange, error) {
|
||||
parts := strings.SplitN(s, "..", 3)
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
size, ok := parseSize(parts[0])
|
||||
if !ok {
|
||||
return nil, errBadSize
|
||||
}
|
||||
return &SizeRange{size, size, MaxIconSize}, nil
|
||||
case 3:
|
||||
n1, ok1 := parseSize(parts[0])
|
||||
n2, ok2 := parseSize(parts[1])
|
||||
n3, ok3 := parseSize(parts[2])
|
||||
if !ok1 || !ok2 || !ok3 {
|
||||
return nil, errBadSize
|
||||
}
|
||||
if !((n1 <= n2) && (n2 <= n3)) {
|
||||
return nil, errBadSize
|
||||
}
|
||||
return &SizeRange{n1, n2, n3}, nil
|
||||
}
|
||||
|
||||
return nil, errBadSize
|
||||
}
|
||||
|
||||
func parseSize(s string) (int, bool) {
|
||||
minSize, err := strconv.Atoi(s)
|
||||
if err != nil || minSize < MinIconSize || minSize > MaxIconSize {
|
||||
return -1, false
|
||||
}
|
||||
return minSize, true
|
||||
}
|
||||
35
vendor/github.com/mat/besticon/besticon/sorting.go
generated
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
package besticon
|
||||
|
||||
import "sort"
|
||||
|
||||
func sortIcons(icons []Icon, sizeDescending bool) {
|
||||
// Order after sorting: (width/height, bytes, url)
|
||||
sort.Stable(byURL(icons))
|
||||
sort.Stable(byBytes(icons))
|
||||
|
||||
if sizeDescending {
|
||||
sort.Stable(sort.Reverse(byWidthHeight(icons)))
|
||||
} else {
|
||||
sort.Stable(byWidthHeight(icons))
|
||||
}
|
||||
}
|
||||
|
||||
type byWidthHeight []Icon
|
||||
|
||||
func (a byWidthHeight) Len() int { return len(a) }
|
||||
func (a byWidthHeight) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a byWidthHeight) Less(i, j int) bool {
|
||||
return (a[i].Width < a[j].Width) || (a[i].Height < a[j].Height)
|
||||
}
|
||||
|
||||
type byBytes []Icon
|
||||
|
||||
func (a byBytes) Len() int { return len(a) }
|
||||
func (a byBytes) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a byBytes) Less(i, j int) bool { return (a[i].Bytes < a[j].Bytes) }
|
||||
|
||||
type byURL []Icon
|
||||
|
||||
func (a byURL) Len() int { return len(a) }
|
||||
func (a byURL) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a byURL) Less(i, j int) bool { return (a[i].URL < a[j].URL) }
|
||||
4
vendor/github.com/mat/besticon/besticon/version.go
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
package besticon
|
||||
|
||||
// Version string, same as VERSION, generated my Make
|
||||
const VersionString = "v3.12.0"
|
||||
201
vendor/github.com/mat/besticon/colorfinder/colorfinder.go
generated
vendored
Normal file
@@ -0,0 +1,201 @@
|
||||
package colorfinder
|
||||
|
||||
// colorfinder takes an image and tries to find its main color.
|
||||
// It is a liberal port of
|
||||
// http://pieroxy.net/blog/pages/color-finder/demo.html
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"image/color"
|
||||
|
||||
// Load supported image formats
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
|
||||
_ "github.com/mat/besticon/ico"
|
||||
)
|
||||
|
||||
func main() {
|
||||
arg := os.Args[1]
|
||||
|
||||
var imageReader io.ReadCloser
|
||||
if strings.HasPrefix(arg, "http") {
|
||||
var err error
|
||||
response, err := http.Get(arg)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
imageReader = response.Body
|
||||
} else {
|
||||
var err error
|
||||
fmt.Fprintln(os.Stderr, "Reading "+arg+"...")
|
||||
imageReader, err = os.Open(arg)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
defer imageReader.Close()
|
||||
|
||||
img, _, err := image.Decode(imageReader)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
cf := ColorFinder{}
|
||||
c, err := cf.FindMainColor(img)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("#" + ColorToHex(c))
|
||||
}
|
||||
|
||||
type ColorFinder struct {
|
||||
img image.Image
|
||||
}
|
||||
|
||||
// FindMainColor tries to identify the most important color in the given logo.
|
||||
func (cf *ColorFinder) FindMainColor(img image.Image) (color.RGBA, error) {
|
||||
cf.img = img
|
||||
|
||||
colorMap := cf.buildColorMap()
|
||||
|
||||
sRGB := cf.findMainColor(colorMap, 6, nil)
|
||||
sRGB = cf.findMainColor(colorMap, 4, &sRGB)
|
||||
sRGB = cf.findMainColor(colorMap, 2, &sRGB)
|
||||
sRGB = cf.findMainColor(colorMap, 0, &sRGB)
|
||||
|
||||
return sRGB.rgb, nil
|
||||
}
|
||||
|
||||
const sampleThreshold = 160 * 160
|
||||
|
||||
func (cf *ColorFinder) buildColorMap() *map[color.RGBA]colorStats {
|
||||
colorMap := make(map[color.RGBA]colorStats)
|
||||
bounds := cf.img.Bounds()
|
||||
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
r, g, b, a := cf.img.At(x, y).RGBA()
|
||||
rgb := color.RGBA{}
|
||||
rgb.R = uint8(r >> shiftRGB)
|
||||
rgb.G = uint8(g >> shiftRGB)
|
||||
rgb.B = uint8(b >> shiftRGB)
|
||||
rgb.A = uint8(a >> shiftRGB)
|
||||
|
||||
colrStats, exist := colorMap[rgb]
|
||||
if exist {
|
||||
colrStats.count++
|
||||
} else {
|
||||
colrStats := colorStats{count: 1, weight: weight(&rgb)}
|
||||
if colrStats.weight <= 0 {
|
||||
colrStats.weight = 1e-10
|
||||
}
|
||||
colorMap[rgb] = colrStats
|
||||
}
|
||||
}
|
||||
}
|
||||
return &colorMap
|
||||
}
|
||||
|
||||
// Turns out using this is faster than using
|
||||
// RGBAModel.Convert(img.At(x, y))).(color.RGBA)
|
||||
const shiftRGB = uint8(8)
|
||||
|
||||
func (cf *ColorFinder) findMainColor(colorMap *map[color.RGBA]colorStats, shift uint, targetColor *shiftedRGBA) shiftedRGBA {
|
||||
colorWeights := make(map[shiftedRGBA]float64)
|
||||
|
||||
bounds := cf.img.Bounds()
|
||||
stepLength := stepLength(bounds)
|
||||
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y += stepLength {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x += stepLength {
|
||||
r, g, b, a := cf.img.At(x, y).RGBA()
|
||||
color := color.RGBA{}
|
||||
color.R = uint8(r >> shiftRGB)
|
||||
color.G = uint8(g >> shiftRGB)
|
||||
color.B = uint8(b >> shiftRGB)
|
||||
color.A = uint8(a >> shiftRGB)
|
||||
|
||||
if rgbMatchesTargetColor(targetColor, &color) {
|
||||
increaseColorWeight(&colorWeights, colorMap, &color, shift)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
maxColor := shiftedRGBA{}
|
||||
maxWeight := 0.0
|
||||
for sRGB, weight := range colorWeights {
|
||||
if weight > maxWeight {
|
||||
maxColor = sRGB
|
||||
maxWeight = weight
|
||||
}
|
||||
}
|
||||
|
||||
return maxColor
|
||||
}
|
||||
|
||||
func increaseColorWeight(weightedColors *map[shiftedRGBA]float64, colorMap *map[color.RGBA]colorStats, rgb *color.RGBA, shift uint) {
|
||||
shiftedColor := color.RGBA{R: rgb.R >> shift, G: rgb.G >> shift, B: rgb.B >> shift}
|
||||
pixelGroup := shiftedRGBA{rgb: shiftedColor, shift: shift}
|
||||
colorStats := (*colorMap)[*rgb]
|
||||
(*weightedColors)[pixelGroup] += colorStats.weight * float64(colorStats.count)
|
||||
}
|
||||
|
||||
type shiftedRGBA struct {
|
||||
rgb color.RGBA
|
||||
shift uint
|
||||
}
|
||||
|
||||
func rgbMatchesTargetColor(targetCol *shiftedRGBA, rgb *color.RGBA) bool {
|
||||
if targetCol == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return targetCol.rgb.R == (rgb.R>>targetCol.shift) &&
|
||||
targetCol.rgb.G == (rgb.G>>targetCol.shift) &&
|
||||
targetCol.rgb.B == (rgb.B>>targetCol.shift)
|
||||
}
|
||||
|
||||
type colorStats struct {
|
||||
weight float64
|
||||
count int64
|
||||
}
|
||||
|
||||
func stepLength(bounds image.Rectangle) int {
|
||||
width := bounds.Dx()
|
||||
height := bounds.Dy()
|
||||
pixelCount := width * height
|
||||
|
||||
var stepLength int
|
||||
if pixelCount > sampleThreshold {
|
||||
stepLength = 2
|
||||
} else {
|
||||
stepLength = 1
|
||||
}
|
||||
|
||||
return stepLength
|
||||
}
|
||||
|
||||
func weight(rgb *color.RGBA) float64 {
|
||||
rr := float64(rgb.R)
|
||||
gg := float64(rgb.G)
|
||||
bb := float64(rgb.B)
|
||||
return (abs(rr-gg)*abs(rr-gg)+abs(rr-bb)*abs(rr-bb)+abs(gg-bb)*abs(gg-bb))/65535.0*1000.0 + 1
|
||||
}
|
||||
|
||||
func abs(n float64) float64 {
|
||||
return math.Abs(float64(n))
|
||||
}
|
||||
|
||||
func ColorToHex(c color.RGBA) string {
|
||||
return fmt.Sprintf("%02x%02x%02x", c.R, c.G, c.B)
|
||||
}
|
||||
BIN
vendor/github.com/mat/besticon/ico/addthis.ico
generated
vendored
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
vendor/github.com/mat/besticon/ico/besticon.ico
generated
vendored
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
vendor/github.com/mat/besticon/ico/broken.ico
generated
vendored
Normal file
|
After Width: | Height: | Size: 32 B |
BIN
vendor/github.com/mat/besticon/ico/codeplex.ico
generated
vendored
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
vendor/github.com/mat/besticon/ico/favicon.ico
generated
vendored
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
vendor/github.com/mat/besticon/ico/github.ico
generated
vendored
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
261
vendor/github.com/mat/besticon/ico/ico.go
generated
vendored
Normal file
@@ -0,0 +1,261 @@
|
||||
// Package ico registers image.Decode and DecodeConfig support
|
||||
// for the icon (container) format.
|
||||
package ico
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"image"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"image/png"
|
||||
|
||||
"golang.org/x/image/bmp"
|
||||
)
|
||||
|
||||
type icondir struct {
|
||||
Reserved uint16
|
||||
Type uint16
|
||||
Count uint16
|
||||
Entries []icondirEntry
|
||||
}
|
||||
|
||||
type icondirEntry struct {
|
||||
Width byte
|
||||
Height byte
|
||||
PaletteCount byte
|
||||
Reserved byte
|
||||
ColorPlanes uint16
|
||||
BitsPerPixel uint16
|
||||
Size uint32
|
||||
Offset uint32
|
||||
}
|
||||
|
||||
func (dir *icondir) FindBestIcon() *icondirEntry {
|
||||
if len(dir.Entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
best := dir.Entries[0]
|
||||
for _, e := range dir.Entries {
|
||||
if (e.width() > best.width()) && (e.height() > best.height()) {
|
||||
best = e
|
||||
}
|
||||
}
|
||||
return &best
|
||||
}
|
||||
|
||||
// ParseIco parses the icon and returns meta information for the icons as icondir.
|
||||
func ParseIco(r io.Reader) (*icondir, error) {
|
||||
dir := icondir{}
|
||||
|
||||
var err error
|
||||
err = binary.Read(r, binary.LittleEndian, &dir.Reserved)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = binary.Read(r, binary.LittleEndian, &dir.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = binary.Read(r, binary.LittleEndian, &dir.Count)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := uint16(0); i < dir.Count; i++ {
|
||||
entry := icondirEntry{}
|
||||
e := parseIcondirEntry(r, &entry)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
dir.Entries = append(dir.Entries, entry)
|
||||
}
|
||||
|
||||
return &dir, err
|
||||
}
|
||||
|
||||
func parseIcondirEntry(r io.Reader, e *icondirEntry) error {
|
||||
err := binary.Read(r, binary.LittleEndian, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type dibHeader struct {
|
||||
dibHeaderSize uint32
|
||||
width uint32
|
||||
height uint32
|
||||
}
|
||||
|
||||
func (e *icondirEntry) ColorCount() int {
|
||||
if e.PaletteCount == 0 {
|
||||
return 256
|
||||
}
|
||||
return int(e.PaletteCount)
|
||||
}
|
||||
|
||||
func (e *icondirEntry) width() int {
|
||||
if e.Width == 0 {
|
||||
return 256
|
||||
}
|
||||
return int(e.Width)
|
||||
}
|
||||
|
||||
func (e *icondirEntry) height() int {
|
||||
if e.Height == 0 {
|
||||
return 256
|
||||
}
|
||||
return int(e.Height)
|
||||
}
|
||||
|
||||
// DecodeConfig returns just the dimensions of the largest image
|
||||
// contained in the icon withou decoding the entire icon file.
|
||||
func DecodeConfig(r io.Reader) (image.Config, error) {
|
||||
dir, err := ParseIco(r)
|
||||
if err != nil {
|
||||
return image.Config{}, err
|
||||
}
|
||||
|
||||
best := dir.FindBestIcon()
|
||||
if best == nil {
|
||||
return image.Config{}, errInvalid
|
||||
}
|
||||
return image.Config{Width: best.width(), Height: best.height()}, nil
|
||||
}
|
||||
|
||||
// The bitmap header structure we read from an icondirEntry
|
||||
type bitmapHeaderRead struct {
|
||||
Size uint32
|
||||
Width uint32
|
||||
Height uint32
|
||||
Planes uint16
|
||||
BitCount uint16
|
||||
Compression uint32
|
||||
ImageSize uint32
|
||||
XPixelsPerMeter uint32
|
||||
YPixelsPerMeter uint32
|
||||
ColorsUsed uint32
|
||||
ColorsImportant uint32
|
||||
}
|
||||
|
||||
// The bitmap header structure we need to generate for bmp.Decode()
|
||||
type bitmapHeaderWrite struct {
|
||||
sigBM [2]byte
|
||||
fileSize uint32
|
||||
resverved [2]uint16
|
||||
pixOffset uint32
|
||||
Size uint32
|
||||
Width uint32
|
||||
Height uint32
|
||||
Planes uint16
|
||||
BitCount uint16
|
||||
Compression uint32
|
||||
ImageSize uint32
|
||||
XPixelsPerMeter uint32
|
||||
YPixelsPerMeter uint32
|
||||
ColorsUsed uint32
|
||||
ColorsImportant uint32
|
||||
}
|
||||
|
||||
var errInvalid = errors.New("ico: invalid ICO image")
|
||||
|
||||
// Decode returns the largest image contained in the icon
|
||||
// which might be a bmp or png
|
||||
func Decode(r io.Reader) (image.Image, error) {
|
||||
icoBytes, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r = bytes.NewReader(icoBytes)
|
||||
dir, err := ParseIco(r)
|
||||
if err != nil {
|
||||
return nil, errInvalid
|
||||
}
|
||||
|
||||
best := dir.FindBestIcon()
|
||||
if best == nil {
|
||||
return nil, errInvalid
|
||||
}
|
||||
|
||||
return parseImage(best, icoBytes)
|
||||
}
|
||||
|
||||
func parseImage(entry *icondirEntry, icoBytes []byte) (image.Image, error) {
|
||||
r := bytes.NewReader(icoBytes)
|
||||
r.Seek(int64(entry.Offset), 0)
|
||||
|
||||
// Try PNG first then BMP
|
||||
img, err := png.Decode(r)
|
||||
if err != nil {
|
||||
return parseBMP(entry, icoBytes)
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
|
||||
func parseBMP(entry *icondirEntry, icoBytes []byte) (image.Image, error) {
|
||||
bmpBytes, err := makeFullBMPBytes(entry, icoBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bmp.Decode(bmpBytes)
|
||||
}
|
||||
|
||||
func makeFullBMPBytes(entry *icondirEntry, icoBytes []byte) (*bytes.Buffer, error) {
|
||||
r := bytes.NewReader(icoBytes)
|
||||
r.Seek(int64(entry.Offset), 0)
|
||||
|
||||
var err error
|
||||
h := bitmapHeaderRead{}
|
||||
|
||||
err = binary.Read(r, binary.LittleEndian, &h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if h.Size != 40 || h.Planes != 1 {
|
||||
return nil, errInvalid
|
||||
}
|
||||
|
||||
var pixOffset uint32
|
||||
if h.ColorsUsed == 0 && h.BitCount <= 8 {
|
||||
pixOffset = 14 + 40 + 4*(1<<h.BitCount)
|
||||
} else {
|
||||
pixOffset = 14 + 40 + 4*h.ColorsUsed
|
||||
}
|
||||
|
||||
writeHeader := &bitmapHeaderWrite{
|
||||
sigBM: [2]byte{'B', 'M'},
|
||||
fileSize: 14 + 40 + uint32(len(icoBytes)), // correct? important?
|
||||
pixOffset: pixOffset,
|
||||
Size: 40,
|
||||
Width: uint32(h.Width),
|
||||
Height: uint32(h.Height / 2),
|
||||
Planes: h.Planes,
|
||||
BitCount: h.BitCount,
|
||||
Compression: h.Compression,
|
||||
ColorsUsed: h.ColorsUsed,
|
||||
ColorsImportant: h.ColorsImportant,
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err = binary.Write(buf, binary.LittleEndian, writeHeader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
io.CopyN(buf, r, int64(entry.Size))
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
const icoHeader = "\x00\x00\x01\x00"
|
||||
|
||||
func init() {
|
||||
image.RegisterFormat("ico", icoHeader, Decode, DecodeConfig)
|
||||
}
|
||||
BIN
vendor/github.com/mat/besticon/ico/wowhead.ico
generated
vendored
Normal file
|
After Width: | Height: | Size: 2.5 KiB |