wolfhece 2.2.27__py3-none-any.whl → 2.2.28__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.
wolfhece/PyDraw.py CHANGED
@@ -4300,12 +4300,20 @@ class WolfMapViewer(wx.Frame):
4300
4300
  if self.active_array is not None:
4301
4301
  return self.active_array.mypal
4302
4302
  else:
4303
- return wolfpalette()
4303
+ if self.active_res2d is not None:
4304
+ logging.warning(_('No active array -- Using active 2D result palette instead'))
4305
+ return self.active_res2d.mypal
4306
+ else:
4307
+ return wolfpalette()
4304
4308
  elif act_res2d:
4305
4309
  if self.active_res2d is not None:
4306
4310
  return self.active_res2d.mypal
4307
4311
  else:
4308
- return wolfpalette()
4312
+ if self.active_array is not None:
4313
+ logging.warning(_('No active 2D result -- Using active array palette instead'))
4314
+ return self.active_array.mypal
4315
+ else:
4316
+ return wolfpalette()
4309
4317
  else:
4310
4318
  return wolfpalette()
4311
4319
 
wolfhece/PyPalette.py CHANGED
@@ -278,6 +278,11 @@ class wolfpalette(wx.Frame, LinearSegmentedColormap):
278
278
  logging.warning('No values in palette - Nothing to do !')
279
279
  return None, None
280
280
 
281
+ if len(self.values) ==0:
282
+ logging.warning('No values in palette - Nothing to do !')
283
+ logging.info(_('Do you have defined the palette values ?'))
284
+ logging.info(_('If yes, please check your Global Options. You may not have defined the correct palette to use.'))
285
+ return None, None
281
286
 
282
287
  if fn == '':
283
288
  file = wx.FileDialog(None, "Choose .pal file", wildcard="png (*.png)|*.png|all (*.*)|*.*", style=wx.FD_SAVE)
@@ -288,6 +293,7 @@ class wolfpalette(wx.Frame, LinearSegmentedColormap):
288
293
  fn = file.GetPath()
289
294
 
290
295
  if h_or_v == 'v':
296
+
291
297
  if figax is None:
292
298
  fig, ax = plt.subplots(1, 1)
293
299
  else:
@@ -636,6 +642,18 @@ class wolfpalette(wx.Frame, LinearSegmentedColormap):
636
642
 
637
643
  self.fill_segmentdata()
638
644
 
645
+ def default_difference3(self):
646
+ """ Palette 3 couleurs pour les différences par défaut dans WOLF """
647
+ self.nb = 3
648
+ self.values = np.asarray([-10., 0., 10.], dtype=np.float64)
649
+ self.colors = np.zeros((self.nb, 4), dtype=int)
650
+ self.colorsflt = np.zeros((self.nb, 4), dtype=np.float64)
651
+ self.colors[0, :] = [0, 0, 255, 255] # Bleu
652
+ self.colors[1, :] = [255, 255, 255, 255] # Blanc
653
+ self.colors[2, :] = [255, 0, 0, 255] # Rouge
654
+ self.fill_segmentdata()
655
+
656
+
639
657
  def set_values_colors(self,
640
658
  values: typing.Union[list[float], np.ndarray],
641
659
  colors: typing.Union[list[tuple[int]], np.ndarray]):
@@ -4111,7 +4111,7 @@ class zone:
4111
4111
  else:
4112
4112
  mypl = vector(name=self.active_vector.myname+"_duplicate")
4113
4113
  mypl.myvertices = [wolfvertex(cur.x,cur.y,cur.z) for cur in self.active_vector.myvertices]
4114
- # mypl.nbvertices = self.active_vector.nbvertices # No longer needed
4114
+
4115
4115
 
4116
4116
  if mypl is None:
4117
4117
  return
@@ -4567,7 +4567,6 @@ class zone:
4567
4567
  vr = sublsr.myvertices.copy()
