aboutsummaryrefslogtreecommitdiffstats
path: root/internal/notify/image.go
blob: 9dd750e14e07c13f898f584612608e4b6fd2cc38 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// 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 spec's top-priority raw image key, image-data,
// or the underscore alias older libnotify sent.
func ImageDataFromHints(hints map[string]dbus.Variant) (*RawImage, bool) {
	for _, key := range []string{"image-data", "image_data"} {
		v, ok := hints[key]
		if !ok {
			continue
		}
		if r, ok := rawImageFromVariant(v); ok {
			return r, true
		}
	}
	return nil, false
}

// IconDataFromHints reads the deprecated icon_data raw image, the lowest
// priority source, below image-data and the image-path URI.
func IconDataFromHints(hints map[string]dbus.Variant) (*RawImage, bool) {
	v, ok := hints["icon_data"]
	if !ok {
		return nil, false
	}
	return rawImageFromVariant(v)
}

// 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
}

// maxImageDimension caps a client-supplied width or height. A notification
// icon is small, and the cap keeps every later width*channels and y*stride
// product far from overflow.
const maxImageDimension = 1 << 16

// 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.Width > maxImageDimension || r.Height > maxImageDimension {
		return nil, fmt.Errorf("notifyd: image %dx%d larger than %d", r.Width, r.Height, maxImageDimension)
	}
	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
	}
	// int64 keeps a hostile RowStride from wrapping the length comparison
	// negative and passing the guard.
	need := int64(stride)*int64(r.Height-1) + int64(r.Width)*int64(r.Channels)
	if int64(len(r.Data)) < need {
		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
}