RGBMatrixEmulator 0.18.0__tar.gz → 0.18.2__tar.gz

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.
Files changed (55) hide show
  1. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/PKG-INFO +2 -2
  2. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/browser_adapter/adapter.py +10 -1
  3. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/browser_adapter/request_handlers/image_web_socket.py +8 -0
  4. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/browser_adapter/static/assets/client.js +11 -6
  5. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/browser_adapter/static/assets/styles.css +3 -2
  6. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/browser_adapter/static/index.html +2 -1
  7. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/raw_adapter/__init__.py +5 -8
  8. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/emulation/canvas.py +9 -32
  9. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/graphics/__init__.py +18 -8
  10. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/version.py +1 -1
  11. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/pyproject.toml +1 -1
  12. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/.gitignore +0 -0
  13. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/LICENSE +0 -0
  14. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/README.md +0 -0
  15. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/__init__.py +0 -0
  16. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/__init__.py +0 -0
  17. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/base.py +0 -0
  18. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/browser_adapter/README.md +0 -0
  19. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/browser_adapter/__init__.py +0 -0
  20. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/browser_adapter/fps.py +0 -0
  21. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/browser_adapter/request_handlers/__init__.py +0 -0
  22. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/browser_adapter/request_handlers/base.py +0 -0
  23. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/browser_adapter/request_handlers/image.py +0 -0
  24. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/browser_adapter/request_handlers/main.py +0 -0
  25. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/browser_adapter/request_handlers/single_file.py +0 -0
  26. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/browser_adapter/server.py +0 -0
  27. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/browser_adapter/static/assets/icon.ico +0 -0
  28. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/pygame_adapter.py +0 -0
  29. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/raw_adapter/README.md +0 -0
  30. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/sixel_adapter.py +0 -0
  31. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/terminal_adapter.py +0 -0
  32. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/tkinter_adapter.py +0 -0
  33. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/adapters/turtle_adapter.py +0 -0
  34. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/cli/__init__.py +0 -0
  35. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/cli/command.py +0 -0
  36. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/cli/config.py +0 -0
  37. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/emulation/__init__.py +0 -0
  38. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/emulation/matrix.py +0 -0
  39. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/emulation/options.py +0 -0
  40. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/graphics/color.py +0 -0
  41. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/graphics/font.py +0 -0
  42. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/icon.png +0 -0
  43. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/internal/adapter_loader.py +0 -0
  44. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/internal/emulator_config.py +0 -0
  45. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/internal/pixel_style.py +0 -0
  46. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/internal/screen.py +0 -0
  47. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/logger.py +0 -0
  48. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/pixel_mappers/__init__.py +0 -0
  49. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/pixel_mappers/identity.py +0 -0
  50. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/pixel_mappers/mirror.py +0 -0
  51. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/pixel_mappers/rotate.py +0 -0
  52. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/pixel_mappers/stack_to_row.py +0 -0
  53. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/pixel_mappers/umapper.py +0 -0
  54. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/pixel_mappers/vmapper.py +0 -0
  55. {rgbmatrixemulator-0.18.0 → rgbmatrixemulator-0.18.2}/RGBMatrixEmulator/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: RGBMatrixEmulator
3
- Version: 0.18.0
3
+ Version: 0.18.2
4
4
  Summary: A PC emulator for Raspberry Pi LED matrices driven by rpi-rgb-led-matrix
5
5
  Project-URL: Homepage, https://github.com/ty-porter/RGBMatrixEmulator
6
6
  Author-email: Tyler Porter <tyler.b.porter@gmail.com>
@@ -20,7 +20,7 @@ Requires-Python: >=3.10
20
20
  Requires-Dist: bdfparser<=2.2.0
21
21
  Requires-Dist: numpy>=1.2.0
22
22
  Requires-Dist: pillow>=10.0.1
23
- Requires-Dist: tornado>=6.5.5
23
+ Requires-Dist: tornado>=6.5.7
24
24
  Provides-Extra: pygame
25
25
  Requires-Dist: pygame<3,>=2.0.1; extra == 'pygame'
26
26
  Provides-Extra: sixel
@@ -35,6 +35,8 @@ class BrowserAdapter(BaseAdapter):
35
35
  self.default_image_format.lower()
36
36
  )
37
37
 
38
+ self._encode_kwargs = self.__build_encode_kwargs()
39
+
38
40
  # Default icon path is browser adapter assets
39
41
  self.default_icon_path = str(
40
42
  (Path(__file__).parent / "static" / "assets" / "icon.ico").resolve()
@@ -61,7 +63,7 @@ class BrowserAdapter(BaseAdapter):
61
63
  bytesIO,
62
64
  self.image_format,
63
65
  quality=self.options.browser.quality,
64
- optimize=True,
66
+ **self._encode_kwargs,
65
67
  )
66
68
  self.image = bytesIO.getvalue()
67
69
 
@@ -78,3 +80,10 @@ class BrowserAdapter(BaseAdapter):
78
80
  except Exception as e:
79
81
  Logger.exception("Failed to open a browser window")
