249 lines
7.2 KiB
Go
249 lines
7.2 KiB
Go
package xgraphics
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"image/draw"
|
|
"math"
|
|
|
|
"github.com/BurntSushi/graphics-go/graphics"
|
|
"github.com/jezek/xgb/xproto"
|
|
|
|
"github.com/jezek/xgbutil"
|
|
"github.com/jezek/xgbutil/ewmh"
|
|
"github.com/jezek/xgbutil/icccm"
|
|
)
|
|
|
|
/*
|
|
xgraphics/util.go contains a variety of image manipulation functions that
|
|
are not specific to xgraphics.Image.
|
|
*/
|
|
|
|
// Scale is a simple wrapper around graphics.Scale.
|
|
func Scale(img image.Image, width, height int) draw.Image {
|
|
dimg := image.NewRGBA(image.Rect(0, 0, width, height))
|
|
graphics.Scale(dimg, img)
|
|
|
|
return dimg
|
|
}
|
|
|
|
// Alpha will modify the alpha channel of the image such that:
|
|
// existingAlpha = existingAlpha * (givenAlpha / 100.0)
|
|
func Alpha(dest *Image, alpha int) {
|
|
r := dest.Bounds()
|
|
|
|
var a, x, y, i int
|
|
for x = r.Min.X; x < r.Max.X; x++ {
|
|
for y = r.Min.Y; y < r.Max.Y; y++ {
|
|
i = dest.PixOffset(x, y)
|
|
a = int(dest.Pix[i+3])
|
|
dest.Pix[i+3] = uint8((a * alpha) / 100)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Blend alpha blends the src image (starting at the spt Point) into the
|
|
// dest image.
|
|
// If you're blending into a solid background color, use BlendBgColor
|
|
// instead. (It's more efficient.)
|
|
// Blend does not (currently) blend with the destination's alpha channel,
|
|
// only the source's alpha channel.
|
|
func Blend(dest *Image, src image.Image, sp image.Point) {
|
|
rsrc, dsrc := src.Bounds(), dest.Bounds()
|
|
_, smxx, _, smxy := rsrc.Min.X, rsrc.Max.X, rsrc.Min.Y, rsrc.Max.Y
|
|
dmnx, dmxx, dmny, dmxy := dsrc.Min.X, dsrc.Max.X, dsrc.Min.Y, dsrc.Max.Y
|
|
|
|
var sx, dx, sy, dy int
|
|
var sr, sg, sb, sa uint32
|
|
var bgra BGRA
|
|
var alpha float64
|
|
for sx, dx = sp.X, dmnx; sx < smxx && dx < dmxx; sx, dx = sx+1, dx+1 {
|
|
for sy, dy = sp.Y, dmny; sy < smxy && dy < dmxy; sy, dy = sy+1, dy+1 {
|
|
sr, sg, sb, sa = src.At(sx, sy).RGBA()
|
|
bgra = dest.At(dx, dy).(BGRA)
|
|
alpha = float64(uint8(sa)) / 255.0
|
|
|
|
dest.SetBGRA(dx, dy, BGRA{
|
|
blend(uint8(bgra.B), uint8(sb), alpha),
|
|
blend(uint8(bgra.G), uint8(sg), alpha),
|
|
blend(uint8(bgra.R), uint8(sr), alpha),
|
|
0xff,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// BlendBgColor blends the Image (receiver) into the background color
|
|
// specified. This is more efficient than creating a background image and
|
|
// blending with Blend.
|
|
func BlendBgColor(dest *Image, c color.Color) {
|
|
r := dest.Bounds()
|
|
cr32, cg32, cb32, _ := c.RGBA()
|
|
cr, cg, cb := uint8(cr32), uint8(cg32), uint8(cb32)
|
|
|
|
var bgra BGRA
|
|
var alpha float64
|
|
for x := r.Min.X; x < r.Max.X; x++ {
|
|
for y := r.Min.Y; y < r.Max.Y; y++ {
|
|
bgra = dest.At(x, y).(BGRA)
|
|
alpha = float64(bgra.A) / 255.0
|
|
dest.SetBGRA(x, y, BGRA{
|
|
B: blend(cb, bgra.B, alpha),
|
|
G: blend(cg, bgra.G, alpha),
|
|
R: blend(cr, bgra.R, alpha),
|
|
A: 0xff,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Blend returns the blended alpha color for src and dest colors.
|
|
// This assumes that the destination has alpha = 1.
|
|
func BlendBGRA(dest, src BGRA) BGRA {
|
|
alpha := float64(src.A) / 255.0
|
|
return BGRA{
|
|
B: blend(dest.B, src.B, alpha),
|
|
G: blend(dest.G, src.G, alpha),
|
|
R: blend(dest.R, src.R, alpha),
|
|
A: 0xff,
|
|
}
|
|
}
|
|
|
|
// blend calculates the value of a color given some alpha value in [0, 1]
|
|
// and a source and destination color. Note that this assumes that the
|
|
// destination is fully opaque (has an alpha value of 1).
|
|
func blend(d, s uint8, alpha float64) uint8 {
|
|
return uint8(float64(s)*alpha + float64(d)*(1-alpha))
|
|
}
|
|
|
|
// FreePixmap is a convenience function for destroying a pixmap resource
|
|
// on the X server.
|
|
// If you're using an xgraphics.Image value, then its Destroy method will call
|
|
// this for you.
|
|
func FreePixmap(X *xgbutil.XUtil, pixid xproto.Pixmap) {
|
|
xproto.FreePixmap(X.Conn(), pixid)
|
|
}
|
|
|
|
// FindIcon takes a window id and attempts to return an xgraphics.Image of
|
|
// that window's icon.
|
|
// It will first try to look for an icon in _NET_WM_ICON that is closest to
|
|
// the size specified.
|
|
// If there are no icons in _NET_WM_ICON, then WM_HINTS will be checked for
|
|
// an icon.
|
|
// If an icon is found from either one and doesn't match the size
|
|
// specified, it will be scaled to that size.
|
|
// If the width and height are 0, then the largest icon will be returned with
|
|
// no scaling.
|
|
// If an icon is not found, an error is returned.
|
|
func FindIcon(X *xgbutil.XUtil, wid xproto.Window,
|
|
width, height int) (*Image, error) {
|
|
|
|
var ewmhErr, icccmErr error
|
|
|
|
// First try to get a EWMH style icon.
|
|
icon, ewmhErr := findIconEwmh(X, wid, width, height)
|
|
if ewmhErr != nil { // now look for an icccm-style icon
|
|
icon, icccmErr = findIconIcccm(X, wid)
|
|
if icccmErr != nil {
|
|
return nil, fmt.Errorf("Neither a EWMH-style or ICCCM-style icon "+
|
|
"could be found for window id %x because: %s *AND* %s",
|
|
wid, ewmhErr, icccmErr)
|
|
}
|
|
}
|
|
|
|
// We should have a valid xgraphics.Image if we're here.
|
|
// If the size doesn't match what's preferred, scale it.
|
|
if width != 0 && height != 0 {
|
|
if icon.Bounds().Dx() != width || icon.Bounds().Dy() != height {
|
|
icon = icon.Scale(width, height)
|
|
}
|
|
}
|
|
return icon, nil
|
|
}
|
|
|
|
// findIconEwmh helps FindIcon by trying to return an ewmh-style icon that is
|
|
// closest to the preferred size specified.
|
|
func findIconEwmh(X *xgbutil.XUtil, wid xproto.Window,
|
|
width, height int) (*Image, error) {
|
|
|
|
icons, err := ewmh.WmIconGet(X, wid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
icon := FindBestEwmhIcon(width, height, icons)
|
|
if icon == nil {
|
|
return nil, fmt.Errorf("Could not find any _NET_WM_ICON icon.")
|
|
}
|
|
|
|
return NewEwmhIcon(X, icon), nil
|
|
}
|
|
|
|
// findIconIcccm helps FindIcon by trying to return an icccm-style icon.
|
|
func findIconIcccm(X *xgbutil.XUtil, wid xproto.Window) (*Image, error) {
|
|
hints, err := icccm.WmHintsGet(X, wid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Only continue if the WM_HINTS flags say an icon is specified and
|
|
// if at least one of icon pixmap or icon mask is non-zero.
|
|
if hints.Flags&icccm.HintIconPixmap == 0 ||
|
|
(hints.IconPixmap == 0 && hints.IconMask == 0) {
|
|
|
|
return nil, fmt.Errorf("No icon found in WM_HINTS.")
|
|
}
|
|
|
|
return NewIcccmIcon(X, hints.IconPixmap, hints.IconMask)
|
|
}
|
|
|
|
// FindBestEwmhIcon takes width/height dimensions and a slice of *ewmh.WmIcon
|
|
// and finds the best matching icon of the bunch. We always prefer bigger.
|
|
// If no icons are bigger than the preferred dimensions, use the biggest
|
|
// available. Otherwise, use the smallest icon that is greater than or equal
|
|
// to the preferred dimensions. The preferred dimensions is essentially
|
|
// what you'll likely scale the resulting icon to.
|
|
// If width and height are 0, then the largest icon found will be returned.
|
|
func FindBestEwmhIcon(width, height int, icons []ewmh.WmIcon) *ewmh.WmIcon {
|
|
// nada nada limonada
|
|
if len(icons) == 0 {
|
|
return nil
|
|
}
|
|
|
|
parea := width * height // preferred size
|
|
best := -1
|
|
|
|
// If zero area, set it to the largest possible.
|
|
if parea == 0 {
|
|
parea = math.MaxInt32
|
|
}
|
|
|
|
var bestArea, iconArea int
|
|
|
|
for i, icon := range icons {
|
|
// the first valid icon we've seen; use it!
|
|
if best == -1 {
|
|
best = i
|
|
continue
|
|
}
|
|
|
|
// load areas for comparison
|
|
bestArea = int(icons[best].Width * icons[best].Height)
|
|
iconArea = int(icon.Width * icon.Height)
|
|
|
|
// We don't always want to accept bigger icons if our best is
|
|
// already bigger. But we always want something bigger if our best
|
|
// is insufficient.
|
|
if (iconArea >= parea && iconArea <= bestArea) ||
|
|
(bestArea < parea && iconArea > bestArea) {
|
|
best = i
|
|
}
|
|
}
|
|
|
|
if best > -1 {
|
|
return &icons[best]
|
|
}
|
|
return nil
|
|
}
|