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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235 | class PillowEngine(DrawEngine):
"""基于 Pillow + numpy 的进程内栅格引擎。"""
name = "pillow"
kind = "raster"
requires = ("PIL", "numpy")
description = "Pillow 超采样栅格化(无额外依赖,永远可用)"
def __init__(self, supersample: "int | None" = None) -> None:
#: 固定超采样倍率;``None`` 表示按面积自适应
self.supersample = supersample
# ------------------------------------------------------------------
def _scale_for(self, width: int, height: int) -> int:
if self.supersample is not None:
return max(1, int(self.supersample))
return _supersample_for(width, height)
@staticmethod
def _gradient_column(height, y0, y1, c1, c2, stop1, stop2):
"""生成 ``(height, 4)`` 的竖直渐变列,stop 之外按 SVG 语义外推。"""
import numpy as np
ys = np.arange(height, dtype=np.float64)
span = (y1 - y0) or 1.0
t = (ys - y0) / span
stop_span = (stop2 - stop1) or 1.0
u = np.clip((t - stop1) / stop_span, 0.0, 1.0)
a = np.asarray(c1, dtype=np.float64)
b = np.asarray(c2, dtype=np.float64)
return a[None, :] + (b - a)[None, :] * u[:, None]
def _paint_gradient(self, size, mask, column):
"""用渐变列 + 灰度遮罩生成一层 RGBA 图像。"""
import numpy as np
from PIL import Image
width, height = size
rgba = np.repeat(column[:, None, :], width, axis=1)
alpha = rgba[:, :, 3] * (np.asarray(mask, dtype=np.float64) / 255.0)
rgba[:, :, 3] = alpha
return Image.fromarray(np.clip(rgba, 0, 255).astype(np.uint8), "RGBA")
# ------------------------------------------------------------------
# 圆角矩形
# ------------------------------------------------------------------
def render_roundrect(self, spec: RoundRectSpec):
from PIL import Image, ImageDraw
width = max(1, int(round(spec.width)))
height = max(1, int(round(spec.height)))
s = self._scale_for(width, height)
W, H = width * s, height * s
base = Image.new("RGBA", (W, H), (0, 0, 0, 0))
stroke_width = float(spec.outline_width or 0.0)
inset = stroke_width / 2.0 if stroke_width > 0 else 0.0
rx = max(0.0, float(spec.rx) - inset)
# 注意:Pillow 的 rounded_rectangle 只接受**一个**圆角半径,所以
# spec.effective_ry(y 方向半径)在这里用不上——这是引擎能力差异,
# 不是漏算;需要椭圆圆角请用 skia / cairo / SVG 引擎。
box = (
inset * s,
inset * s,
W - 1 - inset * s,
H - 1 - inset * s,
)
if box[2] <= box[0]:
box = (box[0], box[1], box[0] + 1, box[3])
if box[3] <= box[1]:
box = (box[0], box[1], box[2], box[1] + 1)
radius = min(rx * s, (box[2] - box[0]) / 2.0, (box[3] - box[1]) / 2.0)
# -- 填充 --
fill = parse_color(spec.fill, spec.fill_opacity)
if fill:
layer = Image.new("RGBA", (W, H), (0, 0, 0, 0))
ImageDraw.Draw(layer).rounded_rectangle(
box, radius=radius, fill=fill
)
base = Image.alpha_composite(base, layer)
# -- 描边 --
if stroke_width > 0:
outline = parse_color(spec.outline, spec.outline_opacity)
outline2 = (
parse_color(spec.outline2, spec.outline2_opacity)
if spec.outline2
else None
)
line_width = max(1, int(round(stroke_width * s)))
if outline2 and outline2 != outline:
mask = Image.new("L", (W, H), 0)
ImageDraw.Draw(mask).rounded_rectangle(
box, radius=radius, outline=255, width=line_width
)
column = self._gradient_column(
H, inset * s, H - inset * s, outline, outline2,
spec.gradient_stop1, spec.gradient_stop2,
)
base = Image.alpha_composite(
base, self._paint_gradient((W, H), mask, column)
)
elif outline:
layer = Image.new("RGBA", (W, H), (0, 0, 0, 0))
ImageDraw.Draw(layer).rounded_rectangle(
box, radius=radius, outline=outline, width=line_width
)
base = Image.alpha_composite(base, layer)
if s != 1:
base = base.resize((width, height), Image.LANCZOS)
return base
# ------------------------------------------------------------------
# 滑块进度条槽
# ------------------------------------------------------------------
def render_track(self, spec: TrackSpec):
from PIL import Image, ImageDraw
width = max(1, int(round(spec.width)))
height = max(1, int(round(spec.height)))
s = self._scale_for(width, height)
W, H = width * s, height * s
base = Image.new("RGBA", (W, H), (0, 0, 0, 0))
left = max(0.0, min(float(spec.width2), float(width))) * s
radius = max(0.0, float(spec.radius) * s)
def _block(x0, x1, color):
nonlocal base
if x1 <= x0 or color is None:
return
layer = Image.new("RGBA", (W, H), (0, 0, 0, 0))
r = min(radius, (x1 - x0) / 2.0, H / 2.0)
ImageDraw.Draw(layer).rounded_rectangle(
(x0, 0, x1 - 1, H - 1), radius=r, fill=color
)
base = Image.alpha_composite(base, layer)
_block(0.0, left, parse_color(spec.track_fill, spec.track_opacity))
_block(left, float(W), parse_color(spec.rail_fill, spec.rail_opacity))
if s != 1:
base = base.resize((width, height), Image.LANCZOS)
return base
# ------------------------------------------------------------------
# 滑块把手
# ------------------------------------------------------------------
def render_thumb(self, spec: ThumbSpec):
from PIL import Image, ImageDraw
width = max(1, int(round(spec.width)))
height = max(1, int(round(spec.height)))
s = self._scale_for(width, height)
W, H = width * s, height * s
base = Image.new("RGBA", (W, H), (0, 0, 0, 0))
r1 = float(spec.r1) * s
r2 = float(spec.r2) * s
cx, cy = W / 2.0, H / 2.0
def _circle(radius, color):
nonlocal base
if color is None or radius <= 0:
return
layer = Image.new("RGBA", (W, H), (0, 0, 0, 0))
ImageDraw.Draw(layer).ellipse(
(cx - radius, cy - radius, cx + radius, cy + radius), fill=color
)
base = Image.alpha_composite(base, layer)
# 外圈伪阴影:竖直渐变
outline = parse_color(spec.outline, spec.outline_opacity)
outline2 = (
parse_color(spec.outline2, spec.outline2_opacity)
if spec.outline2
else None
)
if outline:
if outline2 and outline2 != outline:
mask = Image.new("L", (W, H), 0)
ImageDraw.Draw(mask).ellipse(
(cx - r1, cy - r1, cx + r1, cy + r1), fill=255
)
column = self._gradient_column(
H, 1.0 * s, (float(spec.r1) * 2.0 - 1.0) * s,
outline, outline2, 0.500208, 0.954545,
)
base = Image.alpha_composite(
base, self._paint_gradient((W, H), mask, column)
)
else:
_circle(r1, outline)
_circle(r1 - s, parse_color(spec.fill, spec.fill_opacity))
_circle(r2, parse_color(spec.inner_fill, spec.inner_fill_opacity))
if s != 1:
base = base.resize((width, height), Image.LANCZOS)
return base
|