valetudo-map-parser 0.1.9b100__py3-none-any.whl → 0.1.10__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.
- valetudo_map_parser/__init__.py +24 -8
- valetudo_map_parser/config/auto_crop.py +2 -27
- valetudo_map_parser/config/color_utils.py +3 -4
- valetudo_map_parser/config/colors.py +2 -2
- valetudo_map_parser/config/drawable.py +102 -153
- valetudo_map_parser/config/drawable_elements.py +0 -2
- valetudo_map_parser/config/fonts/FiraSans.ttf +0 -0
- valetudo_map_parser/config/fonts/Inter-VF.ttf +0 -0
- valetudo_map_parser/config/fonts/Lato-Regular.ttf +0 -0
- valetudo_map_parser/config/fonts/MPLUSRegular.ttf +0 -0
- valetudo_map_parser/config/fonts/NotoKufiArabic-VF.ttf +0 -0
- valetudo_map_parser/config/fonts/NotoSansCJKhk-VF.ttf +0 -0
- valetudo_map_parser/config/fonts/NotoSansKhojki.ttf +0 -0
- valetudo_map_parser/config/rand256_parser.py +169 -44
- valetudo_map_parser/config/shared.py +103 -101
- valetudo_map_parser/config/status_text/status_text.py +96 -0
- valetudo_map_parser/config/status_text/translations.py +280 -0
- valetudo_map_parser/config/types.py +42 -13
- valetudo_map_parser/config/utils.py +221 -181
- valetudo_map_parser/hypfer_draw.py +6 -169
- valetudo_map_parser/hypfer_handler.py +40 -130
- valetudo_map_parser/map_data.py +403 -84
- valetudo_map_parser/rand256_handler.py +53 -197
- valetudo_map_parser/reimg_draw.py +14 -24
- valetudo_map_parser/rooms_handler.py +3 -18
- {valetudo_map_parser-0.1.9b100.dist-info → valetudo_map_parser-0.1.10.dist-info}/METADATA +7 -4
- valetudo_map_parser-0.1.10.dist-info/RECORD +34 -0
- {valetudo_map_parser-0.1.9b100.dist-info → valetudo_map_parser-0.1.10.dist-info}/WHEEL +1 -1
- valetudo_map_parser/config/enhanced_drawable.py +0 -324
- valetudo_map_parser/hypfer_rooms_handler.py +0 -599
- valetudo_map_parser-0.1.9b100.dist-info/RECORD +0 -27
- {valetudo_map_parser-0.1.9b100.dist-info → valetudo_map_parser-0.1.10.dist-info/licenses}/LICENSE +0 -0
- {valetudo_map_parser-0.1.9b100.dist-info → valetudo_map_parser-0.1.10.dist-info/licenses}/NOTICE.txt +0 -0
@@ -1,27 +1,29 @@
|
|
1
1
|
"""Utility code for the valetudo map parser."""
|
2
2
|
|
3
3
|
import datetime
|
4
|
-
from time import time
|
5
4
|
import hashlib
|
5
|
+
import io
|
6
6
|
import json
|
7
7
|
from dataclasses import dataclass
|
8
|
-
from
|
9
|
-
import
|
8
|
+
from time import time
|
9
|
+
from typing import Callable, List, Optional, Tuple
|
10
10
|
|
11
11
|
import numpy as np
|
12
12
|
from PIL import Image, ImageOps
|
13
13
|
|
14
|
+
from ..map_data import HyperMapData
|
15
|
+
from .async_utils import AsyncNumPy
|
14
16
|
from .drawable import Drawable
|
15
17
|
from .drawable_elements import DrawingConfig
|
16
|
-
from .
|
18
|
+
from .status_text.status_text import StatusText
|
17
19
|
from .types import (
|
18
20
|
LOGGER,
|
19
21
|
ChargerPosition,
|
20
|
-
|
22
|
+
Destinations,
|
21
23
|
NumpyArray,
|
22
24
|
PilPNG,
|
23
25
|
RobotPosition,
|
24
|
-
|
26
|
+
Size,
|
25
27
|
)
|
26
28
|
|
27
29
|
|
@@ -69,24 +71,27 @@ class BaseHandler:
|
|
69
71
|
self.crop_img_size = [0, 0]
|
70
72
|
self.offset_x = 0
|
71
73
|
self.offset_y = 0
|
72
|
-
self.crop_area =
|
74
|
+
self.crop_area = [0, 0, 0, 0]
|
73
75
|
self.zooming = False
|
74
76
|
self.async_resize_images = async_resize_image
|
77
|
+
# Drawing components are initialized by initialize_drawing_config in handlers
|
78
|
+
self.drawing_config: Optional[DrawingConfig] = None
|
79
|
+
self.draw: Optional[Drawable] = None
|
75
80
|
|
76
81
|
def get_frame_number(self) -> int:
|
77
82
|
"""Return the frame number of the image."""
|
78
83
|
return self.frame_number
|
79
84
|
|
80
|
-
def get_robot_position(self) -> RobotPosition
|
85
|
+
def get_robot_position(self) -> RobotPosition:
|
81
86
|
"""Return the robot position."""
|
82
87
|
return self.robot_pos
|
83
88
|
|
84
89
|
async def async_get_image(
|
85
90
|
self,
|
86
91
|
m_json: dict | None,
|
87
|
-
destinations:
|
92
|
+
destinations: Destinations | None = None,
|
88
93
|
bytes_format: bool = False,
|
89
|
-
) -> PilPNG |
|
94
|
+
) -> Tuple[PilPNG | bytes, dict]:
|
90
95
|
"""
|
91
96
|
Unified async function to get PIL image from JSON data for both Hypfer and Rand256 handlers.
|
92
97
|
|
@@ -99,7 +104,9 @@ class BaseHandler:
|
|
99
104
|
@param m_json: The JSON data to use to draw the image
|
100
105
|
@param destinations: MQTT destinations for labels (used by Rand256)
|
101
106
|
@param bytes_format: If True, also convert to PNG bytes and store in shared.binary_image
|
102
|
-
@
|
107
|
+
@param text_enabled: If True, draw text on the image
|
108
|
+
@param vacuum_status: Vacuum status to display on the image
|
109
|
+
@return: PIL Image or None and data dictionary
|
103
110
|
"""
|
104
111
|
try:
|
105
112
|
# Backup current image to last_image before processing new one
|
@@ -112,13 +119,13 @@ class BaseHandler:
|
|
112
119
|
new_image = await self.get_image_from_rrm(
|
113
120
|
m_json=m_json,
|
114
121
|
destinations=destinations,
|
115
|
-
return_webp=False, # Always return PIL Image
|
116
122
|
)
|
123
|
+
|
117
124
|
elif hasattr(self, "async_get_image_from_json"):
|
118
125
|
# This is a Hypfer handler
|
126
|
+
self.json_data = await HyperMapData.async_from_valetudo_json(m_json)
|
119
127
|
new_image = await self.async_get_image_from_json(
|
120
128
|
m_json=m_json,
|
121
|
-
return_webp=False, # Always return PIL Image
|
122
129
|
)
|
123
130
|
else:
|
124
131
|
LOGGER.warning(
|
@@ -133,36 +140,47 @@ class BaseHandler:
|
|
133
140
|
|
134
141
|
# Store the new image in shared data
|
135
142
|
if new_image is not None:
|
143
|
+
# Update shared data
|
144
|
+
await self._async_update_shared_data(destinations)
|
136
145
|
self.shared.new_image = new_image
|
137
|
-
|
146
|
+
# Add text to the image
|
147
|
+
if self.shared.show_vacuum_state:
|
148
|
+
text_editor = StatusText(self.shared)
|
149
|
+
img_text = await text_editor.get_status_text(new_image)
|
150
|
+
Drawable.status_text(
|
151
|
+
new_image,
|
152
|
+
img_text[1],
|
153
|
+
self.shared.user_colors[8],
|
154
|
+
img_text[0],
|
155
|
+
self.shared.vacuum_status_font,
|
156
|
+
self.shared.vacuum_status_position,
|
157
|
+
)
|
138
158
|
# Convert to binary (PNG bytes) if requested
|
139
159
|
if bytes_format:
|
140
|
-
|
141
|
-
new_image.save(buf, format="PNG", compress_level=1)
|
142
|
-
self.shared.binary_image = buf.getvalue()
|
143
|
-
LOGGER.debug(
|
144
|
-
"%s: Binary image conversion completed", self.file_name
|
145
|
-
)
|
160
|
+
self.shared.binary_image = pil_to_png_bytes(new_image)
|
146
161
|
else:
|
147
|
-
self.shared.binary_image =
|
162
|
+
self.shared.binary_image = pil_to_png_bytes(self.shared.last_image)
|
148
163
|
# Update the timestamp with current datetime
|
149
164
|
self.shared.image_last_updated = datetime.datetime.fromtimestamp(time())
|
150
|
-
LOGGER.debug(
|
151
|
-
|
152
|
-
|
153
|
-
|
165
|
+
LOGGER.debug("%s: Frame Completed.", self.file_name)
|
166
|
+
data = {}
|
167
|
+
if bytes_format:
|
168
|
+
data = self.shared.to_dict()
|
169
|
+
return new_image, data
|
154
170
|
else:
|
155
171
|
LOGGER.warning(
|
156
172
|
"%s: Failed to generate image from JSON data", self.file_name
|
157
173
|
)
|
174
|
+
if bytes_format and hasattr(self.shared, "last_image"):
|
175
|
+
return pil_to_png_bytes(self.shared.last_image), {}
|
158
176
|
return (
|
159
177
|
self.shared.last_image
|
160
178
|
if hasattr(self.shared, "last_image")
|
161
179
|
else None
|
162
|
-
)
|
180
|
+
), {}
|
163
181
|
|
164
182
|
except Exception as e:
|
165
|
-
LOGGER.
|
183
|
+
LOGGER.warning(
|
166
184
|
"%s: Error in async_get_image: %s",
|
167
185
|
self.file_name,
|
168
186
|
str(e),
|
@@ -172,11 +190,71 @@ class BaseHandler:
|
|
172
190
|
self.shared.last_image if hasattr(self.shared, "last_image") else None
|
173
191
|
)
|
174
192
|
|
193
|
+
async def _async_update_shared_data(self, destinations: Destinations | None = None):
|
194
|
+
"""Update the shared data with the latest information."""
|
195
|
+
|
196
|
+
if hasattr(self, "get_rooms_attributes") and (
|
197
|
+
self.shared.map_rooms is None and destinations is not None
|
198
|
+
):
|
199
|
+
self.shared.map_rooms = await self.get_rooms_attributes(destinations)
|
200
|
+
if self.shared.map_rooms:
|
201
|
+
LOGGER.debug("%s: Rand256 attributes rooms updated", self.file_name)
|
202
|
+
|
203
|
+
|
204
|
+
if hasattr(self, "async_get_rooms_attributes") and (
|
205
|
+
self.shared.map_rooms is None
|
206
|
+
):
|
207
|
+
if self.shared.map_rooms is None:
|
208
|
+
self.shared.map_rooms = await self.async_get_rooms_attributes()
|
209
|
+
if self.shared.map_rooms:
|
210
|
+
LOGGER.debug("%s: Hyper attributes rooms updated", self.file_name)
|
211
|
+
|
212
|
+
if (
|
213
|
+
hasattr(self, "get_calibration_data")
|
214
|
+
and self.shared.attr_calibration_points is None
|
215
|
+
):
|
216
|
+
self.shared.attr_calibration_points = self.get_calibration_data(
|
217
|
+
self.shared.image_rotate
|
218
|
+
)
|
219
|
+
|
220
|
+
if not self.shared.image_size:
|
221
|
+
self.shared.image_size = self.get_img_size()
|
222
|
+
|
223
|
+
self.shared.vac_json_id = self.get_json_id()
|
224
|
+
|
225
|
+
if not self.shared.charger_position:
|
226
|
+
self.shared.charger_position = self.get_charger_position()
|
227
|
+
|
228
|
+
self.shared.current_room = self.get_robot_position()
|
229
|
+
|
230
|
+
def prepare_resize_params(
|
231
|
+
self, pil_img: PilPNG, rand: bool = False
|
232
|
+
) -> ResizeParams:
|
233
|
+
"""Prepare resize parameters for image resizing."""
|
234
|
+
if self.shared.image_rotate in [0, 180]:
|
235
|
+
width, height = pil_img.size
|
236
|
+
else:
|
237
|
+
height, width = pil_img.size
|
238
|
+
LOGGER.debug(
|
239
|
+
"Shared PIL image size: %s x %s",
|
240
|
+
self.shared.image_ref_width,
|
241
|
+
self.shared.image_ref_height,
|
242
|
+
)
|
243
|
+
return ResizeParams(
|
244
|
+
pil_img=pil_img,
|
245
|
+
width=width,
|
246
|
+
height=height,
|
247
|
+
aspect_ratio=self.shared.image_aspect_ratio,
|
248
|
+
crop_size=self.crop_img_size,
|
249
|
+
offset_func=self.async_map_coordinates_offset,
|
250
|
+
is_rand=rand,
|
251
|
+
)
|
252
|
+
|
175
253
|
def get_charger_position(self) -> ChargerPosition | None:
|
176
254
|
"""Return the charger position."""
|
177
255
|
return self.charger_pos
|
178
256
|
|
179
|
-
def get_img_size(self) ->
|
257
|
+
def get_img_size(self) -> Size | None:
|
180
258
|
"""Return the size of the image."""
|
181
259
|
return self.img_size
|
182
260
|
|
@@ -194,6 +272,30 @@ class BaseHandler:
|
|
194
272
|
or self.shared.image_aspect_ratio != "None"
|
195
273
|
)
|
196
274
|
|
275
|
+
# Element selection methods centralized here
|
276
|
+
def enable_element(self, element_code):
|
277
|
+
"""Enable drawing of a specific element."""
|
278
|
+
if hasattr(self, "drawing_config") and self.drawing_config is not None:
|
279
|
+
self.drawing_config.enable_element(element_code)
|
280
|
+
|
281
|
+
def disable_element(self, element_code):
|
282
|
+
"""Disable drawing of a specific element."""
|
283
|
+
manage_drawable_elements(self, "disable", element_code=element_code)
|
284
|
+
|
285
|
+
def set_elements(self, element_codes: list):
|
286
|
+
"""Enable only the specified elements, disable all others."""
|
287
|
+
manage_drawable_elements(self, "set_elements", element_codes=element_codes)
|
288
|
+
|
289
|
+
def set_element_property(self, element_code, property_name: str, value):
|
290
|
+
"""Set a drawing property for an element."""
|
291
|
+
manage_drawable_elements(
|
292
|
+
self,
|
293
|
+
"set_property",
|
294
|
+
element_code=element_code,
|
295
|
+
property_name=property_name,
|
296
|
+
value=value,
|
297
|
+
)
|
298
|
+
|
197
299
|
def _set_image_offset_ratio_1_1(
|
198
300
|
self, width: int, height: int, rand256: Optional[bool] = False
|
199
301
|
) -> None:
|
@@ -214,12 +316,6 @@ class BaseHandler:
|
|
214
316
|
elif rotation in [90, 270]:
|
215
317
|
self.offset_y = (self.crop_img_size[0] - width) // 2
|
216
318
|
self.offset_x = self.crop_img_size[1] - height
|
217
|
-
LOGGER.debug(
|
218
|
-
"%s Image Coordinates Offsets (x,y): %s. %s",
|
219
|
-
self.file_name,
|
220
|
-
self.offset_x,
|
221
|
-
self.offset_y,
|
222
|
-
)
|
223
319
|
|
224
320
|
def _set_image_offset_ratio_2_1(
|
225
321
|
self, width: int, height: int, rand256: Optional[bool] = False
|
@@ -242,13 +338,6 @@ class BaseHandler:
|
|
242
338
|
self.offset_x = width - self.crop_img_size[0]
|
243
339
|
self.offset_y = height - self.crop_img_size[1]
|
244
340
|
|
245
|
-
LOGGER.debug(
|
246
|
-
"%s Image Coordinates Offsets (x,y): %s. %s",
|
247
|
-
self.file_name,
|
248
|
-
self.offset_x,
|
249
|
-
self.offset_y,
|
250
|
-
)
|
251
|
-
|
252
341
|
def _set_image_offset_ratio_3_2(
|
253
342
|
self, width: int, height: int, rand256: Optional[bool] = False
|
254
343
|
) -> None:
|
@@ -273,13 +362,6 @@ class BaseHandler:
|
|
273
362
|
self.offset_y = (self.crop_img_size[0] - width) // 2
|
274
363
|
self.offset_x = self.crop_img_size[1] - height
|
275
364
|
|
276
|
-
LOGGER.debug(
|
277
|
-
"%s Image Coordinates Offsets (x,y): %s. %s",
|
278
|
-
self.file_name,
|
279
|
-
self.offset_x,
|
280
|
-
self.offset_y,
|
281
|
-
)
|
282
|
-
|
283
365
|
def _set_image_offset_ratio_5_4(
|
284
366
|
self, width: int, height: int, rand256: Optional[bool] = False
|
285
367
|
) -> None:
|
@@ -305,13 +387,6 @@ class BaseHandler:
|
|
305
387
|
self.offset_y = (self.crop_img_size[0] - width) // 2
|
306
388
|
self.offset_x = self.crop_img_size[1] - height
|
307
389
|
|
308
|
-
LOGGER.debug(
|
309
|
-
"%s Image Coordinates Offsets (x,y): %s. %s",
|
310
|
-
self.file_name,
|
311
|
-
self.offset_x,
|
312
|
-
self.offset_y,
|
313
|
-
)
|
314
|
-
|
315
390
|
def _set_image_offset_ratio_9_16(
|
316
391
|
self, width: int, height: int, rand256: Optional[bool] = False
|
317
392
|
) -> None:
|
@@ -333,13 +408,6 @@ class BaseHandler:
|
|
333
408
|
self.offset_x = width - self.crop_img_size[0]
|
334
409
|
self.offset_y = height - self.crop_img_size[1]
|
335
410
|
|
336
|
-
LOGGER.debug(
|
337
|
-
"%s Image Coordinates Offsets (x,y): %s. %s",
|
338
|
-
self.file_name,
|
339
|
-
self.offset_x,
|
340
|
-
self.offset_y,
|
341
|
-
)
|
342
|
-
|
343
411
|
def _set_image_offset_ratio_16_9(
|
344
412
|
self, width: int, height: int, rand256: Optional[bool] = False
|
345
413
|
) -> None:
|
@@ -361,13 +429,6 @@ class BaseHandler:
|
|
361
429
|
self.offset_x = width - self.crop_img_size[0]
|
362
430
|
self.offset_y = height - self.crop_img_size[1]
|
363
431
|
|
364
|
-
LOGGER.debug(
|
365
|
-
"%s Image Coordinates Offsets (x,y): %s. %s",
|
366
|
-
self.file_name,
|
367
|
-
self.offset_x,
|
368
|
-
self.offset_y,
|
369
|
-
)
|
370
|
-
|
371
432
|
async def async_map_coordinates_offset(
|
372
433
|
self, params: OffsetParams
|
373
434
|
) -> tuple[int, int]:
|
@@ -414,15 +475,21 @@ class BaseHandler:
|
|
414
475
|
return hashlib.sha256(data_json.encode()).hexdigest()
|
415
476
|
return None
|
416
477
|
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
return NumpyArray.copy(original_array)
|
478
|
+
async def async_copy_array(self, original_array: NumpyArray) -> NumpyArray:
|
479
|
+
"""Copy the array using AsyncNumPy to yield control to the event loop."""
|
480
|
+
return await AsyncNumPy.async_copy(original_array)
|
421
481
|
|
422
482
|
def get_map_points(
|
423
483
|
self,
|
424
484
|
) -> list[dict[str, int] | dict[str, int] | dict[str, int] | dict[str, int]]:
|
425
485
|
"""Return the map points."""
|
486
|
+
if not self.crop_img_size:
|
487
|
+
return [
|
488
|
+
{"x": 0, "y": 0},
|
489
|
+
{"x": 0, "y": 0},
|
490
|
+
{"x": 0, "y": 0},
|
491
|
+
{"x": 0, "y": 0},
|
492
|
+
]
|
426
493
|
return [
|
427
494
|
{"x": 0, "y": 0}, # Top-left corner 0
|
428
495
|
{"x": self.crop_img_size[0], "y": 0}, # Top-right corner 1
|
@@ -435,7 +502,13 @@ class BaseHandler:
|
|
435
502
|
|
436
503
|
def get_vacuum_points(self, rotation_angle: int) -> list[dict[str, int]]:
|
437
504
|
"""Calculate the calibration points based on the rotation angle."""
|
438
|
-
|
505
|
+
if not self.crop_area:
|
506
|
+
return [
|
507
|
+
{"x": 0, "y": 0},
|
508
|
+
{"x": 0, "y": 0},
|
509
|
+
{"x": 0, "y": 0},
|
510
|
+
{"x": 0, "y": 0},
|
511
|
+
]
|
439
512
|
# get_calibration_data
|
440
513
|
vacuum_points = [
|
441
514
|
{
|
@@ -528,7 +601,8 @@ class BaseHandler:
|
|
528
601
|
|
529
602
|
return vacuum_points
|
530
603
|
|
531
|
-
|
604
|
+
@staticmethod
|
605
|
+
async def async_zone_propriety(zones_data) -> dict:
|
532
606
|
"""Get the zone propriety"""
|
533
607
|
zone_properties = {}
|
534
608
|
id_count = 1
|
@@ -546,10 +620,11 @@ class BaseHandler:
|
|
546
620
|
}
|
547
621
|
id_count += 1
|
548
622
|
if id_count > 1:
|
549
|
-
|
623
|
+
pass
|
550
624
|
return zone_properties
|
551
625
|
|
552
|
-
|
626
|
+
@staticmethod
|
627
|
+
async def async_points_propriety(points_data) -> dict:
|
553
628
|
"""Get the point propriety"""
|
554
629
|
point_properties = {}
|
555
630
|
id_count = 1
|
@@ -567,7 +642,7 @@ class BaseHandler:
|
|
567
642
|
}
|
568
643
|
id_count += 1
|
569
644
|
if id_count > 1:
|
570
|
-
|
645
|
+
pass
|
571
646
|
return point_properties
|
572
647
|
|
573
648
|
@staticmethod
|
@@ -585,8 +660,14 @@ class BaseHandler:
|
|
585
660
|
|
586
661
|
async def async_resize_image(params: ResizeParams):
|
587
662
|
"""Resize the image to the given dimensions and aspect ratio."""
|
588
|
-
|
589
|
-
|
663
|
+
LOGGER.debug("Resizing image to aspect ratio: %s", params.aspect_ratio)
|
664
|
+
LOGGER.debug("Original image size: %s x %s", params.width, params.height)
|
665
|
+
LOGGER.debug("Image crop size: %s", params.crop_size)
|
666
|
+
if params.aspect_ratio == "None":
|
667
|
+
return params.pil_img
|
668
|
+
if params.aspect_ratio != "None":
|
669
|
+
ratio = params.aspect_ratio.replace(",", ":").replace(" ", "")
|
670
|
+
wsf, hsf = [int(x) for x in ratio.split(":")]
|
590
671
|
|
591
672
|
if wsf == 0 or hsf == 0 or params.width <= 0 or params.height <= 0:
|
592
673
|
LOGGER.warning(
|
@@ -609,29 +690,13 @@ async def async_resize_image(params: ResizeParams):
|
|
609
690
|
new_width = params.pil_img.width
|
610
691
|
new_height = int(params.pil_img.width / new_aspect_ratio)
|
611
692
|
|
612
|
-
LOGGER.debug("Resizing image to aspect ratio: %s, %s", wsf, hsf)
|
613
|
-
LOGGER.debug("New image size: %s x %s", new_width, new_height)
|
614
|
-
|
615
693
|
if (params.crop_size is not None) and (params.offset_func is not None):
|
616
694
|
offset = OffsetParams(wsf, hsf, new_width, new_height, params.is_rand)
|
617
695
|
params.crop_size[0], params.crop_size[1] = await params.offset_func(offset)
|
618
|
-
|
696
|
+
LOGGER.debug("New image size: %r * %r", new_width, new_height)
|
619
697
|
return ImageOps.pad(params.pil_img, (new_width, new_height))
|
620
698
|
|
621
|
-
return
|
622
|
-
|
623
|
-
|
624
|
-
def prepare_resize_params(handler, pil_img, rand):
|
625
|
-
"""Prepare resize parameters for image resizing."""
|
626
|
-
return ResizeParams(
|
627
|
-
pil_img=pil_img,
|
628
|
-
width=handler.shared.image_ref_width,
|
629
|
-
height=handler.shared.image_ref_height,
|
630
|
-
aspect_ratio=handler.shared.image_aspect_ratio,
|
631
|
-
crop_size=handler.crop_img_size,
|
632
|
-
offset_func=handler.async_map_coordinates_offset,
|
633
|
-
is_rand=rand,
|
634
|
-
)
|
699
|
+
return params.pil_img
|
635
700
|
|
636
701
|
|
637
702
|
def initialize_drawing_config(handler):
|
@@ -642,7 +707,7 @@ def initialize_drawing_config(handler):
|
|
642
707
|
handler: The handler instance with shared data and file_name attributes
|
643
708
|
|
644
709
|
Returns:
|
645
|
-
Tuple of (DrawingConfig, Drawable
|
710
|
+
Tuple of (DrawingConfig, Drawable)
|
646
711
|
"""
|
647
712
|
|
648
713
|
# Initialize drawing configuration
|
@@ -654,11 +719,10 @@ def initialize_drawing_config(handler):
|
|
654
719
|
):
|
655
720
|
drawing_config.update_from_device_info(handler.shared.device_info)
|
656
721
|
|
657
|
-
# Initialize
|
658
|
-
draw = Drawable()
|
659
|
-
enhanced_draw = EnhancedDrawable(drawing_config) # New enhanced drawing system
|
722
|
+
# Initialize drawing utilities
|
723
|
+
draw = Drawable()
|
660
724
|
|
661
|
-
return drawing_config, draw
|
725
|
+
return drawing_config, draw
|
662
726
|
|
663
727
|
|
664
728
|
def blend_colors(base_color, overlay_color):
|
@@ -787,6 +851,51 @@ def manage_drawable_elements(
|
|
787
851
|
handler.drawing_config.set_property(element_code, property_name, value)
|
788
852
|
|
789
853
|
|
854
|
+
def point_in_polygon(x: int, y: int, polygon: list) -> bool:
|
855
|
+
"""
|
856
|
+
Check if a point is inside a polygon using ray casting algorithm.
|
857
|
+
Enhanced version with better handling of edge cases.
|
858
|
+
|
859
|
+
Args:
|
860
|
+
x: X coordinate of the point
|
861
|
+
y: Y coordinate of the point
|
862
|
+
polygon: List of (x, y) tuples forming the polygon
|
863
|
+
|
864
|
+
Returns:
|
865
|
+
True if the point is inside the polygon, False otherwise
|
866
|
+
"""
|
867
|
+
# Ensure we have a valid polygon with at least 3 points
|
868
|
+
if len(polygon) < 3:
|
869
|
+
return False
|
870
|
+
|
871
|
+
# Make sure the polygon is closed (last point equals first point)
|
872
|
+
if polygon[0] != polygon[-1]:
|
873
|
+
polygon = polygon + [polygon[0]]
|
874
|
+
|
875
|
+
# Use winding number algorithm for better accuracy
|
876
|
+
wn = 0 # Winding number counter
|
877
|
+
|
878
|
+
# Loop through all edges of the polygon
|
879
|
+
for i in range(len(polygon) - 1): # Last vertex is first vertex
|
880
|
+
p1x, p1y = polygon[i]
|
881
|
+
p2x, p2y = polygon[i + 1]
|
882
|
+
|
883
|
+
# Test if a point is left/right/on the edge defined by two vertices
|
884
|
+
if p1y <= y: # Start y <= P.y
|
885
|
+
if p2y > y: # End y > P.y (upward crossing)
|
886
|
+
# Point left of edge
|
887
|
+
if ((p2x - p1x) * (y - p1y) - (x - p1x) * (p2y - p1y)) > 0:
|
888
|
+
wn += 1 # Valid up intersect
|
889
|
+
else: # Start y > P.y
|
890
|
+
if p2y <= y: # End y <= P.y (downward crossing)
|
891
|
+
# Point right of edge
|
892
|
+
if ((p2x - p1x) * (y - p1y) - (x - p1x) * (p2y - p1y)) < 0:
|
893
|
+
wn -= 1 # Valid down intersect
|
894
|
+
|
895
|
+
# If winding number is not 0, the point is inside the polygon
|
896
|
+
return wn != 0
|
897
|
+
|
898
|
+
|
790
899
|
def handle_room_outline_error(file_name, room_id, error):
|
791
900
|
"""
|
792
901
|
Handle errors during room outline extraction.
|
@@ -943,83 +1052,14 @@ async def async_extract_room_outline(
|
|
943
1052
|
return rect_outline
|
944
1053
|
|
945
1054
|
|
946
|
-
|
947
|
-
|
948
|
-
)
|
949
|
-
|
950
|
-
|
1055
|
+
def pil_to_png_bytes(pil_img: Image.Image, compress_level: int = 1) -> bytes:
|
1056
|
+
"""Convert PIL Image to PNG bytes asynchronously."""
|
1057
|
+
with io.BytesIO() as buf:
|
1058
|
+
pil_img.save(buf, format="PNG", compress_level=compress_level)
|
1059
|
+
return buf.getvalue()
|
951
1060
|
|
952
|
-
Args:
|
953
|
-
img_np_array: RGBA NumPy array
|
954
|
-
quality: WebP quality (0-100, ignored if lossless=True)
|
955
|
-
lossless: Use lossless WebP compression
|
956
1061
|
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
pil_img = Image.fromarray(img_np_array, mode="RGBA")
|
962
|
-
|
963
|
-
# Create bytes buffer
|
964
|
-
webp_buffer = io.BytesIO()
|
965
|
-
|
966
|
-
# Save as WebP - PIL images should use lossless=True for best results
|
967
|
-
pil_img.save(
|
968
|
-
webp_buffer,
|
969
|
-
format="WEBP",
|
970
|
-
lossless=True, # Always lossless for PIL images
|
971
|
-
method=1, # Fastest method for lossless
|
972
|
-
)
|
973
|
-
|
974
|
-
# Get bytes and cleanup
|
975
|
-
webp_bytes = webp_buffer.getvalue()
|
976
|
-
webp_buffer.close()
|
977
|
-
|
978
|
-
return webp_bytes
|
979
|
-
|
980
|
-
|
981
|
-
async def pil_to_webp_bytes(
|
982
|
-
pil_img: Image.Image, quality: int = 85, lossless: bool = False
|
983
|
-
) -> bytes:
|
984
|
-
"""
|
985
|
-
Convert PIL Image to WebP bytes.
|
986
|
-
|
987
|
-
Args:
|
988
|
-
pil_img: PIL Image object
|
989
|
-
quality: WebP quality (0-100, ignored if lossless=True)
|
990
|
-
lossless: Use lossless WebP compression
|
991
|
-
|
992
|
-
Returns:
|
993
|
-
WebP image as bytes
|
994
|
-
"""
|
995
|
-
# Create bytes buffer
|
996
|
-
webp_buffer = io.BytesIO()
|
997
|
-
|
998
|
-
# Save as WebP - PIL images should use lossless=True for best results
|
999
|
-
pil_img.save(
|
1000
|
-
webp_buffer,
|
1001
|
-
format="WEBP",
|
1002
|
-
lossless=True, # Always lossless for PIL images
|
1003
|
-
method=1, # Fastest method for lossless
|
1004
|
-
)
|
1005
|
-
|
1006
|
-
# Get bytes and cleanup
|
1007
|
-
webp_bytes = webp_buffer.getvalue()
|
1008
|
-
webp_buffer.close()
|
1009
|
-
|
1010
|
-
return webp_bytes
|
1011
|
-
|
1012
|
-
|
1013
|
-
def webp_bytes_to_pil(webp_bytes: bytes) -> Image.Image:
|
1014
|
-
"""
|
1015
|
-
Convert WebP bytes back to PIL Image for display or further processing.
|
1016
|
-
|
1017
|
-
Args:
|
1018
|
-
webp_bytes: WebP image as bytes
|
1019
|
-
|
1020
|
-
Returns:
|
1021
|
-
PIL Image object
|
1022
|
-
"""
|
1023
|
-
webp_buffer = io.BytesIO(webp_bytes)
|
1024
|
-
pil_img = Image.open(webp_buffer)
|
1025
|
-
return pil_img
|
1062
|
+
def png_bytes_to_pil(png_bytes: bytes) -> Image.Image:
|
1063
|
+
"""Convert PNG bytes back to a PIL Image."""
|
1064
|
+
png_buffer = io.BytesIO(png_bytes)
|
1065
|
+
return Image.open(png_buffer)
|