maps4fs 2.8.1__tar.gz → 2.8.3__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.

Potentially problematic release.


This version of maps4fs might be problematic. Click here for more details.

Files changed (36) hide show
  1. {maps4fs-2.8.1 → maps4fs-2.8.3}/PKG-INFO +15 -11
  2. {maps4fs-2.8.1 → maps4fs-2.8.3}/README.md +13 -10
  3. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs/generator/component/background.py +282 -1
  4. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs/generator/component/base/component.py +23 -4
  5. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs/generator/component/base/component_image.py +33 -0
  6. maps4fs-2.8.3/maps4fs/generator/component/base/component_mesh.py +678 -0
  7. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs/generator/game.py +11 -1
  8. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs/generator/map.py +15 -1
  9. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs/generator/settings.py +7 -0
  10. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs.egg-info/PKG-INFO +15 -11
  11. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs.egg-info/requires.txt +1 -0
  12. {maps4fs-2.8.1 → maps4fs-2.8.3}/pyproject.toml +2 -1
  13. maps4fs-2.8.1/maps4fs/generator/component/base/component_mesh.py +0 -271
  14. {maps4fs-2.8.1 → maps4fs-2.8.3}/LICENSE.md +0 -0
  15. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs/__init__.py +0 -0
  16. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs/generator/__init__.py +0 -0
  17. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs/generator/component/__init__.py +0 -0
  18. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs/generator/component/base/__init__.py +0 -0
  19. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs/generator/component/base/component_xml.py +0 -0
  20. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs/generator/component/config.py +0 -0
  21. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs/generator/component/dem.py +0 -0
  22. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs/generator/component/grle.py +0 -0
  23. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs/generator/component/i3d.py +0 -0
  24. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs/generator/component/layer.py +0 -0
  25. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs/generator/component/satellite.py +0 -0
  26. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs/generator/component/texture.py +0 -0
  27. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs/generator/config.py +0 -0
  28. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs/generator/qgis.py +0 -0
  29. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs/generator/statistics.py +0 -0
  30. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs/generator/utils.py +0 -0
  31. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs/logger.py +0 -0
  32. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs.egg-info/SOURCES.txt +0 -0
  33. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs.egg-info/dependency_links.txt +0 -0
  34. {maps4fs-2.8.1 → maps4fs-2.8.3}/maps4fs.egg-info/top_level.txt +0 -0
  35. {maps4fs-2.8.1 → maps4fs-2.8.3}/setup.cfg +0 -0
  36. {maps4fs-2.8.1 → maps4fs-2.8.3}/tests/test_generator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maps4fs
3
- Version: 2.8.1
3
+ Version: 2.8.3
4
4
  Summary: Generate map templates for Farming Simulator from real places.
5
5
  Author-email: iwatkot <iwatkot@gmail.com>
6
6
  License: GNU Affero General Public License v3.0
@@ -26,18 +26,22 @@ Requires-Dist: scipy
26
26
  Requires-Dist: pydtmdl
27
27
  Requires-Dist: manifold3d
28
28
  Requires-Dist: fast-simplification
29
+ Requires-Dist: open3d
29
30
  Dynamic: license-file
30
31
 
