GHIElectronics.Endpoint.Drawing 0.5.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,609 @@
1
+ """GHIElectronics.Endpoint.Drawing, off-screen 2D drawing primitives.
2
+ API mirrors the .NET package character-for-character (PascalCase).
3
+
4
+ from GHIElectronics.Endpoint.Drawing import Canvas, Color, PixelFormat
5
+
6
+ canvas = Canvas.Create(240, 320, PixelFormat.Rgb565)
7
+ canvas.Clear(Color.Black)
8
+ canvas.DrawLine(10, 10, 230, 310, Color.Red, 2)
9
+ canvas.FillCircle(120, 160, 40, Color.Blue)
10
+ spi.Write(canvas.Buffer)
11
+
12
+ The Canvas drawing primitives run in pure Python against an in-memory
13
+ bytearray; Image / Font delegate to the stb_image + stb_truetype decoders
14
+ inside libendpoint. Hand `Canvas.Buffer` to any device that accepts raw
15
+ bytes (DisplayController.Flush, SpiDevice.Write, I2cDevice.Write, ...)."""
16
+
17
+ import ctypes
18
+ import math
19
+
20
+ from GHIElectronics.Endpoint.Core._native import native
21
+ from GHIElectronics.Endpoint.Core._errors import (
22
+ ArgumentException, ArgumentOutOfRangeException,
23
+ NotSupportedException, NotImplementedException, ObjectDisposedException,
24
+ )
25
+
26
+
27
+ __all__ = ["Canvas", "Color", "Font", "Image", "PixelFormat"]
28
+
29
+
30
+ # ------------------------------------------------------------------
31
+ # PixelFormat
32
+ # ------------------------------------------------------------------
33
+
34
+ class PixelFormat:
35
+ """Pixel layout of a Canvas buffer. Values mirror the .NET enum."""
36
+
37
+ Monochrome = 1 # 1 bpp, MSB-first packed (SSD1306 / SH1106 OLEDs)
38
+ Rgb565 = 2 # 16 bpp little-endian (on-board panel, most SPI TFTs)
39
+ Rgb888 = 3 # 24 bpp R-G-B byte order (parallel RGB panels @ 24 bpp)
40
+ Argb8888 = 4 # 32 bpp A-R-G-B byte order (off-screen compositing)
41
+
42
+
43
+ # ------------------------------------------------------------------
44
+ # Color
45
+ # ------------------------------------------------------------------
46
+
47
+ class Color:
48
+ """A 32-bit color with 8 bits each of alpha, red, green, blue. Canvas
49
+ maps it to its PixelFormat at draw time."""
50
+
51
+ __slots__ = ("R", "G", "B", "A")
52
+
53
+ def __init__(self, r, g, b, a=0xFF):
54
+ self.R = int(r) & 0xFF
55
+ self.G = int(g) & 0xFF
56
+ self.B = int(b) & 0xFF
57
+ self.A = int(a) & 0xFF
58
+
59
+ @staticmethod
60
+ def FromArgb(argb):
61
+ """Color from a 0xAARRGGBB value."""
62
+ argb = int(argb)
63
+ return Color((argb >> 16) & 0xFF, (argb >> 8) & 0xFF, argb & 0xFF, (argb >> 24) & 0xFF)
64
+
65
+ @staticmethod
66
+ def FromRgb(rgb):
67
+ """Color from a 0xRRGGBB value, opaque."""
68
+ rgb = int(rgb)
69
+ return Color((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF)
70
+
71
+
72
+ # Common colors — names match System.Drawing / CSS where they overlap.
73
+ # Attached after the class body to sidestep the forward-reference problem.
74
+ Color.Transparent = Color(0, 0, 0, 0)
75
+ Color.Black = Color(0x00, 0x00, 0x00)
76
+ Color.White = Color(0xFF, 0xFF, 0xFF)
77
+ Color.Red = Color(0xFF, 0x00, 0x00)
78
+ Color.Green = Color(0x00, 0xFF, 0x00)
79
+ Color.Blue = Color(0x00, 0x00, 0xFF)
80
+ Color.Yellow = Color(0xFF, 0xFF, 0x00)
81
+ Color.Cyan = Color(0x00, 0xFF, 0xFF)
82
+ Color.Magenta = Color(0xFF, 0x00, 0xFF)
83
+ Color.Navy = Color(0x00, 0x00, 0x80)
84
+ Color.Maroon = Color(0x80, 0x00, 0x00)
85
+ Color.Olive = Color(0x80, 0x80, 0x00)
86
+ Color.Gray = Color(0x80, 0x80, 0x80)
87
+ Color.Silver = Color(0xC0, 0xC0, 0xC0)
88
+ Color.Orange = Color(0xFF, 0xA5, 0x00)
89
+
90
+
91
+ # ------------------------------------------------------------------
92
+ # Image
93
+ # ------------------------------------------------------------------
94
+
95
+ class Image:
96
+ """A decoded image (JPEG / PNG / BMP / GIF / TGA — detected from the
97
+ file's magic bytes), ready to blit into a Canvas. Decode runs once in
98
+ Image.Load; Canvas.DrawImage then just blits + converts."""
99
+
100
+ def __init__(self, width, height, pixel_format, pixels):
101
+ self.Width = width
102
+ self.Height = height
103
+ self.PixelFormat = pixel_format
104
+ # Internal RGB888 pixel bytes — read by Canvas.DrawImage / Widget.
105
+ self._pixels = pixels
106
+
107
+ @staticmethod
108
+ def Load(file_bytes):
109
+ """Decode an image file into an in-memory bitmap. The returned Image
110
+ is the format-neutral handle — pass it to Canvas.DrawImage / Widget
111
+ .SetBackgroundImage."""
112
+ if file_bytes is None:
113
+ raise ArgumentException("file_bytes must not be None")
114
+ if len(file_bytes) == 0:
115
+ raise ArgumentException("Image file bytes are empty.")
116
+
117
+ data = bytes(file_bytes)
118
+ src = (ctypes.c_uint8 * len(data)).from_buffer_copy(data)
119
+ out_pixels = ctypes.POINTER(ctypes.c_uint8)()
120
+ out_w = ctypes.c_int()
121
+ out_h = ctypes.c_int()
122
+ native.image_decode(src, len(data),
123
+ ctypes.byref(out_pixels), ctypes.byref(out_w), ctypes.byref(out_h))
124
+ try:
125
+ byte_count = out_w.value * out_h.value * 3
126
+ # Copy pixels out of native storage into a managed bytes object so
127
+ # the native allocation can be freed immediately.
128
+ managed = bytes(ctypes.string_at(out_pixels, byte_count))
129
+ return Image(out_w.value, out_h.value, PixelFormat.Rgb888, managed)
130
+ finally:
131
+ native.image_free(out_pixels)
132
+
133
+
134
+ # ------------------------------------------------------------------
135
+ # Font
136
+ # ------------------------------------------------------------------
137
+
138
+ class Font:
139
+ """A TrueType / OpenType font for Canvas.DrawText. Rasterizes glyphs
140
+ on demand through stb_truetype inside libendpoint. Separate from
141
+ GHIElectronics.Endpoint.UI.Font (which is LVGL's lv_tiny_ttf for
142
+ widget text)."""
143
+
144
+ def __init__(self, handle):
145
+ self._handle = handle
146
+
147
+ @staticmethod
148
+ def Load(ttf_bytes):
149
+ """Load a TTF / OTF font from raw file bytes. The bytes are copied
150
+ into native storage, so the caller's buffer can be released as soon
151
+ as this returns."""
152
+ if ttf_bytes is None:
153
+ raise ArgumentException("ttf_bytes must not be None")
154
+ if len(ttf_bytes) == 0:
155
+ raise ArgumentException("Font bytes are empty.")
156
+
157
+ data = bytes(ttf_bytes)
158
+ src = (ctypes.c_uint8 * len(data)).from_buffer_copy(data)
159
+ out_handle = native.out_handle()
160
+ native.font_init(src, len(data), native.ref(out_handle))
161
+ return Font(out_handle.value)
162
+
163
+ def GetMetrics(self, size_px):
164
+ """Font-wide vertical metrics at the given pixel height. Returns
165
+ (ascent, descent, line_gap). line_height = ascent - descent + line_gap."""
166
+ if self._handle is None:
167
+ raise ObjectDisposedException("The Font is disposed")
168
+ a, d, g = native.out_int(), native.out_int(), native.out_int()
169
+ native.font_metrics(self._handle, int(size_px), native.ref(a), native.ref(d), native.ref(g))
170
+ return a.value, d.value, g.value
171
+
172
+ def Dispose(self):
173
+ if self._handle is not None:
174
+ native.font_free(self._handle)
175
+ self._handle = None
176
+
177
+ def __del__(self):
178
+ try: self.Dispose()
179
+ except Exception: pass
180
+
181
+
182
+ # ------------------------------------------------------------------
183
+ # Canvas
184
+ # ------------------------------------------------------------------
185
+
186
+ def _rgb565(c):
187
+ # Pack an sRGB Color into 5-6-5 little-endian (matches /dev/fb0 on
188
+ # little-endian ARM; SPI TFTs expecting MSB-first need a byte swap on write).
189
+ return ((c.R & 0xF8) << 8) | ((c.G & 0xFC) << 3) | (c.B >> 3)
190
+
191
+
192
+ def _byte_count_for(width, height, fmt):
193
+ if fmt == PixelFormat.Monochrome: return ((width + 7) // 8) * height
194
+ if fmt == PixelFormat.Rgb565: return width * height * 2
195
+ if fmt == PixelFormat.Rgb888: return width * height * 3
196
+ if fmt == PixelFormat.Argb8888: return width * height * 4
197
+ raise ArgumentOutOfRangeException(f"Unsupported pixel format: {fmt}")
198
+
199
+
200
+ class Canvas:
201
+ """A pixel buffer plus the primitives to draw into it. Pick the size
202
+ and PixelFormat at Create() time; when done, hand Buffer to whichever
203
+ device accepts bytes - DisplayController.Flush, SpiDevice.Write,
204
+ I2cDevice.Write, a file, anywhere.
205
+
206
+ Example::
207
+
208
+ canvas = Canvas.Create(240, 320, PixelFormat.Rgb565)
209
+ canvas.Clear(Color.Black)
210
+ canvas.DrawLine(10, 10, 230, 310, Color.Red, 2)
211
+ canvas.FillCircle(120, 160, 40, Color.Blue)
212
+ canvas.DrawText("Hello Endpoint", font, 10, 30, 20, Color.White)
213
+ spi.Write(canvas.Buffer)
214
+ """
215
+
216
+ def __init__(self, width, height, pixel_format):
217
+ if width <= 0 or height <= 0:
218
+ raise ArgumentOutOfRangeException("Width and height must be positive.")
219
+ self.Width = int(width)
220
+ self.Height = int(height)
221
+ self.PixelFormat = pixel_format
222
+ self._buffer = bytearray(_byte_count_for(self.Width, self.Height, pixel_format))
223
+ self._disposed = False
224
+
225
+ @staticmethod
226
+ def Create(width, height, pixel_format):
227
+ """Allocate a new Canvas of the given size and pixel format."""
228
+ return Canvas(width, height, pixel_format)
229
+
230
+ @property
231
+ def Buffer(self):
232
+ """Raw pixel bytes. Length and layout match Width/Height/PixelFormat."""
233
+ if self._disposed:
234
+ raise ObjectDisposedException("The Canvas is disposed")
235
+ return self._buffer
236
+
237
+ # ---------- draw primitives (managed) ----------------------------------
238
+
239
+ def Clear(self, color):
240
+ """Fill the whole buffer with a single color."""
241
+ if self._disposed:
242
+ raise ObjectDisposedException("The Canvas is disposed")
243
+ fmt = self.PixelFormat
244
+ buf = self._buffer
245
+ # Slice-assignment hands the fill to the C bytearray slot: ~100x faster
246
+ # than a per-byte Python loop on a Cortex-A7 at 800x480.
247
+ if fmt == PixelFormat.Monochrome:
248
+ fill = 0xFF if (color.R | color.G | color.B) != 0 else 0x00
249
+ buf[:] = bytes([fill]) * len(buf)
250
+ elif fmt == PixelFormat.Rgb565:
251
+ px = _rgb565(color)
252
+ buf[:] = bytes([px & 0xFF, (px >> 8) & 0xFF]) * (len(buf) // 2)
253
+ elif fmt == PixelFormat.Rgb888:
254
+ buf[:] = bytes([color.R, color.G, color.B]) * (len(buf) // 3)
255
+ elif fmt == PixelFormat.Argb8888:
256
+ buf[:] = bytes([color.A, color.R, color.G, color.B]) * (len(buf) // 4)
257
+
258
+ def SetPixel(self, x, y, color):
259
+ """Set a single pixel. No-op if (x, y) is outside the canvas."""
260
+ if self._disposed:
261
+ raise ObjectDisposedException("The Canvas is disposed")
262
+ if x < 0 or x >= self.Width or y < 0 or y >= self.Height:
263
+ return
264
+ fmt = self.PixelFormat
265
+ buf = self._buffer
266
+ if fmt == PixelFormat.Monochrome:
267
+ byte_index = y * ((self.Width + 7) // 8) + (x // 8)
268
+ bit = 7 - (x & 7)
269
+ if (color.R | color.G | color.B) != 0:
270
+ buf[byte_index] |= (1 << bit)
271
+ else:
272
+ buf[byte_index] &= (~(1 << bit)) & 0xFF
273
+ elif fmt == PixelFormat.Rgb565:
274
+ px = _rgb565(color)
275
+ i = (y * self.Width + x) * 2
276
+ buf[i] = px & 0xFF
277
+ buf[i + 1] = (px >> 8) & 0xFF
278
+ elif fmt == PixelFormat.Rgb888:
279
+ i = (y * self.Width + x) * 3
280
+ buf[i] = color.R; buf[i + 1] = color.G; buf[i + 2] = color.B
281
+ elif fmt == PixelFormat.Argb8888:
282
+ i = (y * self.Width + x) * 4
283
+ buf[i] = color.A; buf[i + 1] = color.R; buf[i + 2] = color.G; buf[i + 3] = color.B
284
+
285
+ def DrawLine(self, x0, y0, x1, y1, color, thickness=1):
286
+ """Bresenham line, honoring thickness by stamping an r-pixel disc at each step."""
287
+ x0 = int(x0); y0 = int(y0); x1 = int(x1); y1 = int(y1)
288
+ dx = abs(x1 - x0); sx = 1 if x0 < x1 else -1
289
+ dy = -abs(y1 - y0); sy = 1 if y0 < y1 else -1
290
+ err = dx + dy
291
+ r = thickness // 2
292
+ while True:
293
+ self._stamp_disc(x0, y0, r, color)
294
+ if x0 == x1 and y0 == y1:
295
+ break
296
+ e2 = 2 * err
297
+ if e2 >= dy:
298
+ err += dy; x0 += sx
299
+ if e2 <= dx:
300
+ err += dx; y0 += sy
301
+
302
+ def DrawRectangle(self, x, y, width, height, color, thickness=1):
303
+ """Stroke a rectangle outline."""
304
+ if thickness <= 0 or width <= 0 or height <= 0:
305
+ return
306
+ t = int(thickness)
307
+ x = int(x); y = int(y); width = int(width); height = int(height)
308
+ # Four axis-aligned bands via FillRectangle (one scanline slice per
309
+ # row on Rgb565): orders of magnitude faster than Bresenham DrawLine
310
+ # for borders. Top + bottom span the full width; left + right fill
311
+ # the remaining vertical strip so corners are not double-drawn.
312
+ self.FillRectangle(x, y, width, t, color)
313
+ self.FillRectangle(x, y + height - t, width, t, color)
314
+ inner_h = height - 2 * t
315
+ if inner_h > 0:
316
+ self.FillRectangle(x, y + t, t, inner_h, color)
317
+ self.FillRectangle(x + width - t, y + t, t, inner_h, color)
318
+
319
+ def FillRectangle(self, x, y, width, height, color):
320
+ """Fill a rectangle."""
321
+ if self._disposed:
322
+ raise ObjectDisposedException("The Canvas is disposed")
323
+ # Clip to canvas bounds.
324
+ x0 = max(0, int(x)); y0 = max(0, int(y))
325
+ x1 = min(self.Width, int(x) + int(width))
326
+ y1 = min(self.Height, int(y) + int(height))
327
+ if x0 >= x1 or y0 >= y1:
328
+ return
329
+ fmt = self.PixelFormat
330
+ buf = self._buffer
331
+ # Rgb565 fast path: slice-assign one scanline at a time.
332
+ if fmt == PixelFormat.Rgb565:
333
+ px = _rgb565(color)
334
+ row = bytes([px & 0xFF, (px >> 8) & 0xFF]) * (x1 - x0)
335
+ stride = self.Width * 2
336
+ for yy in range(y0, y1):
337
+ off = yy * stride + x0 * 2
338
+ buf[off: off + len(row)] = row
339
+ return
340
+ # Fallback: per-pixel (less common formats).
341
+ for yy in range(y0, y1):
342
+ for xx in range(x0, x1):
343
+ self.SetPixel(xx, yy, color)
344
+
345
+ def DrawCircle(self, cx, cy, radius, color, thickness=1):
346
+ """Midpoint-algorithm circle outline."""
347
+ for t in range(thickness):
348
+ self._midpoint_circle(cx, cy, radius - t, color, fill=False)
349
+
350
+ def FillCircle(self, cx, cy, radius, color):
351
+ """Filled circle."""
352
+ self._midpoint_circle(cx, cy, radius, color, fill=True)
353
+
354
+ def DrawArc(self, cx, cy, radius, start_deg, end_deg, color, thickness=1):
355
+ """Planned — not implemented in the managed prototype; needs an
356
+ LVGL-backed native backend."""
357
+ raise NotImplementedException("DrawArc is pending the LVGL-backed native implementation.")
358
+
359
+ def DrawText(self, text, font, x, y, size_px, color):
360
+ """Rasterize `text` with `font` at `size_px`, top-left at (x, y).
361
+ Alpha-blends against whatever is already in the canvas. The font is
362
+ rasterized on every call; cache into a Canvas and re-blit for high FPS."""
363
+ if self._disposed:
364
+ raise ObjectDisposedException("The Canvas is disposed")
365
+ if text is None:
366
+ raise ArgumentException("text must not be None")
367
+ if font is None:
368
+ raise ArgumentException("font must not be None")
369
+ if self.PixelFormat not in (PixelFormat.Rgb565, PixelFormat.Monochrome):
370
+ raise NotSupportedException(
371
+ f"DrawText into a {self.PixelFormat} canvas is not implemented "
372
+ "(Rgb565 and Monochrome targets only).")
373
+ if size_px <= 0 or len(text) == 0:
374
+ return
375
+
376
+ ascent, _, _ = font.GetMetrics(int(size_px))
377
+ pen_x = int(x)
378
+ baseline_y = int(y) + ascent
379
+ mono = self.PixelFormat == PixelFormat.Monochrome
380
+ lit = (color.R | color.G | color.B) != 0
381
+
382
+ for ch in text:
383
+ codepoint = ord(ch)
384
+ bmp = ctypes.POINTER(ctypes.c_uint8)()
385
+ gw = ctypes.c_int(); gh = ctypes.c_int()
386
+ gxo = ctypes.c_int(); gyo = ctypes.c_int()
387
+ advance = ctypes.c_int()
388
+ native.font_rasterize(font._handle, codepoint, int(size_px),
389
+ ctypes.byref(bmp),
390
+ ctypes.byref(gw), ctypes.byref(gh),
391
+ ctypes.byref(gxo), ctypes.byref(gyo),
392
+ ctypes.byref(advance))
393
+ try:
394
+ if bmp and gw.value > 0 and gh.value > 0:
395
+ if mono:
396
+ self._stamp_glyph_monochrome(bmp, gw.value, gh.value,
397
+ pen_x + gxo.value, baseline_y + gyo.value, lit)
398
+ else:
399
+ self._alpha_blend_glyph(bmp, gw.value, gh.value,
400
+ pen_x + gxo.value, baseline_y + gyo.value, color)
401
+ finally:
402
+ if bmp:
403
+ native.image_free(bmp)
404
+ pen_x += advance.value
405
+
406
+ def DrawImage(self, source, x, y, width=None, height=None, pixel_format=None):
407
+ """Blit an image into this canvas at (x, y).
408
+
409
+ - `source` is an Image: use its cached decoded pixels (fast path).
410
+ - `source` is bytes-like and width/height/pixel_format are None: treat
411
+ it as encoded file bytes (JPG/PNG/BMP/GIF/TGA), decode once and blit.
412
+ - `source` is bytes-like and width/height/pixel_format are given:
413
+ treat as a raw pixel buffer of that size and format."""
414
+ if self._disposed:
415
+ raise ObjectDisposedException("The Canvas is disposed")
416
+ if source is None:
417
+ raise ArgumentException("source must not be None")
418
+
419
+ if isinstance(source, Image):
420
+ self._blit_pixels(source._pixels, x, y, source.Width, source.Height, source.PixelFormat)
421
+ return
422
+
423
+ if width is None or height is None or pixel_format is None:
424
+ # Encoded file bytes: decode then blit.
425
+ if len(source) == 0:
426
+ raise ArgumentException("Image file bytes are empty.")
427
+ data = bytes(source)
428
+ src = (ctypes.c_uint8 * len(data)).from_buffer_copy(data)
429
+ out_pixels = ctypes.POINTER(ctypes.c_uint8)()
430
+ out_w = ctypes.c_int(); out_h = ctypes.c_int()
431
+ native.image_decode(src, len(data),
432
+ ctypes.byref(out_pixels), ctypes.byref(out_w), ctypes.byref(out_h))
433
+ try:
434
+ byte_count = out_w.value * out_h.value * 3
435
+ managed = bytes(ctypes.string_at(out_pixels, byte_count))
436
+ self._blit_pixels(managed, x, y, out_w.value, out_h.value, PixelFormat.Rgb888)
437
+ finally:
438
+ native.image_free(out_pixels)
439
+ return
440
+
441
+ # Raw pixel buffer path.
442
+ self._blit_pixels(bytes(source), x, y, int(width), int(height), pixel_format)
443
+
444
+ def Dispose(self):
445
+ """Release the pixel buffer. Idempotent."""
446
+ self._disposed = True
447
+ self._buffer = bytearray()
448
+
449
+ def __del__(self):
450
+ try: self.Dispose()
451
+ except Exception: pass
452
+
453
+ # ---------- internals -------------------------------------------------
454
+
455
+ def _stamp_disc(self, cx, cy, r, color):
456
+ if r <= 0:
457
+ self.SetPixel(cx, cy, color)
458
+ return
459
+ # Fill the disc as horizontal spans: one FillRectangle per row (which
460
+ # is a slice-assignment for Rgb565). Massively faster than per-pixel.
461
+ r2 = r * r
462
+ for yy in range(-r, r + 1):
463
+ # Half-width of the span at this row: largest xx with xx*xx+yy*yy <= r2.
464
+ # Closed form avoids an inner loop.
465
+ dx2 = r2 - yy * yy
466
+ if dx2 < 0:
467
+ continue
468
+ w = int(math.isqrt(dx2))
469
+ self.FillRectangle(cx - w, cy + yy, 2 * w + 1, 1, color)
470
+
471
+ def _midpoint_circle(self, cx, cy, r, color, fill):
472
+ if r <= 0:
473
+ self.SetPixel(cx, cy, color)
474
+ return
475
+ if fill:
476
+ # Filled circle = _stamp_disc; avoids calling FillRectangle 2r+1
477
+ # times for each ring of the midpoint traversal.
478
+ self._stamp_disc(cx, cy, r, color)
479
+ return
480
+ x, y, err = r, 0, 1 - r
481
+ while x >= y:
482
+ self.SetPixel(cx + x, cy + y, color); self.SetPixel(cx - x, cy + y, color)
483
+ self.SetPixel(cx + x, cy - y, color); self.SetPixel(cx - x, cy - y, color)
484
+ self.SetPixel(cx + y, cy + x, color); self.SetPixel(cx - y, cy + x, color)
485
+ self.SetPixel(cx + y, cy - x, color); self.SetPixel(cx - y, cy - x, color)
486
+ y += 1
487
+ if err <= 0:
488
+ err += 2 * y + 1
489
+ else:
490
+ x -= 1
491
+ err += 2 * (y - x) + 1
492
+
493
+ def _blit_pixels(self, pixels, x, y, width, height, pixel_format):
494
+ if width <= 0 or height <= 0:
495
+ return
496
+ if self.PixelFormat != PixelFormat.Rgb565:
497
+ raise NotSupportedException(
498
+ f"DrawImage into a {self.PixelFormat} canvas is not implemented "
499
+ "in the managed prototype (Rgb565 target only).")
500
+ if pixel_format == PixelFormat.Rgb565:
501
+ src_bpp = 2
502
+ elif pixel_format == PixelFormat.Rgb888:
503
+ src_bpp = 3
504
+ else:
505
+ raise NotSupportedException(
506
+ f"DrawImage source format {pixel_format} is not implemented "
507
+ "(Rgb565 and Rgb888 only).")
508
+ if len(pixels) < width * height * src_bpp:
509
+ raise ArgumentException("Pixel buffer is smaller than width * height * bytes-per-pixel.")
510
+
511
+ # Clip (x, y, width, height) to the canvas bounds.
512
+ src_x = max(0, -x)
513
+ src_y = max(0, -y)
514
+ dst_x = x + src_x
515
+ dst_y = y + src_y
516
+ clip_w = min(width - src_x, self.Width - dst_x)
517
+ clip_h = min(height - src_y, self.Height - dst_y)
518
+ if clip_w <= 0 or clip_h <= 0:
519
+ return
520
+
521
+ src_stride = width * src_bpp
522
+ dst_stride = self.Width * 2
523
+ buf = self._buffer
524
+
525
+ if pixel_format == PixelFormat.Rgb565:
526
+ for row in range(clip_h):
527
+ src_off = (src_y + row) * src_stride + src_x * src_bpp
528
+ dst_off = (dst_y + row) * dst_stride + dst_x * 2
529
+ buf[dst_off: dst_off + clip_w * 2] = pixels[src_off: src_off + clip_w * 2]
530
+ else: # Rgb888
531
+ for row in range(clip_h):
532
+ src_off = (src_y + row) * src_stride + src_x * src_bpp
533
+ dst_off = (dst_y + row) * dst_stride + dst_x * 2
534
+ for col in range(clip_w):
535
+ r = pixels[src_off + col * 3 + 0]
536
+ g = pixels[src_off + col * 3 + 1]
537
+ b = pixels[src_off + col * 3 + 2]
538
+ px = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
539
+ buf[dst_off + col * 2 + 0] = px & 0xFF
540
+ buf[dst_off + col * 2 + 1] = (px >> 8) & 0xFF
541
+
542
+ def _stamp_glyph_monochrome(self, glyph, gw, gh, dst_x, dst_y, lit):
543
+ # Stamp an 8-bit grayscale glyph bitmap into a 1bpp Monochrome canvas.
544
+ # Thresholds alpha: >= 128 sets/clears the bit (anti-aliasing is
545
+ # meaningless at 1bpp). `glyph` is a ctypes POINTER(c_uint8).
546
+ src_x0 = max(0, -dst_x)
547
+ src_y0 = max(0, -dst_y)
548
+ dst_x0 = dst_x + src_x0
549
+ dst_y0 = dst_y + src_y0
550
+ clip_w = min(gw - src_x0, self.Width - dst_x0)
551
+ clip_h = min(gh - src_y0, self.Height - dst_y0)
552
+ if clip_w <= 0 or clip_h <= 0:
553
+ return
554
+ row_bytes = (self.Width + 7) // 8
555
+ buf = self._buffer
556
+ for row in range(clip_h):
557
+ src_row = (src_y0 + row) * gw + src_x0
558
+ dst_row = (dst_y0 + row) * row_bytes
559
+ for col in range(clip_w):
560
+ if glyph[src_row + col] < 128:
561
+ continue
562
+ x = dst_x0 + col
563
+ bit = 7 - (x & 7)
564
+ off = dst_row + (x >> 3)
565
+ if lit:
566
+ buf[off] |= (1 << bit)
567
+ else:
568
+ buf[off] &= ~(1 << bit) & 0xFF
569
+
570
+ def _alpha_blend_glyph(self, glyph, gw, gh, dst_x, dst_y, color):
571
+ # Alpha-blends an 8-bit grayscale glyph bitmap over the current RGB565
572
+ # canvas at (dst_x, dst_y). `glyph` is a ctypes POINTER(c_uint8).
573
+ src_x0 = max(0, -dst_x)
574
+ src_y0 = max(0, -dst_y)
575
+ dst_x0 = dst_x + src_x0
576
+ dst_y0 = dst_y + src_y0
577
+ clip_w = min(gw - src_x0, self.Width - dst_x0)
578
+ clip_h = min(gh - src_y0, self.Height - dst_y0)
579
+ if clip_w <= 0 or clip_h <= 0:
580
+ return
581
+
582
+ src_r, src_g, src_b = color.R, color.G, color.B
583
+ dst_stride = self.Width * 2
584
+ buf = self._buffer
585
+
586
+ for row in range(clip_h):
587
+ src_row = (src_y0 + row) * gw + src_x0
588
+ dst_row = (dst_y0 + row) * dst_stride + dst_x0 * 2
589
+ for col in range(clip_w):
590
+ a = glyph[src_row + col]
591
+ if a == 0:
592
+ continue
593
+ dst_off = dst_row + col * 2
594
+ if a == 255:
595
+ px = ((src_r & 0xF8) << 8) | ((src_g & 0xFC) << 3) | (src_b >> 3)
596
+ buf[dst_off] = px & 0xFF
597
+ buf[dst_off + 1] = (px >> 8) & 0xFF
598
+ else:
599
+ old = buf[dst_off] | (buf[dst_off + 1] << 8)
600
+ old_r = ((old >> 11) & 0x1F) << 3
601
+ old_g = ((old >> 5) & 0x3F) << 2
602
+ old_b = (old & 0x1F) << 3
603
+ inv = 255 - a
604
+ r = (src_r * a + old_r * inv) // 255
605
+ g = (src_g * a + old_g * inv) // 255
606
+ b = (src_b * a + old_b * inv) // 255
607
+ px = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
608
+ buf[dst_off] = px & 0xFF
609
+ buf[dst_off + 1] = (px >> 8) & 0xFF
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: GHIElectronics.Endpoint.Drawing
3
+ Version: 0.5.3
4
+ Summary: Off-screen 2D drawing primitives for Endpoint: Canvas, Color, Font, Image, PixelFormat. Pure pixel drawing independent of the LVGL-backed UI widget library. Hand the Canvas buffer to any display, SPI/I2C device, or file.
5
+ Author-email: "GHI Electronics, LLC" <support@ghielectronics.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://www.ghielectronics.com/
8
+ Project-URL: Repository, https://github.com/ghi-electronics/Endpoint-Libraries
9
+ Project-URL: Issues, https://github.com/ghi-electronics/Endpoint-Libraries/issues
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: GHIElectronics.Endpoint.Core<0.6,>=0.5.3
15
+
16
+ # GHIElectronics.Endpoint.Drawing
17
+
18
+ Off-screen 2D drawing primitives for Endpoint: Canvas, Color, Font, Image, PixelFormat. Pure pixel drawing independent of the LVGL-backed UI widget library. Hand the Canvas buffer to any display, SPI/I2C device, or file.
19
+
20
+ Part of the [GHI Electronics Endpoint](https://github.com/ghi-electronics/Endpoint-Libraries) libraries. See the repository README for the full package list and getting-started guide.
@@ -0,0 +1,5 @@
1
+ GHIElectronics/Endpoint/Drawing/__init__.py,sha256=I_R9T3uVhv6-2rdhlR8Rol9AHH_W0XS1OF16Qrd0BSo,25799
2
+ ghielectronics_endpoint_drawing-0.5.3.dist-info/METADATA,sha256=Z9NMDfh5L60JtML8kaXWrsphM39Uubn-GkAk1fkEznM,1287
3
+ ghielectronics_endpoint_drawing-0.5.3.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
4
+ ghielectronics_endpoint_drawing-0.5.3.dist-info/top_level.txt,sha256=NpOZNgd0iquwrRPrTOhUDxc2V9TtNFLv5Z_Dx3YZtIU,15
5
+ ghielectronics_endpoint_drawing-0.5.3.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ GHIElectronics