aboutsummaryrefslogtreecommitdiffstats
path: root/internal/notify/image.go
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-15 17:49:34 +0200
committerDanilo M. <danix@danix.xyz>2026-09-15 17:49:34 +0200
commit36208161e177d277a22e32fb21295df37cd8d218 (patch)
tree80d00584b6ce70358fe39ec8538a67d6bc3fca25 /internal/notify/image.go
parent739d98262ea2a14d4cd4ca08a17dc91bce267d0e (diff)
downloadnotifyd-36208161e177d277a22e32fb21295df37cd8d218.tar.gz
notifyd-36208161e177d277a22e32fb21295df37cd8d218.zip
feat(notify): parse the image hints and encode them to PNG
image-data (and the deprecated icon_data) is the (iiibiiay) struct; the PNG encoder honours rowstride and both 3- and 4-channel data. The path and data readers are pure, so the service can apply the spec's priority and the store stays free of image handling.
Diffstat (limited to 'internal/notify/image.go')
-rw-r--r--internal/notify/image.go134
1 files changed, 134 insertions, 0 deletions
diff --git a/internal/notify/image.go b/internal/notify/image.go
new file mode 100644
index 0000000..4a0f6df
--- /dev/null
+++ b/internal/notify/image.go
@@ -0,0 +1,134 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// 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
+}