31
- <p align="center">
32
- <a href="https://github.com/iwatkot/maps4fs">maps4fs</a> •
33
- <a href="https://github.com/iwatkot/maps4fsui">maps4fs UI</a> •
34
- <a href="https://github.com/iwatkot/maps4fsdata">maps4fs Data</a> •
35
- <a href="https://github.com/iwatkot/maps4fsapi">maps4fs API</a> •
36
- <a href="https://github.com/iwatkot/maps4fsstats">maps4fs Stats</a> •
37
- <a href="https://github.com/iwatkot/maps4fsbot">maps4fs Bot</a><br>
38
- <a href="https://github.com/iwatkot/pygmdl">pygmdl</a> •
39
- <a href="https://github.com/iwatkot/pydtmdl">pydtmdl</a>
40
- </p>
32
+ <div align="center" markdown>
33
+
34
+ [![Maps4FS](https://img.shields.io/badge/maps4fs-gray?style=for-the-badge)](https://github.com/iwatkot/maps4fs)
35
+ [![PYDTMDL](https://img.shields.io/badge/pydtmdl-blue?style=for-the-badge)](https://github.com/iwatkot/pydtmdl)
36
+ [![PYGDMDL](https://img.shields.io/badge/pygmdl-teal?style=for-the-badge)](https://github.com/iwatkot/pygmdl)
37
+ [![Maps4FS API](https://img.shields.io/badge/maps4fs-api-green?style=for-the-badge)](https://github.com/iwatkot/maps4fsapi)
38
+ [![Maps4FS UI](https://img.shields.io/badge/maps4fs-ui-blue?style=for-the-badge)](https://github.com/iwatkot/maps4fsui)
39
+ [![Maps4FS Data](https://img.shields.io/badge/maps4fs-data-orange?style=for-the-badge)](https://github.com/iwatkot/maps4fsdata)
40
+ [![Maps4FS Upgrader](https://img.shields.io/badge/maps4fs-upgrader-yellow?style=for-the-badge)](https://github.com/iwatkot/maps4fsupgrader)
41
+ [![Maps4FS Stats](https://img.shields.io/badge/maps4fs-stats-red?style=for-the-badge)](https://github.com/iwatkot/maps4fsstats)
42
+ [![Maps4FS Bot](https://img.shields.io/badge/maps4fs-bot-teal?style=for-the-badge)](https://github.com/iwatkot/maps4fsbot)
43
+
44
+ </div>
41
45
 
42
46
  <div align="center" markdown>
43
47
  <a href="https://discord.gg/Sj5QKKyE42">
@@ -1,13 +1,16 @@
1
- <p align="center">
2
- <a href="https://github.com/iwatkot/maps4fs">maps4fs</a> •
3
- <a href="https://github.com/iwatkot/maps4fsui">maps4fs UI</a> •
4
- <a href="https://github.com/iwatkot/maps4fsdata">maps4fs Data</a> •
5
- <a href="https://github.com/iwatkot/maps4fsapi">maps4fs API</a> •
6
- <a href="https://github.com/iwatkot/maps4fsstats">maps4fs Stats</a> •
7
- <a href="https://github.com/iwatkot/maps4fsbot">maps4fs Bot</a><br>
8
- <a href="https://github.com/iwatkot/pygmdl">pygmdl</a> •
9
- <a href="https://github.com/iwatkot/pydtmdl">pydtmdl</a>
10
- </p>
1
+ <div align="center" markdown>
2
+
3
+ [![Maps4FS](https://img.shields.io/badge/maps4fs-gray?style=for-the-badge)](https://github.com/iwatkot/maps4fs)
4
+ [![PYDTMDL](https://img.shields.io/badge/pydtmdl-blue?style=for-the-badge)](https://github.com/iwatkot/pydtmdl)
5
+ [![PYGDMDL](https://img.shields.io/badge/pygmdl-teal?style=for-the-badge)](https://github.com/iwatkot/pygmdl)
6
+ [![Maps4FS API](https://img.shields.io/badge/maps4fs-api-green?style=for-the-badge)](https://github.com/iwatkot/maps4fsapi)
7
+ [![Maps4FS UI](https://img.shields.io/badge/maps4fs-ui-blue?style=for-the-badge)](https://github.com/iwatkot/maps4fsui)
8
+ [![Maps4FS Data](https://img.shields.io/badge/maps4fs-data-orange?style=for-the-badge)](https://github.com/iwatkot/maps4fsdata)
9
+ [![Maps4FS Upgrader](https://img.shields.io/badge/maps4fs-upgrader-yellow?style=for-the-badge)](https://github.com/iwatkot/maps4fsupgrader)
10
+ [![Maps4FS Stats](https://img.shields.io/badge/maps4fs-stats-red?style=for-the-badge)](https://github.com/iwatkot/maps4fsstats)
11
+ [![Maps4FS Bot](https://img.shields.io/badge/maps4fs-bot-teal?style=for-the-badge)](https://github.com/iwatkot/maps4fsbot)
12
+
13
+ </div>
11
14
 
12
15
  <div align="center" markdown>
13
16
  <a href="https://discord.gg/Sj5QKKyE42">
@@ -6,7 +6,7 @@ from __future__ import annotations
6
6
  import os
7
7
  import shutil
8
8
  from copy import deepcopy
9
- from typing import Any
9
+ from typing import Any, Literal
10
10
 
11
11
  import cv2
12
12
  import numpy as np
@@ -57,6 +57,14 @@ class Background(MeshComponent, ImageComponent):
57
57
  os.makedirs(self.background_directory, exist_ok=True)
58
58
  os.makedirs(self.water_directory, exist_ok=True)
59
59
 
60
+ self.textured_mesh_directory = os.path.join(self.background_directory, "textured_mesh")
61
+ os.makedirs(self.textured_mesh_directory, exist_ok=True)
62
+
63
+ self.assets_background_directory = os.path.join(self.map.assets_directory, "background")
64
+ self.assets_water_directory = os.path.join(self.map.assets_directory, "water")
65
+ os.makedirs(self.assets_background_directory, exist_ok=True)
66
+ os.makedirs(self.assets_water_directory, exist_ok=True)
67
+
60
68
  self.water_resources_path = os.path.join(self.water_directory, "water_resources.png")
61
69
 
62
70
  self.output_path = os.path.join(self.background_directory, f"{Parameters.FULL}.png")
@@ -110,8 +118,28 @@ class Background(MeshComponent, ImageComponent):
110
118
 
111
119
  if self.map.background_settings.generate_background:
112
120
  self.generate_obj_files()
121
+ if self.game.mesh_processing:
122
+ self.logger.debug("Mesh processing is enabled, will decimate, texture and convert.")
123
+ self.decimate_background_mesh()
124
+ self.texture_background_mesh()
125
+ background_conversion_result = self.convert_background_mesh_to_i3d()
126
+ if background_conversion_result:
127
+ self.add_note_file(asset="background")
128
+ else:
129
+ self.logger.warning(
130
+ "Mesh processing is disabled for the game, skipping background mesh processing."
131
+ )
113
132
  if self.map.background_settings.generate_water:
114
133
  self.generate_water_resources_obj()
134
+ if self.game.mesh_processing:
135
+ self.logger.debug("Mesh processing is enabled, will convert water mesh to i3d.")
136
+ water_conversion_result = self.convert_water_mesh_to_i3d()
137
+ if water_conversion_result:
138
+ self.add_note_file(asset="water")
139
+ else:
140
+ self.logger.warning(
141
+ "Mesh processing is disabled for the game, skipping water mesh processing."
142
+ )
115
143
 
116
144
  def create_foundations(self, dem_image: np.ndarray) -> np.ndarray:
117
145
  """Creates foundations for buildings based on the DEM data.
@@ -253,6 +281,257 @@ class Background(MeshComponent, ImageComponent):
253
281
  remove_center=self.map.background_settings.remove_center,
254
282
  )
255
283
 
284
+ @staticmethod
285
+ def get_decimate_factor(map_size: int) -> float:
286
+ """Returns the decimation factor based on the map size.
287
+
288
+ Arguments:
289
+ map_size (int): The size of the map in pixels.
290
+
291
+ Raises:
292
+ ValueError: If the map size is too large for decimation.
293
+
294
+ Returns:
295
+ float -- The decimation factor.
296
+ """
297
+ thresholds = {
298
+ 2048: 0.1,
299
+ 4096: 0.05,
300
+ 8192: 0.025,
301
+ 16384: 0.0125,
302
+ }
303
+ for threshold, factor in thresholds.items():
304
+ if map_size <= threshold:
305
+ return factor
306
+ raise ValueError(
307
+ "Map size is too large for decimation, perform manual decimation in Blender."
308
+ )
309
+
310
+ @staticmethod
311
+ def get_background_texture_resolution(map_size: int) -> int:
312
+ """Returns the background texture resolution based on the map size.
313
+
314
+ Arguments:
315
+ map_size (int): The size of the map in pixels.
316
+
317
+ Returns:
318
+ int -- The background texture resolution.
319
+ """
320
+ resolutions = {
321
+ 2048: 2048,
322
+ 4096: Parameters.MAXIMUM_BACKGROUND_TEXTURE_SIZE,
323
+ 8192: Parameters.MAXIMUM_BACKGROUND_TEXTURE_SIZE,
324
+ 16384: Parameters.MAXIMUM_BACKGROUND_TEXTURE_SIZE,
325
+ }
326
+ for threshold, resolution in resolutions.items():
327
+ if map_size <= threshold:
328
+ return resolution
329
+ return Parameters.MAXIMUM_BACKGROUND_TEXTURE_SIZE
330
+
331
+ def decimate_background_mesh(self) -> None:
332
+ """ ""Decimates the background mesh based on the map size."""
333
+ if not self.assets.background_mesh or not os.path.isfile(self.assets.background_mesh):
334
+ self.logger.warning("Background mesh not found, cannot generate i3d background.")
335
+ return
336
+
337
+ try:
338
+ mesh = trimesh.load_mesh(self.assets.background_mesh, force="mesh")
339
+ except Exception as e:
340
+ self.logger.error("Could not load background mesh: %s", e)
341
+ return
342
+
343
+ try:
344
+ decimate_factor = self.get_decimate_factor(self.map_size)
345
+ except ValueError as e:
346
+ self.logger.error("Could not determine decimation factor: %s", e)
347
+ return
348
+
349
+ decimated_save_path = os.path.join(
350
+ self.background_directory, f"{Parameters.DECIMATED_BACKGROUND}.obj"
351
+ )
352
+ try:
353
+ self.logger.debug("Decimating background mesh with factor %s.", decimate_factor)
354
+ decimated_mesh = self.decimate_mesh(mesh, decimate_factor)
355
+ self.logger.debug("Decimation completed.")
356
+ except Exception as e:
357
+ self.logger.error("Could not decimate background mesh: %s", e)
358
+ return
359
+
360
+ decimated_mesh.export(decimated_save_path)
361
+ self.logger.debug("Decimated background mesh saved: %s", decimated_save_path)
362
+
363
+ self.assets.decimated_background_mesh = decimated_save_path
364
+
365
+ def texture_background_mesh(self) -> None:
366
+ """Textures the background mesh using satellite imagery."""
367
+ satellite_component = self.map.get_satellite_component()
368
+ if not satellite_component:
369
+ self.logger.warning("Satellite component not found, cannot texture background mesh.")
370
+ return
371
+
372
+ background_texture_path = satellite_component.assets.background
373
+
374
+ if not background_texture_path or not os.path.isfile(background_texture_path):
375
+ self.logger.warning("Background texture not found, cannot texture background mesh.")
376
+ return
377
+
378
+ decimated_background_mesh_path = self.assets.decimated_background_mesh
379
+ if not decimated_background_mesh_path or not os.path.isfile(decimated_background_mesh_path):
380
+ self.logger.warning(
381
+ "Decimated background mesh not found, cannot texture background mesh."
382
+ )
383
+ return
384
+
385
+ background_texture_resolution = self.get_background_texture_resolution(self.map_size)
386
+ non_resized_texture_image = cv2.imread(background_texture_path, cv2.IMREAD_UNCHANGED)
387
+
388
+ if non_resized_texture_image is None:
389
+ self.logger.error(
390
+ "Failed to read background texture image: %s", background_texture_path
391
+ )
392
+ return
393
+
394
+ resized_texture_image = cv2.resize(
395
+ non_resized_texture_image, # type: ignore
396
+ (background_texture_resolution, background_texture_resolution),
397
+ interpolation=cv2.INTER_AREA,
398
+ )
399
+
400
+ resized_texture_save_path = os.path.join(
401
+ self.textured_mesh_directory,
402
+ "background_texture.jpg",
403
+ )
404
+
405
+ cv2.imwrite(resized_texture_save_path, resized_texture_image)
406
+ self.logger.debug("Resized background texture saved: %s", resized_texture_save_path)
407
+
408
+ decimated_mesh = trimesh.load_mesh(decimated_background_mesh_path, force="mesh")
409
+
410
+ if decimated_mesh is None:
411
+ self.logger.error("Failed to load decimated mesh after all retry attempts")
412
+ return
413
+
414
+ try:
415
+ obj_save_path, mtl_save_path = self.texture_mesh(
416
+ decimated_mesh,
417
+ resized_texture_save_path,
418
+ output_directory=self.textured_mesh_directory,
419
+ output_name="background_textured_mesh",
420
+ )
421
+
422
+ self.assets.textured_background_mesh = obj_save_path
423
+ self.assets.textured_background_mtl = mtl_save_path
424
+ self.assets.resized_background_texture = resized_texture_save_path
425
+ self.logger.debug("Textured background mesh saved: %s", obj_save_path)
426
+ except Exception as e:
427
+ self.logger.error("Could not texture background mesh: %s", e)
428
+ return
429
+
430
+ def convert_background_mesh_to_i3d(self) -> bool:
431
+ """Converts the textured background mesh to i3d format.
432
+
433
+ Returns:
434
+ bool -- True if the conversion was successful, False otherwise.
435
+ """
436
+ if not self.assets.textured_background_mesh or not os.path.isfile(
437
+ self.assets.textured_background_mesh
438
+ ):
439
+ self.logger.warning("Textured background mesh not found, cannot convert to i3d.")
440
+ return False
441
+
442
+ if not self.assets.resized_background_texture or not os.path.isfile(
443
+ self.assets.resized_background_texture
444
+ ):
445
+ self.logger.warning("Resized background texture not found, cannot convert to i3d.")
446
+ return False
447
+
448
+ try:
449
+ mesh = trimesh.load_mesh(self.assets.textured_background_mesh, force="mesh")
450
+ except Exception as e:
451
+ self.logger.error("Could not load textured background mesh: %s", e)
452
+ return False
453
+
454
+ try:
455
+ i3d_background_terrain = self.mesh_to_i3d(
456
+ mesh,
457
+ output_dir=self.assets_background_directory,
458
+ name=Parameters.BACKGROUND_TERRAIN,
459
+ texture_path=self.assets.resized_background_texture,
460
+ water_mesh=False,
461
+ )
462
+ self.logger.debug(
463
+ "Background mesh converted to i3d successfully: %s", i3d_background_terrain
464
+ )
465
+ self.assets.background_terrain_i3d = i3d_background_terrain
466
+ return True
467
+ except Exception as e:
468
+ self.logger.error("Could not convert background mesh to i3d: %s", e)
469
+ return False
470
+
471
+ def add_note_file(self, asset: Literal["background", "water"]) -> None:
472
+ """Adds a note file to the background or water directory.
473
+
474
+ Arguments:
475
+ asset (Literal["background", "water"]): The asset type to add the note file to.
476
+ """
477
+ filename = "DO_NOT_USE_THESE_FILES.txt"
478
+ note_template = (
479
+ "Please find the ready-to-use {asset} i3d files in the {asset_directory} directory."
480
+ )
481
+ directory = {
482
+ "background": self.background_directory,
483
+ "water": self.water_directory,
484
+ }
485
+
486
+ content = (
487
+ "The files in this directory can be used to create the mesh files manually in Blender. "
488
+ "However, it's recommended to use the ready-to-use i3d files located in the assets "
489
+ "directory. There you'll find the i3d files, that can be imported directly into the "
490
+ "Giants Editor without any additional processing."
491
+ )
492
+ note = note_template.format(
493
+ asset=asset,
494
+ asset_directory=f"assets/{asset}",
495
+ )
496
+
497
+ file_path = os.path.join(directory[asset], filename)
498
+ with open(file_path, "w", encoding="utf-8") as f:
499
+ f.write(content + "\n\n" + note)
500
+
501
+ def convert_water_mesh_to_i3d(self) -> bool:
502
+ """Converts the line-based water mesh to i3d format.
503
+
504
+ Returns:
505
+ bool -- True if the conversion was successful, False otherwise.
506
+ """
507
+ if not self.assets.line_based_water_mesh or not os.path.isfile(
508
+ self.assets.line_based_water_mesh
509
+ ):
510
+ self.logger.warning("Line-based water mesh not found, cannot convert to i3d.")
511
+ return False
512
+
513
+ try:
514
+ mesh = trimesh.load_mesh(self.assets.line_based_water_mesh, force="mesh")
515
+ except Exception as e:
516
+ self.logger.error("Could not load line-based water mesh: %s", e)
517
+ return False
518
+
519
+ try:
520
+ i3d_water_resources = self.mesh_to_i3d(
521
+ mesh,
522
+ output_dir=self.assets_water_directory,
523
+ name=Parameters.WATER_RESOURCES,
524
+ water_mesh=True,
525
+ )
526
+ self.logger.debug(
527
+ "Water resources mesh converted to i3d successfully: %s", i3d_water_resources
528
+ )
529
+ self.assets.water_resources_i3d = i3d_water_resources
530
+ return True
531
+ except Exception as e:
532
+ self.logger.error("Could not convert water mesh to i3d: %s", e)
533
+ return False
534
+
256
535
  def save_map_dem(self, dem_path: str, save_path: str | None = None) -> str:
257
536
  """Cuts out the center of the DEM (the actual map) and saves it as a separate file.
258
537
 
@@ -623,6 +902,8 @@ class Background(MeshComponent, ImageComponent):
623
902
  mesh.export(line_based_save_path)
624
903
  self.logger.debug("Line-based water mesh saved to %s", line_based_save_path)
625
904
 
905
+ self.assets.line_based_water_mesh = line_based_save_path
906
+
626
907
  def mesh_from_3d_polygons(
627
908
  self, polygons: list[shapely.Polygon], single_z_value: int | None = None
628
909
  ) -> Trimesh | None:
@@ -22,12 +22,31 @@ if TYPE_CHECKING:
22
22
 
23
23
 
24
24
  class AttrDict(dict):
25
- """A dictionary that allows attribute-style access to its keys."""
25
+ """A dictionary that allows attribute-style access to its keys.
26
+ Allows safe access to non-existing keys, returning None instead of raising KeyError.
27
+ """
28
+
29
+ def __getattr__(self, name: str) -> Any | None:
30
+ """Returns the value of the given key or None if the key does not exist.
26
31
 
27
- def __getattr__(self, name):
28
- return self[name]
32
+ Arguments:
33
+ name (str): The key to retrieve.
29
34
 
30
- def __setattr__(self, name, value):
35
+ Returns:
36
+ Any | None: The value of the key or None if the key does not exist.
37
+ """
38
+ try:
39
+ return self[name]
40
+ except KeyError:
41
+ return None
42
+
43
+ def __setattr__(self, name: str, value: Any) -> None:
44
+ """Sets the value of the given key.
45
+
46
+ Arguments:
47
+ name (str): The key to set.
48
+ value (Any): The value to set.
49
+ """
31
50
  self[name] = value
32
51
 
33
52
 
@@ -4,6 +4,7 @@ import os
4
4
 
5
5
  import cv2
6
6
  import numpy as np
7
+ from PIL import Image, ImageFile
7
8
 
8
9
  from maps4fs.generator.component.base.component import Component
9
10
  from maps4fs.generator.settings import Parameters
@@ -237,3 +238,35 @@ class ImageComponent(Component):
237
238
  edge_mask = cv2.subtract(bigger_mask, smaller_mask)
238
239
 
239
240
  return self.blur_by_mask(data, edge_mask)
241
+
242
+ @staticmethod
243
+ def convert_png_to_dds(input_png_path: str, output_dds_path: str):
244
+ """Convert a PNG file to DDS format using PIL
245
+
246
+ Arguments:
247
+ input_png_path (str): Path to input PNG file
248
+ output_dds_path (str): Path for output DDS file
249
+
250
+ Raises:
251
+ FileNotFoundError: If the input PNG file does not exist.
252
+ RuntimeError: If the DDS conversion fails.
253
+ """
254
+ if not os.path.exists(input_png_path):
255
+ raise FileNotFoundError(f"Input PNG file not found: {input_png_path}")
256
+
257
+ try:
258
+ ImageFile.LOAD_TRUNCATED_IMAGES = True
259
+
260
+ with Image.open(input_png_path) as img:
261
+ # Convert to RGB if needed (DDS works better with RGB)
262
+ if img.mode == "RGBA":
263
+ # Create RGB version on white background
264
+ rgb_img = Image.new("RGB", img.size, (255, 255, 255))
265
+ rgb_img.paste(img, mask=img.split()[-1]) # Use alpha as mask
266
+ img = rgb_img
267
+ elif img.mode != "RGB":
268
+ img = img.convert("RGB")
269
+
270
+ img.save(output_dds_path, format="DDS")
271
+ except Exception as e:
272
+ raise RuntimeError(f"DDS conversion failed: {e}")