4568
4568
  vr.reverse()
4569
4569
  curvec.myvertices = sublsl.myvertices.copy() + vr
4570
- # curvec.nbvertices = len(curvec.myvertices) FIXME Not needed anymore
4571
4570
  for curv in curvec.myvertices:
4572
4571
  curv.z = smean
4573
4572
  else:
@@ -4631,8 +4630,6 @@ class zone:
4631
4630
  curv.z = smean
4632
4631
  for curv in curvecright.myvertices:
4633
4632
  curv.z = smean
4634
- curvecleft.nbvertices = len(curvecleft.myvertices)
4635
- curvecright.nbvertices = len(curvecright.myvertices)
4636
4633
  else:
4637
4634
  #left poly
4638
4635
  if sublsl.geom_type=='Point':
wolfhece/apps/version.py CHANGED
@@ -5,7 +5,7 @@ class WolfVersion():
5
5
 
6
6
  self.major = 2
7
7
  self.minor = 2
8
- self.patch = 27
8
+ self.patch = 28
9
9
 
10
10
  def __str__(self):
11
11
 
@@ -0,0 +1,496 @@
1
+ import sys
2
+ import wx
3
+ import os
4
+ import platform
5
+
6
+ import pymupdf as pdf
7
+ from tempfile import NamedTemporaryFile
8
+ from tempfile import TemporaryDirectory
9
+
10
+ import matplotlib.pyplot as plt
11
+ import numpy as np
12
+ import logging
13
+ from pathlib import Path
14
+ from datetime import datetime as dt
15
+
16
+ from .. import __version__ as wolfhece_version
17
+ try:
18
+ from wolfgpu.version import __version__ as wolfgpu_version
19
+ except ImportError:
20
+ wolfgpu_version = "not installed"
21
+
22
+ from ..PyVertexvectors import vector, zone, Zones, wolfvertex as wv
23
+ from ..PyTranslate import _
24
+
25
+ def pts2cm(pts):
26
+ """ Convert points to centimeters for PyMuPDF.
27
+
28
+ One point equals 1/72 inches.
29
+ """
30
+ return pts / 28.346456692913385 # 1 point = 1/28.346456692913385 cm = 2.54/72
31
+
32
+ def pt2inches(pts):
33
+ """ Convert points to inches for PyMuPDF.
34
+
35
+ One point equals 1/72 inches.
36
+ """
37
+ return pts / 72.0 # 1 point = 1/72 inches
38
+
39
+ def inches2cm(inches):
40
+ """ Convert inches to centimeters.
41
+
42
+ One inch equals 2.54 centimeters.
43
+ """
44
+ return inches * 2.54 # 1 inch = 2.54 cm
45
+
46
+ def cm2pts(cm):
47
+ """ Convert centimeters to points for PyMuPDF.
48
+
49
+ One point equals 1/72 inches.
50
+ """
51
+ return cm * 28.346456692913385 # 1 cm = 28.346456692913385 points = 72/2.54
52
+
53
+ def cm2inches(cm):
54
+ """ Convert centimeters to inches.
55
+
56
+ One inch equals 2.54 centimeters.
57
+ """
58
+ return cm / 2.54 # 1 cm = 1/2.54 inches
59
+
60
+ def A4_rect():
61
+ """ Return the A4 rectangle in PyMuPDF units.
62
+
63
+ (0, 0) is the top-left corner in PyMuPDF coordinates.
64
+ """
65
+ return pdf.Rect(0, 0, cm2pts(21), cm2pts(29.7)) # A4 size in points (PDF units)
66
+
67
+ def rect_cm(x, y, width, height):
68
+ """ Create a rectangle in PyMuPDF units from centimeters.
69
+
70
+ (0, 0) is the top-left corner in PyMuPDF coordinates.
71
+ """
72
+ return pdf.Rect(cm2pts(x), cm2pts(y), cm2pts(x) + cm2pts(width), cm2pts(y) + cm2pts(height))
73
+
74
+ def get_rect_from_text(text, width, fontsize=10, padding=5):
75
+ """ Get a rectangle that fits the text in PyMuPDF units.
76
+
77
+ :param text: The text to fit in the rectangle.
78
+ :param width: The width of the rectangle in centimeters.
79
+ :param fontsize: The font size in points.
80
+ :param padding: Padding around the text in points.
81
+ :return: A PyMuPDF rectangle that fits the text.
82
+ """
83
+ # Create a temporary PDF document to measure the text size
84
+ with NamedTemporaryFile(delete=True, suffix='.pdf') as temp_pdf:
85
+ doc = pdf.Document()
86
+ page = doc.new_page(A4_rect())
87
+ text_rect = page.insert_text((0, 0), text, fontsize=fontsize, width=cm2pts(width))
88
+ doc.save(temp_pdf.name)
89
+
90
+ # Get the size of the text rectangle
91
+ text_width = text_rect.width + padding * 2
92
+ text_height = text_rect.height + padding * 2
93
+ # Create a rectangle with the specified width and height
94
+ rect = pdf.Rect(0, 0, cm2pts(width), text_height)
95
+ # Adjust the rectangle to fit the text
96
+ rect.x0 -= padding
97
+ rect.y0 -= padding
98
+ rect.x1 += padding
99
+ rect.y1 += padding
100
+ return rect
101
+
102
+
103
+ def list_to_html(list_items, font_size="10pt", font_family="Helvetica"):
104
+ # Génère le CSS
105
+ css = f"""
106
+ p {{font-size:{font_size};
107
+ font-family:{font_family};
108
+ color:#BEBEBE;
109
+ align-text:center}}
110
+
111
+ ul.list {{
112
+ font-size: {font_size};
113
+ font-family: {font_family};
114
+ color: #2C3E50;
115
+ padding-left: 20px;
116
+ }}
117
+ li {{
118
+ margin-bottom: 5px;
119
+ }}
120
+ """
121
+
122
+ # Génère le HTML
123
+ html = "<ul class='list'>"
124
+ for item in list_items:
125
+ html += f" <li>{item}</li>\n"
126
+ html += "</ul>"
127
+
128
+ css = css.replace('\n', ' ') # Remove newlines in CSS for better readability
129
+
130
+ return html, css
131
+
132
+
133
+ def list_to_html_aligned(list_items, font_size="10pt", font_family="Helvetica"):
134
+ # Génère le CSS
135
+ css = f"""
136
+ p {{font-size:{font_size};
137
+ font-family:{font_family};
138
+ color:#BEBEBE;
139
+ align-text:left}}
140
+
141
+ div.list {{
142
+ font-size: {font_size};
143
+ font-family: {font_family};
144
+ color: #2C3E50;
145
+ padding-left: 8px;
146
+ align-text:left
147
+ }}
148
+ li {{
149
+ margin-bottom: 5px;
150
+ }}
151
+ """
152
+
153
+ # Génère le HTML
154
+ html = "<div class='list'>"
155
+ html += " - ".join(list_items) # Join the items with a hyphen
156
+ html += "</div>"
157
+
158
+ css = css.replace('\n', ' ') # Remove newlines in CSS for better readability
159
+
160
+ return html, css
161
+
162
+ # A4 format
163
+ PAGE_WIDTH = 21 # cm
164
+ PAGE_HEIGHT = 29.7 # cm
165
+
166
+ # Default Powerpoint 16:9 slide dimensions
167
+ SLIDE_HEIGHT = inches2cm(7.5) # cm
168
+ SLIDE_WIDTH = inches2cm(13.3333) # cm
169
+
170
+ class DefaultLayoutA4(Zones):
171
+ """
172
+ Enum for default layout options.
173
+ """
174
+
175
+ def __init__(self, title:str, filename = '', ox = 0, oy = 0, tx = 0, ty = 0, parent=None, is2D=True, idx = '', plotted = True, mapviewer=None, need_for_wx = False, bbox = None, find_minmax = True, shared = False, colors = None):
176
+ super().__init__(filename, ox, oy, tx, ty, parent, is2D, idx, plotted, mapviewer, need_for_wx, bbox, find_minmax, shared, colors)
177
+
178
+ self.title = title
179
+
180
+ self.left_right_margin = 1 # cm
181
+ self.top_bottom_margin = 0.5 # cm
182
+ self.padding = 0.5 # cm
183
+
184
+ WIDTH_TITLE = 16 # cm
185
+ HEIGHT_TITLE = 1.5 # cm
186
+
187
+ WIDTH_VERSIONS = 16 # cm
188
+ HEIGHT_VERSIONS = .5 # cm
189
+
190
+ X_LOGO = 18.5 # Logo starts after the title and versions
191
+ WIDTH_LOGO = 1.5 # cm
192
+ HEIGHT_LOGO = 1.5 # cm
193
+
194
+ HEIGHT_FOOTER = 1.2 # cm
195
+
196
+ page = zone(name='Page')
197
+ elts = zone(name='Elements')
198
+
199
+ self.add_zone(page, forceparent= True)
200
+ self.add_zone(elts, forceparent= True)
201
+
202
+ vec_page = vector(name=_("Page"))
203
+ vec_page.add_vertex(wv(0, 0))
204
+ vec_page.add_vertex(wv(PAGE_WIDTH, 0))
205
+ vec_page.add_vertex(wv(PAGE_WIDTH, PAGE_HEIGHT))
206
+ vec_page.add_vertex(wv(0, PAGE_HEIGHT))
207
+ vec_page.force_to_close()
208
+ page.add_vector(vec_page, forceparent=True)
209
+
210
+ vec_title = vector(name=_("Title"))
211
+ y_from_top = PAGE_HEIGHT - self.top_bottom_margin
212
+ vec_title.add_vertex(wv(self.left_right_margin, y_from_top))
213
+ vec_title.add_vertex(wv(self.left_right_margin + WIDTH_TITLE, y_from_top))
214
+ vec_title.add_vertex(wv(self.left_right_margin + WIDTH_TITLE, y_from_top - HEIGHT_TITLE))
215
+ vec_title.add_vertex(wv(self.left_right_margin, y_from_top - HEIGHT_TITLE))
216
+ vec_title.force_to_close()
217
+ vec_title.set_legend_text(_("Title of the report"))
218
+ vec_title.set_legend_position_to_centroid()
219
+ vec_title.myprop.legendvisible = True
220
+ vec_title.find_minmax()
221
+ elts.add_vector(vec_title, forceparent=True)
222
+
223
+ vec_versions = vector(name=_("Versions"))
224
+ y_from_top = PAGE_HEIGHT - self.top_bottom_margin - HEIGHT_TITLE - self.padding
225
+ vec_versions.add_vertex(wv(self.left_right_margin, y_from_top))
226
+ vec_versions.add_vertex(wv(self.left_right_margin + WIDTH_VERSIONS, y_from_top))
227
+ vec_versions.add_vertex(wv(self.left_right_margin + WIDTH_VERSIONS, y_from_top - HEIGHT_VERSIONS))
228
+ vec_versions.add_vertex(wv(self.left_right_margin, y_from_top - HEIGHT_VERSIONS))
229
+ vec_versions.force_to_close()
230
+ vec_versions.set_legend_text(_("Versions of the software"))
231
+ vec_versions.set_legend_position_to_centroid()
232
+ vec_versions.myprop.legendvisible = True
233
+ vec_versions.find_minmax()
234
+ elts.add_vector(vec_versions, forceparent=True)
235
+
236
+ vec_logo = vector(name=_("Logo"))
237
+ # Logo is placed at the top right corner, after the title and versions
238
+ # Adjust the position based on the logo size
239
+ y_from_top = PAGE_HEIGHT - self.top_bottom_margin
240
+ vec_logo.add_vertex(wv(X_LOGO, y_from_top))
241
+ vec_logo.add_vertex(wv(X_LOGO + WIDTH_LOGO, y_from_top))
242
+ vec_logo.add_vertex(wv(X_LOGO + WIDTH_LOGO, y_from_top - HEIGHT_LOGO))
243
+ vec_logo.add_vertex(wv(X_LOGO, y_from_top - HEIGHT_LOGO))
244
+ vec_logo.force_to_close()
245
+ vec_logo.set_legend_text(_("Logo"))
246
+ vec_logo.set_legend_position_to_centroid()
247
+ vec_logo.myprop.legendvisible = True
248
+ vec_logo.find_minmax()
249
+ elts.add_vector(vec_logo, forceparent=True)
250
+
251
+ vec_footer = vector(name=_("Footer"))
252
+ vec_footer.add_vertex(wv(self.left_right_margin, self.top_bottom_margin))
253
+ vec_footer.add_vertex(wv(PAGE_WIDTH - self.left_right_margin, self.top_bottom_margin))
254
+ vec_footer.add_vertex(wv(PAGE_WIDTH - self.left_right_margin, self.top_bottom_margin + HEIGHT_FOOTER))
255
+ vec_footer.add_vertex(wv(self.left_right_margin, self.top_bottom_margin + HEIGHT_FOOTER))
256
+ vec_footer.force_to_close()
257
+ vec_footer.set_legend_text(_("Footer of the report"))
258
+ vec_footer.set_legend_position_to_centroid()
259
+ vec_footer.myprop.legendvisible = True
260
+ vec_footer.find_minmax()
261
+ elts.add_vector(vec_footer, forceparent=True)
262
+
263
+ self._layout = {}
264
+ self._doc = None # Placeholder for the PDF document
265
+ self._pdf_path = None # Placeholder for the PDF file path
266
+
267
+ def add_element(self, name:str, width:float, height:float, x:float = 0, y:float = 0) -> vector:
268
+ """
269
+ Add an element to the layout.
270
+ """
271
+ vec = vector(name=name)
272
+ vec.add_vertex(wv(x, y))
273
+ vec.add_vertex(wv(x + width, y))
274
+ vec.add_vertex(wv(x + width, y + height))
275
+ vec.add_vertex(wv(x, y + height))
276
+ vec.force_to_close()
277
+ vec.find_minmax()
278
+ vec.set_legend_text(name)
279
+ vec.set_legend_position_to_centroid()
280
+ vec.myprop.legendvisible = True
281
+
282
+ self['Elements'].add_vector(vec, forceparent=True)
283
+
284
+ return vec
285
+
286
+ def add_element_repeated(self, name:str, width:float, height:float,
287
+ first_x:float = 0, first_y:float = 0,
288
+ count_x:int = 1, count_y:int = 1,
289
+ padding:float = None) -> zone:
290
+
291
+ if padding is None:
292
+ padding = self.padding
293
+
294
+ delta_x = width + padding if count_x > 0 else -(padding + width)
295
+ delta_y = height + padding if count_y > 0 else -(padding + height)
296
+
297
+ count_x = abs(count_x)
298
+ count_y = abs(count_y)
299
+
300
+ x = first_x
301
+ y = first_y if delta_y > 0 else first_y - height
302
+
303
+ elements = zone(name=name + '_elements')
304
+ for j in range(count_y):
305
+ for i in range(count_x):
306
+ elements.add_vector(self.add_element(name + f"_{i}-{j}", width, height, x, y), forceparent=False)
307
+ x += delta_x
308
+ x = first_x
309
+ y += delta_y
310
+
311
+ elements.find_minmax()
312
+
313
+ return elements
314
+
315
+
316
+ def check_if_overlap(self, vec:vector) -> bool:
317
+ """
318
+ Check if the vector overlaps with any existing vector in the layout.
319
+ """
320
+ for existing_vec in self['Elements'].myvectors:
321
+ if vec.linestring.overlaps(existing_vec.linestring):
322
+ return True
323
+ return False
324
+
325
+ @property
326
+ def useful_part(self) -> vector:
327
+ """
328
+ Get the useful part of the page, excluding margins.
329
+ """
330
+ vec = self[('Page', _('Page'))]
331
+ vec.find_minmax()
332
+
333
+ version = self[('Elements', _('Versions'))]
334
+ version.find_minmax()
335
+
336
+ footer = self[('Elements', _('Footer'))]
337
+ footer.find_minmax()
338
+
339
+ useful_part = vector(name=_("Useful part of the page"))
340
+ useful_part.add_vertex(wv(vec.xmin + self.left_right_margin, version.ymin - self.padding))
341
+ useful_part.add_vertex(wv(vec.xmax - self.left_right_margin, version.ymin - self.padding))
342
+ useful_part.add_vertex(wv(vec.xmax - self.left_right_margin, footer.ymax + self.padding))
343
+ useful_part.add_vertex(wv(vec.xmin + self.left_right_margin, footer.ymax + self.padding))
344
+ useful_part.force_to_close()
345
+
346
+ useful_part.find_minmax()
347
+ useful_part.set_legend_text(_("Useful part of the page"))
348
+ useful_part.set_legend_position_to_centroid()
349
+ useful_part.myprop.legendvisible = True
350
+
351
+ return useful_part
352
+
353
+ @property
354
+ def page_dimension(self) -> tuple[float, float]:
355
+ """
356
+ Get the dimensions of the page in centimeters.
357
+ """
358
+ vec = self[('Page', _('Page'))]
359
+ vec.find_minmax()
360
+ width = vec.xmax - vec.xmin
361
+ height = vec.ymax - vec.ymin
362
+ return width, height
363
+
364
+ @property
365
+ def keys(self) -> list[str]:
366
+ """
367
+ Get the keys of the layout.
368
+ """
369
+ return [vec.myname for vec in self['Elements'].myvectors]
370
+
371
+ def to_dict(self):
372
+ """
373
+ Convert the layout Zones to a dictionary.
374
+ """
375
+ layout = {}
376
+
377
+ for vec in self['Elements'].myvectors:
378
+ vec.find_minmax()
379
+ layout[vec.myname] = rect_cm(vec.xmin, PAGE_HEIGHT - vec.ymax, vec.xmax - vec.xmin, vec.ymax - vec.ymin)
380
+
381
+ def plot(self, scale=1.):
382
+ """
383
+ Plot the layout using matplotlib.
384
+ :param scale: Scale factor for the plot.
385
+ """
386
+ fig, ax = plt.subplots(figsize=(cm2inches(PAGE_WIDTH) * scale, cm2inches(PAGE_HEIGHT)*scale))
387
+
388
+ self['Elements'].plot_matplotlib(ax = ax)
389
+ ax.set_aspect('equal')
390
+
391
+ ax.set_xlim(0, PAGE_WIDTH)
392
+ ax.set_ylim(0, PAGE_HEIGHT)
393
+
394
+ ax.set_yticks(list(np.arange(0, PAGE_HEIGHT, 1))+[PAGE_HEIGHT])
395
+ ax.set_xticks(list(np.arange(0, PAGE_WIDTH + 1, 1)))
396
+
397
+ plt.title(_("Layout of the report"))
398
+ plt.xlabel(_("Width (cm)"))
399
+ plt.ylabel(_("Height (cm)"))
400
+ # plt.grid(True)
401
+
402
+ return fig, ax
403
+
404
+ def _create_layout_pdf(self) -> dict:
405
+ """
406
+ Create the layout dictionary for the report.
407
+ :return: A dictionary with layout information.
408
+ """
409
+ for vec in self['Elements'].myvectors:
410
+ vec.find_minmax()
411
+ self._layout[vec.myname] = rect_cm(vec.xmin, PAGE_HEIGHT - vec.ymax, vec.xmax - vec.xmin, vec.ymax - vec.ymin)
412
+
413
+ return self._layout
414
+
415
+ def _summary_versions(self):
416
+ """ Find the versions of the simulation, wolfhece and the wolfgpu package """
417
+ import json
418
+
419
+ group_title = "Versions"
420
+ text = [f"Wolfhece : {wolfhece_version}",
421
+ f"Wolfgpu : {wolfgpu_version}",
422
+ f"Python : {sys.version.split()[0]}",
423
+ f"Operating System: {os.name}"
424
+ ]
425
+
426
+ return group_title, text
427
+
428
+ def _insert_to_page(self, page: pdf.Page):
429
+
430
+ layout = self._create_layout_pdf()
431
+
432
+ page.insert_htmlbox(layout['Title'], f"<h1>{self.title}</h1>",
433
+ css='h1 {font-size:16pt; font-family:Helvetica; color:#333}')
434
+
435
+ # versions box
436
+ try:
437
+ text = self._summary_versions()
438
+ html, css = list_to_html_aligned(text[1], font_size="10pt", font_family="Helvetica")
439
+ spare_height, scale = page.insert_htmlbox(layout['Versions'], html, css=css, scale_low = 0.1)
440
+
441
+ if spare_height < 0.:
442
+ logging.warning("Text overflow in versions box. Adjusting scale.")
443
+ except:
444
+ logging.error("Failed to insert versions text. Using fallback method.")
445
+
446
+ rect = layout['Logo']
447
+ # Add the logo to the top-right corner
448
+ logo_path = Path(__file__).parent / 'wolf_report.png'
449
+ if logo_path.exists():
450
+ page.insert_image(rect, filename=str(logo_path),
451
+ keep_proportion=True,
452
+ overlay=True)
453
+
454
+ # Footer
455
+ # ------
456
+ # Insert the date and time of the report generation, the user and the PC name
457
+ footer_rect = layout['Footer']
458
+ footer_text = f"<p>Report generated on {dt.now()} by {os.getlogin()} on {platform.uname().node} - {platform.uname().machine} - {platform.uname().release} - {platform.uname().version}</br> \
459
+ This report does not guarantee the quality of the model and in no way commits the software developers.</p>"
460
+ page.insert_htmlbox(footer_rect, footer_text,
461
+ css='p {font-size:10pt; font-family:Helvetica; color:#BEBEBE; align-text:center}',)
462
+
463
+ def create_report(self) -> pdf.Document:
464
+ """ Create the PDF report for the default LayoutA4. """
465
+
466
+ # Create a new PDF document
467
+ self._doc = pdf.Document()
468
+
469
+ # Add a page
470
+ self._page = self._doc.new_page()
471
+
472
+ # Insert the layout into the page
473
+ self._insert_to_page(self._page)
474
+
475
+ return self._doc
476
+
477
+ def save_report(self, output_path: Path | str):
478
+ """ Save the report to a PDF file """
479
+
480
+ if self._doc is None:
481
+ self.create_report()
482
+
483
+ try:
484
+ self._doc.subset_fonts()
485
+ self._doc.save(output_path, garbage=3, deflate=True)
486
+ self._pdf_path = output_path
487
+ except Exception as e:
488
+ logging.error(f"Failed to save the report to {output_path}: {e}")
489
+ logging.error("Please check if the file is already opened.")
490
+ self._pdf_path = None
491
+ return
492
+
493
+ @property
494
+ def pdf_path(self):
495
+ """ Return the PDF document """
496
+ return self._pdf_path