80
82
  Logger.exception(e)
83
+
84
+ def __build_encode_kwargs(self) -> dict:
85
+ # use libwebp's realtime encoder
86
+ if self.image_format == "WebP":
87
+ return {"method": 0}
88
+
89
+ return {}
@@ -41,6 +41,14 @@ class ImageWebSocketHandler(tornado.websocket.WebSocketHandler):
41
41
  ImageWebSocketHandler.clients.add(self)
42
42
  Logger.info("WebSocket opened from: " + self.request.remote_ip)
43
43
 
44
+ # Push the current frame immediately so a newly-connected client sees the
45
+ # display right away instead of waiting for the next changed frame.
46
+ if ImageWebSocketHandler.adapter.image:
47
+ io_loop = tornado.ioloop.IOLoop.current()
48
+ io_loop.add_callback(
49
+ self.write_message, ImageWebSocketHandler.adapter.image, binary=True
50
+ )
51
+
44
52
  def on_message(self, _message):
45
53
  if not ImageWebSocketHandler.adapter.image:
46
54
  Logger.warning(
@@ -3,7 +3,8 @@
3
3
  const FPS_DEFAULT = 60; // seconds
4
4
  const FPS_UPDATE_RATE = 300; // milliseconds
5
5
 
6
- const img = document.getElementById("liveImg");
6
+ const canvas = document.getElementById("liveImg");
7
+ const ctx = canvas.getContext("2d", { alpha: false });
7
8
  const fpsText = document.getElementById("fps");
8
9
  const fpsTarget = parseInt(document.getElementById("targetFps").value) || FPS_DEFAULT;
9
10
 
@@ -50,11 +51,15 @@
50
51
  ws.onmessage = function(evt) {
51
52
  nFrames++;
52
53
 
53
- const arrayBuffer = evt.data;
54
- const blob = new Blob([new Uint8Array(arrayBuffer)], { type: "image/jpeg" });
55
- const old_img = img.src.slice()
56
- img.src = window.URL.createObjectURL(blob);
57
- window.URL.revokeObjectURL(old_img);
54
+ const blob = new Blob([evt.data], { type: IMAGE_MIME });
55
+ createImageBitmap(blob).then((bitmap) => {
56
+ if (canvas.width !== bitmap.width || canvas.height !== bitmap.height) {
57
+ canvas.width = bitmap.width;
58
+ canvas.height = bitmap.height;
59
+ }
60
+ ctx.drawImage(bitmap, 0, 0);
61
+ bitmap.close();
62
+ }).catch(() => {});
58
63
 
59
64
  if (fpsText) {
60
65
  const endTime = performance.now();
@@ -4,11 +4,12 @@ body {
4
4
  font-family: monospace;
5
5
  }
6
6
 
7
- img#liveImg {
7
+ #liveImg {
8
8
  border: 1px solid gray;
9
+ image-rendering: pixelated;
9
10
  }
10
11
 
11
- img#liveImg.no-border {
12
+ #liveImg.no-border {
12
13
  border: none;
13
14
  }
14
15
 
@@ -6,7 +6,7 @@
6
6
  <link rel="stylesheet" href="assets/styles.css">
7
7
  </head>
8
8
  <body>
9
- <img id="liveImg" class={{ "" if adapter.options.browser.image_border else "no-border" }} />
9
+ <canvas id="liveImg" class={{ "" if adapter.options.browser.image_border else "no-border" }}></canvas>
10
10
 
11
11
  {% if adapter.options.browser.fps_display %}
12
12
  <div id="fpsDisplay">
@@ -139,6 +139,7 @@
139
139
 
140
140
  <input id="targetFps" type="hidden" value={{ adapter.options.browser.target_fps }} />
141
141
 
142
+ <script>const IMAGE_MIME = "image/{{ adapter.image_format.lower() }}";</script>
142
143
  <script type="text/javascript" src="assets/client.js"></script>
143
144
  </body>
144
145
  </html>
@@ -15,7 +15,11 @@ class RawAdapter(BaseAdapter):
15
15
  def __init__(self, width, height, options):
16
16
  super().__init__(width, height, options)
17
17
 
18
- self._reset()
18
+ self.frames = {}
19
+ self.frame = 0
20
+
21
+ self.halt_after = RawAdapter.DEFAULT_MAX_FRAME
22
+ self.halt_fn = lambda: 1 + 1
19
23
 
20
24
  def draw_to_screen(self, pixels):
21
25
  self.frames[self.frame] = pixels
@@ -37,10 +41,3 @@ class RawAdapter(BaseAdapter):
37
41
 
38
42
  def _last_frame(self):
39
43
  return self.frames[self.frame - 1]
40
-
41
- def _reset(self):
42
- self.frames = {}
43
- self.frame = 0
44
-
45
- self.halt_after = RawAdapter.DEFAULT_MAX_FRAME
46
- self.halt_fn = lambda: 1 + 1
@@ -25,20 +25,14 @@ class Canvas:
25
25
  self.display_adapter.load_emulator_window()
26
26
 
27
27
  def Clear(self) -> None:
28
- self.__pixels = np.full(
29
- self.__pdims, self.__create_pixel((0, 0, 0)), dtype=np.uint8
30
- )
28
+ self.__pixels = np.full(self.__pdims, (0, 0, 0), dtype=np.uint8)
31
29
 
32
30
  def Fill(self, r: int, g: int, b: int) -> None:
33
- self.__pixels = np.full(
34
- self.__pdims, self.__create_pixel((r, g, b)), dtype=np.uint8
35
- )
31
+ self.__pixels = np.full(self.__pdims, (r, g, b), dtype=np.uint8)
36
32
 
37
33
  def SetPixel(self, x: int, y: int, r: int, g: int, b: int) -> None:
38
- if self.__pixel_out_of_bounds(x, y):
39
- return
40
-
41
- self.__pixels[int(y)][int(x)] = self.__create_pixel((r, g, b))
34
+ if 0 <= x < self.width and 0 <= y < self.height:
35
+ self.__pixels[int(y), int(x)] = (r, g, b)
42
36
 
43
37
  def SetImage(
44
38
  self,
@@ -47,9 +41,6 @@ class Canvas:
47
41
  offset_y: int = 0,
48
42
  unsafe: bool = True,
49
43
  ) -> None:
50
- enhancer = ImageEnhance.Brightness(image)
51
- image = enhancer.enhance(self.brightness / 100.0)
52
-
53
44
  original = Image.fromarray(self.__pixels, "RGB")
54
45
  original.paste(image, (offset_x, offset_y))
55
46
  self.__pixels = np.copy(original) # type: ignore
@@ -69,27 +60,13 @@ class Canvas:
69
60
 
70
61
  self.options.brightness = value
71
62
 
72
- def __create_pixel(self, pixel):
73
- return self.__adjust_brightness(tuple(pixel), self.brightness / 100.0)
74
-
75
- def __pixel_out_of_bounds(self, x, y):
76
- if x < 0 or x >= self.width:
77
- return True
78
-
79
- if y < 0 or y >= self.height:
80
- return True
81
-
82
- return False
83
-
84
- def __adjust_brightness(self, pixel, alpha, to_int=False):
85
- if to_int:
86
- return tuple(int(channel) for channel in pixel)
87
-
88
- return tuple(channel * alpha for channel in pixel)
89
-
90
63
  # These are delegated to the display adapter to handle specific implementation.
91
64
  def draw_to_screen(self) -> None:
92
- self.display_adapter.draw_to_screen(self.__screen.render(self.__pixels))
65
+ # Handle brightness across the entire pixel buffer as soon as it's ready to draw.
66
+ alpha = self.brightness / 100.0
67
+ pixels = (self.__screen.render(self.__pixels) * alpha).astype(np.uint8)
68
+
69
+ self.display_adapter.draw_to_screen(pixels)
93
70
 
94
71
  def check_for_quit_event(self) -> None:
95
72
  self.display_adapter.check_for_quit_event()
@@ -12,18 +12,28 @@ def validate_color(func):
12
12
  """
13
13
  Decorator to validate that the 'color' argument is of type Color.
14
14
  If not, attempts to map to the rpi-rgb-led-matrix error message.
15
+
16
+ The 'color' parameter's position is resolved once at decoration time so the
17
+ per-call path is just a lookup and an isinstance check -- these draw
18
+ functions run in tight per-frame loops, so inspecting the signature on every
19
+ call (signature/bind/apply_defaults) is far too expensive.
15
20
  """
21
+ try:
22
+ color_index = list(signature(func).parameters).index("color")
23
+ except ValueError:
24
+ color_index = None
16
25
 
17
26
  @wraps(func)
18
27
  def wrapper(*args, **kwargs):
19
- # Map positional + keyword args to parameter names
20
- sig = signature(func)
21
- bound = sig.bind(*args, **kwargs)
22
- bound.apply_defaults()
23
-
24
- if "color" in bound.arguments:
25
- value = bound.arguments["color"]
26
- if not isinstance(value, Color):
28
+ value = None
29
+
30
+ if color_index is not None:
31
+ if "color" in kwargs:
32
+ value = kwargs["color"]
33
+ elif color_index < len(args):
34
+ value = args[color_index]
35
+
36
+ if value is not None and not isinstance(value, Color):
27
37
  expected = f"{Color.__module__}.{Color.__qualname__}"
28
38
  actual = f"{type(value).__module__}.{type(value).__qualname__}"
29
39
  raise TypeError(
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python
2
2
 
3
3
  # package version
4
- __version__ = "0.18.0"
4
+ __version__ = "0.18.2"
5
5
  """Installed version of RGBMatrixEmulator."""
@@ -35,7 +35,7 @@ dependencies = [
35
35
  "bdfparser<=2.2.0",
36
36
  "numpy>=1.2.0",
37
37
  "Pillow>=10.0.1",
38
- "tornado>=6.5.5",
38
+ "tornado>=6.5.7",
39
39
  ]
40
40
 
41
41
  [project.scripts]