rephorm 1.0.1__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.
Files changed (73) hide show
  1. rephorm/__init__.py +19 -0
  2. rephorm/decorators/__init__.py +0 -0
  3. rephorm/decorators/settings_validation.py +49 -0
  4. rephorm/dict/__init__.py +1 -0
  5. rephorm/dict/colors.py +22 -0
  6. rephorm/dict/legend_positions.py +136 -0
  7. rephorm/dict/styles.py +206 -0
  8. rephorm/dict/update_layout.py +129 -0
  9. rephorm/dict/update_traces.py +90 -0
  10. rephorm/object_mappers/__init__.py +0 -0
  11. rephorm/object_mappers/_globals.py +16 -0
  12. rephorm/object_mappers/_utilities/__init__.py +0 -0
  13. rephorm/object_mappers/_utilities/chart_mapper_utility.py +34 -0
  14. rephorm/object_mappers/_utilities/grid_mapper_utility.py +11 -0
  15. rephorm/object_mappers/_utilities/table_mapper_utility.py +59 -0
  16. rephorm/object_mappers/chapter_mapper.py +82 -0
  17. rephorm/object_mappers/chart_mapper.py +120 -0
  18. rephorm/object_mappers/chart_series_mapper.py +52 -0
  19. rephorm/object_mappers/grid_mapper.py +106 -0
  20. rephorm/object_mappers/page_break_mapper.py +8 -0
  21. rephorm/object_mappers/report_mapper.py +76 -0
  22. rephorm/object_mappers/table_mapper.py +191 -0
  23. rephorm/object_mappers/table_section_mapper.py +41 -0
  24. rephorm/object_mappers/table_series_mapper.py +79 -0
  25. rephorm/object_mappers/text_mapper.py +36 -0
  26. rephorm/object_params/__init__.py +0 -0
  27. rephorm/object_params/settings.py +184 -0
  28. rephorm/objects/__init__.py +0 -0
  29. rephorm/objects/_utilities/__init__.py +0 -0
  30. rephorm/objects/_utilities/settings_container.py +8 -0
  31. rephorm/objects/chapter.py +44 -0
  32. rephorm/objects/chart.py +70 -0
  33. rephorm/objects/chart_series.py +40 -0
  34. rephorm/objects/footnote.py +13 -0
  35. rephorm/objects/grid.py +57 -0
  36. rephorm/objects/page_break.py +13 -0
  37. rephorm/objects/report.py +196 -0
  38. rephorm/objects/table.py +76 -0
  39. rephorm/objects/table_section.py +48 -0
  40. rephorm/objects/table_series.py +36 -0
  41. rephorm/objects/text.py +35 -0
  42. rephorm/utility/PDF.py +80 -0
  43. rephorm/utility/__init__.py +0 -0
  44. rephorm/utility/add_style_prefix.py +30 -0
  45. rephorm/utility/fonts/OpenSans-Bold.ttf +0 -0
  46. rephorm/utility/fonts/OpenSans-BoldItalic.ttf +0 -0
  47. rephorm/utility/fonts/OpenSans-Italic.ttf +0 -0
  48. rephorm/utility/fonts/OpenSans.ttf +0 -0
  49. rephorm/utility/fonts/__init__.py +0 -0
  50. rephorm/utility/fonts/font_loading.py +74 -0
  51. rephorm/utility/is_set.py +9 -0
  52. rephorm/utility/merge/__init__.py +0 -0
  53. rephorm/utility/merge/chart_properties_manager.py +80 -0
  54. rephorm/utility/merge/merge_settings.py +134 -0
  55. rephorm/utility/merge/merge_styles.py +79 -0
  56. rephorm/utility/merge/pdf_merger.py +29 -0
  57. rephorm/utility/report/__init__.py +0 -0
  58. rephorm/utility/report/add_footnotes.py +46 -0
  59. rephorm/utility/report/cleanup_utility.py +19 -0
  60. rephorm/utility/report/footnotes_counter.py +15 -0
  61. rephorm/utility/report/image_utility.py +14 -0
  62. rephorm/utility/report/layout_utility.py +59 -0
  63. rephorm/utility/report/range_utility.py +94 -0
  64. rephorm/utility/report/report_utility.py +93 -0
  65. rephorm/utility/report/resolve_color.py +39 -0
  66. rephorm/utility/report/table_utility.py +71 -0
  67. rephorm/utility/report/title_utility.py +49 -0
  68. rephorm/utility/unit_converter.py +12 -0
  69. rephorm-1.0.1.dist-info/METADATA +41 -0
  70. rephorm-1.0.1.dist-info/RECORD +73 -0
  71. rephorm-1.0.1.dist-info/WHEEL +5 -0
  72. rephorm-1.0.1.dist-info/licenses/LICENSE +21 -0
  73. rephorm-1.0.1.dist-info/top_level.txt +1 -0
