// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. package notify import ( "bytes" "fmt" "image" "image/color" "image/png" "github.com/godbus/dbus/v5" ) // RawImage is the spec's image-data structure (iiibiiay). Data is RGB byte // order: 4 bytes per pixel with alpha, 3 without, and rows are RowStride // bytes apart, which may exceed Width*Channels. type RawImage struct { Width int Height int RowStride int HasAlpha bool BitsPerSample int Channels int Data []byte } // ImageDataFromHints reads the raw image struct, preferring the spec key then // the deprecated icon_data, then the underscore alias older libnotify sent. func ImageDataFromHints(hints map[string]dbus.Variant) (*RawImage, bool) { for _, key := range []string{"image-data", "icon_data", "image_data"} { v, ok := hints[key] if !ok { continue } if r, ok := rawImageFromVariant(v); ok { return r, true } } return nil, false } // ImagePathFromHints reads image-path, a URI, a path, or a theme icon name. func ImagePathFromHints(hints map[string]dbus.Variant) (string, bool) { v, ok := hints["image-path"] if !ok { return "", false } s, ok := v.Value().(string) if !ok || s == "" { return "", false } return s, true } // rawImageFromVariant accepts the []interface{} godbus yields for a struct. // Each numeric field may arrive as int32 or int depending on the encoder. func rawImageFromVariant(v dbus.Variant) (*RawImage, bool) { f, ok := v.Value().([]interface{}) if !ok || len(f) != 7 { return nil, false } r := &RawImage{} var okW, okH, okS, okC, okD bool r.Width, okW = asInt(f[0]) r.Height, okH = asInt(f[1]) r.RowStride, okS = asInt(f[2]) r.HasAlpha, _ = f[3].(bool) r.BitsPerSample, _ = asInt(f[4]) r.Channels, okC = asInt(f[5]) r.Data, okD = f[6].([]byte) if !okW || !okH || !okS || !okC || !okD { return nil, false } return r, true } func asInt(v any) (int, bool) { switch n := v.(type) { case int: return n, true case int32: return int(n), true case int64: return int(n), true case uint32: return int(n), true } return 0, false } // PNG encodes the raw pixels as a PNG the renderer can load. func (r *RawImage) PNG() ([]byte, error) { if r.Width <= 0 || r.Height <= 0 { return nil, fmt.Errorf("notifyd: image %dx%d", r.Width, r.Height) } if r.BitsPerSample != 8 || (r.Channels != 3 && r.Channels != 4) { return nil, fmt.Errorf("notifyd: image bits=%d channels=%d", r.BitsPerSample, r.Channels) } stride := r.RowStride if stride < r.Width*r.Channels { stride = r.Width * r.Channels } if len(r.Data) < stride*(r.Height-1)+r.Width*r.Channels { return nil, fmt.Errorf("notifyd: image data short: %d bytes", len(r.Data)) } img := image.NewRGBA(image.Rect(0, 0, r.Width, r.Height)) for y := 0; y < r.Height; y++ { row := r.Data[y*stride:] for x := 0; x < r.Width; x++ { if r.Channels == 4 { i := x * 4 img.SetRGBA(x, y, color.RGBA{row[i], row[i+1], row[i+2], row[i+3]}) } else { i := x * 3 img.SetRGBA(x, y, color.RGBA{row[i], row[i+1], row[i+2], 255}) } } } var buf bytes.Buffer if err := png.Encode(&buf, img); err != nil { return nil, err } return buf.Bytes(), nil }