File without changes
@@ -0,0 +1,34 @@
1
+ import numpy as np
2
+
3
+ def process_bar_data(neg_data, pos_data, flattened):
4
+ """
5
+ Processes a flattened array by slicing it into chunks if its length differs
6
+ from the target data arrays, then applies np.where per chunk and accumulates
7
+ the negative or positive values into the corresponding data arrays.
8
+
9
+ Args:
10
+ neg_data (np.ndarray): negative data array.
11
+ pos_data (np.ndarray): positive data array.
12
+ flattened (np.ndarray): Flattened input array. (The actual series data)
13
+
14
+ Returns:
15
+ Tuple[np.ndarray, np.ndarray]: Updated negative and positive data arrays.
16
+ """
17
+ target_length = neg_data.shape[0]
18
+ total_length = flattened.shape[0]
19
+
20
+ # If equal, direct add
21
+ if total_length == target_length:
22
+ neg_data += np.where(flattened < 0, flattened, 0)
23
+ pos_data += np.where(flattened > 0, flattened, 0)
24
+
25
+ # If longer, break into chunks
26
+ else:
27
+ num_full_chunks = total_length // target_length
28
+
29
+ for i in range(num_full_chunks):
30
+ chunk = flattened[i * target_length : (i + 1) * target_length]
31
+ neg_data += np.where(chunk < 0, chunk, 0)
32
+ pos_data += np.where(chunk > 0, chunk, 0)
33
+
34
+ return neg_data, pos_data
@@ -0,0 +1,11 @@
1
+ def compute_grid_layout(nrow = 1, ncol = 1):
2
+ """
3
+ Computes the layout of a grid based on the number of rows and columns.
4
+ """
5
+ layout = []
6
+ for indx in range(nrow):
7
+ row_layout = []
8
+ for indx in range(ncol):
9
+ row_layout.append({})
10
+ layout.append(row_layout)
11
+ return layout
@@ -0,0 +1,59 @@
1
+ #TODO: extract this function to external module.
2
+ from typing import Literal
3
+ from rephorm.objects.table_section import TableSection
4
+ from rephorm.objects.table_series import TableSeries
5
+
6
+ # Adjustment for text width, because fpdf does not calculate it correctly
7
+ # Todo: Investigate why, and if it is possible to fix it
8
+ magic_number = 10
9
+
10
+ def get_max_numerical_width(table, pdf, font_family, font_size, biggest_nr, table_element=None, max_width = 0):
11
+ """
12
+ Determine the maximum width needed for numerical values in the table.
13
+ Ensures width does not exceed max_width.
14
+
15
+ How it works: Finds the longest formatted numerical string within the whole table
16
+ """
17
+ pdf.set_font(font_family, size=font_size)
18
+ max_cap = pdf.get_string_width(str(biggest_nr)) + magic_number
19
+
20
+ table_element = table_element if table_element else table
21
+
22
+ for child in table_element.CHILDREN:
23
+ if isinstance(child, TableSeries):
24
+ series_data = child.data.get_values(child.settings.span)
25
+ for value in series_data:
26
+ formatted_value = f"{value:.{child.settings.decimal_precision}f}"
27
+ text_width = pdf.get_string_width(str(formatted_value)) + magic_number
28
+ if text_width > max_width:
29
+ max_width = min(text_width, max_cap)
30
+
31
+ elif isinstance(child, TableSection):
32
+ section_max_width = get_max_numerical_width(table, pdf, font_family, font_size, biggest_nr, child, max_width)
33
+ max_width = max(max_width, section_max_width)
34
+
35
+ return max_width
36
+
37
+ def get_max_width(table, pdf, font_family, font_size, table_element = None, parameter: Literal["title", "unit"] = "unit", max_width = 0):
38
+
39
+ pdf.set_font(font_family, size=font_size)
40
+
41
+ table_element = table_element if table_element else table
42
+
43
+ for child in table_element.CHILDREN:
44
+
45
+ if isinstance(child, TableSeries):
46
+ attr_value = getattr(child, parameter)
47
+ if attr_value is not None:
48
+ text_width = pdf.get_string_width(attr_value) + magic_number
49
+ if text_width > max_width:
50
+ max_width = text_width
51
+
52
+ elif isinstance(child, TableSection):
53
+ if parameter == "title":
54
+ section_title_width = pdf.get_string_width(child.TITLE) + magic_number
55
+ if section_title_width > max_width:
56
+ max_width = section_title_width
57
+ section_max_width = get_max_width(table, pdf, font_family, font_size, child, parameter)
58
+ max_width = max(max_width, section_max_width)
59
+ return max_width
@@ -0,0 +1,82 @@
1
+ from rephorm.utility.report.add_footnotes import add_footnotes_at_bottom
2
+ from rephorm.utility.report.footnotes_counter import generate_footnote_numbers
3
+ from rephorm.utility.report.layout_utility import LayoutManager
4
+ from rephorm.utility.report.title_utility import add_title_with_references
5
+
6
+
7
+ class ChapterMapper:
8
+ def __init__(self, chapter):
9
+ self.chapter = chapter
10
+
11
+ # Todo: Reconsider add_to_pdf method naming. it would make more sense "render" or "map" because we do
12
+ # add things to the pdf and then go through the children here and eventually render them too...
13
+ def add_to_pdf(self, **kwargs):
14
+
15
+ pdf = kwargs["pdf"]
16
+ footer_last_y = kwargs["footer_last_y"] if "footer_last_y" in kwargs else None
17
+ starting_page = pdf.page_no()
18
+ n_children = len(self.chapter.CHILDREN)
19
+
20
+
21
+ rs = self.chapter.settings.styles["report"]
22
+ used_top_height = rs["header"]["height"] + rs["header"]["bottom_margin"]
23
+ footer_top_padding = rs["footer"]["top_padding"]
24
+ footer_top_margin = rs["footer"]["top_margin"]
25
+
26
+ pdf.set_y(used_top_height)
27
+
28
+ pdf.set_font(
29
+ family=self.chapter.settings.styles["chapter"]["font_family"],
30
+ size=self.chapter.settings.styles["chapter"]["font_size"],
31
+ style=self.chapter.settings.styles["chapter"]["font_style"],
32
+ )
33
+
34
+ if self.chapter.title is not None:
35
+ footnote_references = generate_footnote_numbers(self.chapter.footnotes)
36
+ title_y_pos = pdf.h / 2 - pdf.font_size / 2 # Todo: consider multiline title in the future (would need to calculate h of the text cell)
37
+ add_title_with_references(pdf, self.chapter.title, footnote_references,
38
+ font_family=self.chapter.settings.styles["chapter"]["title"]["font_family"],
39
+ font_size=self.chapter.settings.styles["chapter"]["title"]["font_size"],
40
+ font_style=self.chapter.settings.styles["chapter"]["title"]["font_style"],
41
+ title_y=title_y_pos, cell_width=pdf.epw)
42
+ if n_children > 0:
43
+ pdf.add_page()
44
+
45
+ if bool(self.chapter.footnotes):
46
+ footer_last_y = add_footnotes_at_bottom(
47
+ pdf,
48
+ self.chapter.footnotes,
49
+ footnote_references,
50
+ last_y=footer_last_y,
51
+ font_family=self.chapter.settings.styles["footnotes"]["font_family"],
52
+ font_size=self.chapter.settings.styles["footnotes"]["font_size"],
53
+ font_style=self.chapter.settings.styles["footnotes"]["font_style"],
54
+ footer_padding=footer_top_padding,
55
+ footer_top_margin=footer_top_margin,
56
+ )
57
+
58
+ layout_manager = LayoutManager(
59
+ pdf,
60
+ used_top_height=used_top_height,
61
+ )
62
+
63
+ for i, item in enumerate(self.chapter.CHILDREN):
64
+
65
+ x, y, width, height = layout_manager.get_default_layout()
66
+
67
+ current_page_nr = pdf.page_no()
68
+ if current_page_nr > starting_page:
69
+ starting_page = current_page_nr
70
+ # footer_last_y = None
71
+
72
+ mapper = item._get_mapper()
73
+
74
+ # TODO: IMPORTANT: Review the design and implement everything in a way where we pass single Y position
75
+ # across the object mappers, so we do not need Y - to set the position of chapter elements/objects,
76
+ # then title_y to correctly set the titles. We should pass Y along, and render elements/titles from the
77
+ # LAST known Y position (which is the last element rendered). This way we can avoid the need for
78
+ # title_y and footer_last_y, and god knows how many more Y's :D
79
+ mapper.add_to_pdf(pdf=pdf, x=x, y=y, w=width, h=height, footer_last_y=footer_last_y)
80
+
81
+ if i < n_children - 1:
82
+ pdf.add_page()
@@ -0,0 +1,120 @@
1
+ import os
2
+
3
+ import irispie
4
+ import numpy as np
5
+ from plotly.graph_objects import Figure
6
+
7
+ from rephorm.object_mappers._globals import set_figure_map
8
+ from rephorm.object_mappers._utilities.chart_mapper_utility import process_bar_data
9
+ from rephorm.utility.report.image_utility import to_image
10
+ from rephorm.dict.update_layout import update_layout
11
+ from rephorm.utility.report.resolve_color import resolve_highlight_color
12
+
13
+
14
+ class ChartMapper:
15
+
16
+ def __init__(self, chart):
17
+ self.chart = chart
18
+ self.figure = Figure()
19
+ self.apply_report_layout = chart.settings.apply_report_layout
20
+
21
+ def add_to_pdf(self, **kwargs):
22
+ grid_size = kwargs.get("grid_size", 1)
23
+ pdf = kwargs["pdf"]
24
+ x = kwargs["x"]
25
+ y = kwargs["y"]
26
+ w = kwargs["w"]
27
+ h = kwargs["h"]
28
+
29
+ scale_f = pdf.k
30
+
31
+ img_w = w * scale_f
32
+ img_h = h * scale_f
33
+
34
+ if grid_size > 9:
35
+ img_w = None
36
+ img_h = None
37
+
38
+ # Todo: Directory creation and check, should be extracted from here, too many concerns...
39
+ directory = "./tmp/pdf_figures"
40
+ os.makedirs(directory, exist_ok=True)
41
+
42
+ self.figure = self.chart.figure or self._add_internal_figure()
43
+
44
+ if self.apply_report_layout and bool(self.chart.figure):
45
+ update_layout(self.chart.figure.layout.title.text, self.chart.settings, self.figure)
46
+
47
+ data = to_image(self.figure, image_format="pdf", width=img_w, height=img_h)
48
+
49
+ if data is None:
50
+ raise ValueError(f"ChartMapper: Failed to render chart image. Data is: {data}")
51
+
52
+ file_path = f"{directory}/{id(self.chart)}.pdf"
53
+ with open(file_path, "wb") as f:
54
+ f.write(data)
55
+
56
+ fig_map = ({"page": pdf.page_no(),
57
+ "pdf_path": file_path,
58
+ "x": x * scale_f,
59
+ "y": y * scale_f,
60
+ "width": w * scale_f,
61
+ "height": h * scale_f})
62
+
63
+ set_figure_map(fig_map)
64
+
65
+ def _add_internal_figure(self):
66
+
67
+ min_y = None
68
+ max_y = None
69
+
70
+ span = self.chart._get_span()
71
+ span_length = len(span)
72
+ neg_stacked_bars_data = np.zeros(span_length)
73
+ pos_stacked_bars_data = np.zeros(span_length)
74
+ stacked_bars = False
75
+
76
+ for item in self.chart.CHILDREN:
77
+ for variant in range(1, item.data.num_variants + 1):
78
+ y_values = item.data.get_data_variant_from_until(span, variant).flatten()
79
+ if item.settings.series_type in ("barcon", "conbar", "contribution_bar", "bar_relative"):
80
+ stacked_bars = True
81
+ neg_stacked_bars_data, pos_stacked_bars_data = process_bar_data(
82
+ neg_stacked_bars_data,
83
+ pos_stacked_bars_data,
84
+ y_values
85
+ )
86
+ else:
87
+ # Direct min/max calculation for non-barcon series
88
+ current_min = y_values.min()
89
+ current_max = y_values.max()
90
+
91
+ min_y = current_min if min_y is None else min(min_y, current_min)
92
+ max_y = current_max if max_y is None else max(max_y, current_max)
93
+
94
+ series_mapper = item._get_mapper()
95
+ series_mapper.add_to_pdf(
96
+ figure=self.figure) # Todo: This add_to_pdf call is confusing, yes, we wanted to unify function names, but in this case it does not make sense.
97
+
98
+ if stacked_bars:
99
+ min_y = min(neg_stacked_bars_data) if min_y is None else min(min_y, min(neg_stacked_bars_data))
100
+ max_y = max(pos_stacked_bars_data) if max_y is None else max(max_y, max(pos_stacked_bars_data))
101
+
102
+ dy = max_y - min_y
103
+ y_range = [min_y - dy * 0.01, max_y + dy * 0.01]
104
+
105
+ update_layout(self.chart.title, self.chart.settings, self.figure, y_range)
106
+
107
+ # Adjusts view SPAN on charts. "Slices" any padding between.
108
+ # self.figure.layout.xaxis.range = [span.start.to_python_date(position="start"),
109
+ # span.end.to_python_date(position="end")]
110
+
111
+ if self.chart.settings.highlight is not None:
112
+ irispie.plotly.highlight(self.figure, self.chart._get_highlight(),
113
+ color=resolve_highlight_color(
114
+ self.chart.settings.styles["chart"]["highlight_color"], alpha=0.25))
115
+
116
+ # Todo: This is a workaround for the issue with the highlight. It should be fixed in the irispie library.
117
+ # therefore, delete update_shapes() call here, when it's fixed in iris-pie
118
+ self.figure.update_shapes({"layer": "below"})
119
+
120
+ return self.figure
@@ -0,0 +1,52 @@
1
+ from rephorm.dict.update_traces import update_base_traces, apply_per_trace_styles
2
+
3
+
4
+ class ChartSeriesMapper:
5
+ def __init__(self, series):
6
+ self.series = series
7
+
8
+ def add_to_pdf(self, **kwargs):
9
+
10
+ figure = kwargs["figure"]
11
+ num_variants = self.series.data.num_variants
12
+ # Here we will get either single value or a list of values, depends if multivariate or not:
13
+ line_colors = self.series.settings.styles["chart"]["series"]["line_color"]
14
+ line_widths = self.series.settings.styles["chart"]["series"]["line_width"]
15
+ line_styles = self.series.settings.styles["chart"]["series"]["line_style"]
16
+ bar_colors = self.series.settings.styles["chart"]["series"]["bar_face_color"]
17
+
18
+ def get_series_type(key):
19
+ mapping = {
20
+ "line": "line",
21
+ "bar": "bar",
22
+ "bar_stack": "bar_stack",
23
+ "contribution_bar": "bar_relative",
24
+ "conbar": "bar_relative",
25
+ "barcon": "bar_relative",
26
+ "bar_relative": "bar_relative",
27
+ "bar_group": "bar_group",
28
+ "bar_overlay": "bar_overlay",
29
+ }
30
+ return mapping.get(key)
31
+
32
+ fig = self.series.data.plot(
33
+ span=self.series.settings.span,
34
+ date_format_style="compact",
35
+ date_axis_mode="instant",
36
+ figure=figure,
37
+ chart_type=get_series_type(self.series.settings.series_type),
38
+ legend=self.series.settings.legend,
39
+ show_legend=self.series.settings.show_legend,
40
+ return_info=True,
41
+ show_figure=False,
42
+ update_traces=update_base_traces(self.series.settings)
43
+ )
44
+
45
+ apply_per_trace_styles(
46
+ fig["figure"],
47
+ line_colors,
48
+ line_widths,
49
+ line_styles,
50
+ bar_colors,
51
+ num_variants,
52
+ )
@@ -0,0 +1,106 @@
1
+ from rephorm.object_mappers._utilities.grid_mapper_utility import compute_grid_layout
2
+ from rephorm.utility.report.add_footnotes import add_footnotes_at_bottom
3
+ from rephorm.utility.report.footnotes_counter import generate_footnote_numbers
4
+ from rephorm.utility.report.layout_utility import LayoutManager
5
+ from rephorm.utility.report.title_utility import add_title_with_references
6
+
7
+
8
+ class GridMapper:
9
+ def __init__(self, grid):
10
+ self.grid = grid
11
+
12
+ def add_to_pdf(self, **kwargs):
13
+
14
+ pdf = kwargs["pdf"]
15
+ layout = self.grid.settings.layout
16
+ nrow = self.grid.settings.nrow
17
+ ncol = self.grid.settings.ncol
18
+ footer_last_y = kwargs["footer_last_y"] if "footer_last_y" in kwargs else None
19
+ starting_page = pdf.page_no()
20
+ rs = self.grid.settings.styles["report"]
21
+ footer_top_padding = rs["footer"]["top_padding"]
22
+ footer_top_margin = rs["footer"]["top_margin"]
23
+ title_bottom_padding = rs["title_bottom_padding"]
24
+ # Todo: For grid why we dont take as in chapter? The used top height calculation?
25
+ # What is the reason to have Y coming from kwargs? Because then it takes Y from chapter/report layout calculations,
26
+ # and here it passes to layout... So I guess this is what offsets it differently, when adding chart to
27
+ # report/chapter directly and when having it inside 1x1 grid
28
+ y = kwargs["y"]
29
+ grid_size = self.grid.settings.ncol * self.grid.settings.nrow
30
+
31
+ if layout is None:
32
+ layout = compute_grid_layout(nrow=nrow, ncol=ncol)
33
+
34
+ if len(self.grid.CHILDREN) > grid_size:
35
+ raise Exception(
36
+ f"Grid capacity exceeded: attempting to add {len(self.grid.CHILDREN)} object(-s) to a grid with capacity for only "
37
+ f"{grid_size} object(-s) ({self.grid.settings.ncol}x{self.grid.settings.nrow}). "
38
+ f"Please either increase grid size or reduce the number of objects."
39
+ )
40
+
41
+ footnote_references = generate_footnote_numbers(self.grid.footnotes)
42
+ y = add_title_with_references(pdf, self.grid.title, footnote_references,
43
+ self.grid.settings.styles["grid"]["title"]["font_family"],
44
+ self.grid.settings.styles["grid"]["title"]["font_size"],
45
+ self.grid.settings.styles["grid"]["title"]["font_style"], title_y=y)
46
+
47
+ if bool(self.grid.footnotes):
48
+
49
+ footer_last_y = add_footnotes_at_bottom(
50
+ pdf,
51
+ self.grid.footnotes,
52
+ footnote_references,
53
+ last_y=footer_last_y,
54
+ font_family=self.grid.settings.styles["footnotes"]["font_family"],
55
+ font_size=self.grid.settings.styles["footnotes"]["font_size"],
56
+ font_style=self.grid.settings.styles["footnotes"]["font_style"],
57
+ footer_padding=footer_top_padding,
58
+ footer_top_margin=footer_top_margin,
59
+ )
60
+
61
+ layout_manager = LayoutManager(
62
+ pdf=pdf,
63
+ used_top_height=y+title_bottom_padding,
64
+ )
65
+
66
+ positions = layout_manager.calculate_position(layout=layout, nrow=nrow, ncol=ncol)
67
+ valid_positions = list(positions.keys())
68
+ # Todo: set to the last chart | Do not remove, yet
69
+ # last_element = valid_positions[-1]
70
+ # last_element_data = positions[last_element]
71
+ # pdf.line(pdf.l_margin, last_element_data["y"] + last_element_data["height"], pdf.w - pdf.r_margin, last_element_data["y"] + last_element_data["height"])
72
+
73
+ for i, item in enumerate(self.grid.CHILDREN):
74
+
75
+ position = valid_positions[i]
76
+ pos_data = positions[position]
77
+
78
+ current_page = pdf.page_no()
79
+ if current_page > starting_page:
80
+ starting_page = current_page
81
+ footer_last_y = None
82
+
83
+
84
+ grid_size = self.grid.settings.ncol * self.grid.settings.nrow
85
+
86
+ mapper = item._get_mapper()
87
+ mapper.add_to_pdf(
88
+ pdf=pdf,
89
+ x=pos_data["x"],
90
+ y=pos_data["y"],
91
+ w=pos_data["width"],
92
+ h=pos_data["height"],
93
+ grid_size=grid_size,
94
+ footer_last_y=footer_last_y,
95
+ context="grid",
96
+ )
97
+
98
+ # For debugging
99
+ # pdf.set_draw_color(255, 0, 0)
100
+ # pdf.rect(
101
+ # x=pos_data["x"],
102
+ # y=pos_data["y"],
103
+ # w=pos_data["width"],
104
+ # h=pos_data["height"],
105
+ # )
106
+
@@ -0,0 +1,8 @@
1
+
2
+ class PageBreakMapper:
3
+ def __init__(self, page_break):
4
+ self.page_break = page_break
5
+
6
+ def add_to_pdf(self, **kwargs):
7
+ pdf = kwargs["pdf"]
8
+ pdf.add_page()
@@ -0,0 +1,76 @@
1
+ from logging import warning
2
+
3
+ from fpdf import FPDF
4
+
5
+ from rephorm.object_mappers._globals import get_figure_map
6
+ from rephorm.utility.report.cleanup_utility import perform_cleanup
7
+ from rephorm.utility.report.layout_utility import LayoutManager
8
+ from rephorm.utility.merge.pdf_merger import overlay_pdfs
9
+ from rephorm.utility.report.report_utility import set_title_page
10
+
11
+
12
+ class ReportMapper:
13
+
14
+ def __init__(self, pdf: FPDF):
15
+ self.pdf = pdf
16
+
17
+ def compile(self, report, file_name: str, cleanup:bool = False):
18
+
19
+ rs = report.settings.styles["report"]
20
+
21
+ self.pdf.t_margin = rs["page_margin_top"]
22
+
23
+ # TODO: Currently, we offset all bottom spacing to the bottom margin because FPDF2
24
+ # automatically breaks tables as soon as it reaches the bottom margin. This prevents
25
+ # any overlap with the footer, since the footer is included in fpdf.b_margin.
26
+ # We need to apply the same approach for the top margin and top spacing
27
+ # (e.g., header, title padding, etc.).
28
+ # Also, make sure to verify whether any top padding or margin we have is actually
29
+ # padding or margin. Padding refers to inner spacing, while margin is outer spacing.
30
+ # Follow the standard convention.
31
+
32
+ self.pdf.b_margin = rs["page_margin_bottom"] + rs["footer"]["height"] + rs["footer"]["top_margin"]
33
+ self.pdf.l_margin = rs["page_margin_left"]
34
+ self.pdf.r_margin = rs["page_margin_right"]
35
+ self.pdf.c_margin = 0
36
+
37
+ self.pdf.add_page()
38
+ base_file_path = f"./tmp/report_tmp.pdf"
39
+ directory_path = "./tmp/pdf_figures"
40
+
41
+ used_top_height = rs["header"]["height"] + rs["header"]["bottom_margin"]
42
+
43
+ layout_manager = LayoutManager(pdf=self.pdf, used_top_height=used_top_height)
44
+
45
+ set_title_page(self.pdf, report)
46
+
47
+ n_children = len(report.CHILDREN)
48
+
49
+ for i, item in enumerate(report.CHILDREN):
50
+
51
+ x, y, w, h = layout_manager.get_default_layout()
52
+ mapper = item._get_mapper()
53
+ # reset width to default
54
+ # if children does not have width set by user, use the default width, if they do:
55
+ # check if it does not exceed pdf.w and then use children's width instead.
56
+ if hasattr(item, "width") and item.width is not None:
57
+ if item.width > self.pdf.w:
58
+ warning(
59
+ f"{repr(item)} width: ({item.width}) | exceeds the PDF page width: ({self.pdf.w}). Falling back to defaults"
60
+ )
61
+ else:
62
+ w = item.width
63
+
64
+ mapper.add_to_pdf( pdf = self.pdf, w = w, h = h, x = x, y = y)
65
+
66
+ if i < n_children-1:
67
+ self.pdf.add_page()
68
+
69
+ if not get_figure_map():
70
+ self.pdf.output(f"{file_name}.pdf")
71
+ else:
72
+ self.pdf.output(base_file_path)
73
+ overlay_pdfs(base_pdf_path=base_file_path, pdf_data=get_figure_map(),
74
+ output_pdf_path=f"{file_name}.pdf")
75
+ perform_cleanup(cleanup, directory_path, base_file_path)
76
+