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
@@ -0,0 +1,74 @@
1
+ import os
2
+ import sys
3
+ from logging import warning
4
+
5
+
6
+ def get_fonts(load_fonts):
7
+
8
+ default_fonts = get_default_fonts()
9
+
10
+ if not load_fonts:
11
+ return default_fonts
12
+
13
+ font_map = {(font['font_path']): font for font in default_fonts}
14
+
15
+ # Update or append additional fonts
16
+ for font in load_fonts:
17
+ key = (font['font_path'])
18
+ font_map[key] = font
19
+
20
+ return list(font_map.values())
21
+
22
+ def get_default_fonts():
23
+ base_path = os.path.abspath(os.path.dirname(__file__))
24
+ return [
25
+ {
26
+ "font_family": "Open Sans",
27
+ "font_path": os.path.join(base_path, "OpenSans.ttf"),
28
+ "font_style": ""},
29
+ {
30
+ "font_family": "Open Sans",
31
+ "font_path": os.path.join(base_path, "OpenSans-Italic.ttf"),
32
+ "font_style": "I"},
33
+ {
34
+ "font_family": "Open Sans",
35
+ "font_path": os.path.join(base_path, "OpenSans-Bold.ttf"),
36
+ "font_style": "B"},
37
+ {
38
+ "font_family": "Open Sans",
39
+ "font_path": os.path.join(base_path, "OpenSans-BoldItalic.ttf"),
40
+ "font_style": "BI"},
41
+ ]
42
+
43
+ def validate_fonts(fonts):
44
+
45
+ required_styles = {"", "I", "B"}
46
+
47
+ grouped_fonts = {}
48
+ for font in fonts:
49
+ family = font["font_family"]
50
+ style = font["font_style"].upper()
51
+ if family not in grouped_fonts:
52
+ grouped_fonts[family] = set()
53
+ grouped_fonts[family].add(style)
54
+
55
+ for font_family, available_styles in grouped_fonts.items():
56
+ missing_styles = required_styles - available_styles
57
+ if missing_styles:
58
+ raise Exception(f"Font family '{font_family}' is missing styles: {', '.join(missing_styles)}")
59
+
60
+
61
+ def add_custom_fonts(pdf, fonts):
62
+
63
+ if fonts is not None:
64
+ try:
65
+ validate_fonts(fonts)
66
+ except Exception as e:
67
+ warning(e)
68
+ sys.exit(1)
69
+
70
+ # Font loading
71
+ for font_dict in fonts:
72
+ pdf.add_font(family = font_dict.get("font_family"),
73
+ fname=font_dict.get('font_path'),
74
+ style=font_dict.get('font_style', ''))
@@ -0,0 +1,9 @@
1
+ """
2
+ Function that checks whether all provided arguments are non-falsy.
3
+
4
+ Example:
5
+ is_set(param_x, param_y)
6
+ This will return True if BOTH params are truthy (not None, False, 0, '').
7
+ """
8
+ def is_set(*args):
9
+ return all(arg is not None for arg in args)
File without changes
@@ -0,0 +1,80 @@
1
+ import copy
2
+
3
+ from rephorm.dict.styles import default_styles
4
+
5
+ class ChartPropertiesManager:
6
+ def __init__(self):
7
+ self.init_properties()
8
+
9
+ def init_properties(self):
10
+
11
+ # actually we don't need default here bcs it takes from styles in merge_settings anyway.
12
+ self.default_bar_colors = default_styles()["chart"]["bar_color_order"]
13
+ self.default_line_colors = default_styles()["chart"]["line_color_order"]
14
+ self.default_line_styles = default_styles()["chart"]["line_styles_order"]
15
+ self.default_line_widths = default_styles()["chart"]["line_width_order"]
16
+ # set memory
17
+ self.bar_colors = copy.deepcopy(self.default_bar_colors)
18
+ self.line_widths = copy.deepcopy(self.default_line_widths)
19
+ self.line_colors = copy.deepcopy(self.default_line_colors)
20
+ self.line_styles = copy.deepcopy(self.default_line_styles)
21
+
22
+ # set working stack
23
+ self.bar_color_stack = copy.deepcopy(self.bar_colors)
24
+ self.line_widths_stack = copy.deepcopy(self.line_widths)
25
+ self.line_color_stack = copy.deepcopy(self.line_colors)
26
+ self.line_style_stack = copy.deepcopy(self.line_styles)
27
+
28
+ def get_bar_color(self):
29
+ if not self.bar_color_stack:
30
+ self.bar_color_stack = copy.deepcopy(self.bar_colors)
31
+ return self.bar_color_stack.pop(0)
32
+
33
+ def get_line_width(self):
34
+ if not self.line_widths_stack:
35
+ self.line_widths_stack = copy.deepcopy(self.line_widths)
36
+ return self.line_widths_stack.pop(0)
37
+
38
+ def get_line_color(self):
39
+ if not self.line_color_stack:
40
+ self.line_color_stack = copy.deepcopy(self.line_colors)
41
+ return self.line_color_stack.pop(0)
42
+
43
+ def get_line_style(self):
44
+ if not self.line_style_stack:
45
+ self.line_style_stack = copy.deepcopy(self.line_styles)
46
+ return self.line_style_stack.pop(0)
47
+
48
+ def update_colors_for_chart(self, bar_colors=None, line_colors=None, line_styles=None, line_widths=None):
49
+
50
+ self.bar_color_stack.clear()
51
+ self.line_color_stack.clear()
52
+ self.line_style_stack.clear()
53
+ self.line_widths_stack.clear()
54
+
55
+ self.bar_colors = bar_colors if bar_colors and bar_colors is not None else copy.deepcopy(self.default_bar_colors)
56
+ self.line_colors = line_colors if line_colors and line_colors is not None else copy.deepcopy(self.default_line_colors)
57
+ self.line_styles = line_styles if line_styles and line_styles is not None else copy.deepcopy(self.default_line_styles)
58
+ self.line_widths = line_widths if line_widths and line_widths is not None else copy.deepcopy(self.default_line_widths)
59
+
60
+ self.bar_color_stack = copy.deepcopy(self.bar_colors)
61
+ self.line_color_stack = copy.deepcopy(self.line_colors)
62
+ self.line_style_stack = copy.deepcopy(self.line_styles)
63
+
64
+ chart_properties = ChartPropertiesManager()
65
+
66
+ def get_bar_color():
67
+ return chart_properties.get_bar_color()
68
+
69
+ def get_line_width():
70
+ return chart_properties.get_line_width()
71
+
72
+ def get_line_color():
73
+ return chart_properties.get_line_color()
74
+
75
+
76
+ def get_line_style():
77
+ return chart_properties.get_line_style()
78
+
79
+ def update_chart_properties(bar_colors=None, line_colors=None, line_styles=None, line_widths=None):
80
+ return chart_properties.update_colors_for_chart(bar_colors, line_colors, line_styles, line_widths)
@@ -0,0 +1,134 @@
1
+ import copy
2
+
3
+ from rephorm.dict.styles import default_styles, DEFAULT_FONT_FAMILY
4
+ from rephorm.object_params.settings import object_params
5
+ from rephorm.utility.merge.chart_properties_manager import get_bar_color, get_line_color, \
6
+ get_line_style, update_chart_properties, get_line_width
7
+ from rephorm.utility.merge.merge_styles import update_nested_structure, create_nested_structure
8
+
9
+
10
+ def merge_settings(obj, params):
11
+ """
12
+ obj - The current object (e.g., Report, Chapter, etc.)
13
+ params - A dictionary of parameters to propagate | That is report. settings
14
+ """
15
+ #Todo: Known issue: if you set report.font_size on level of chart,
16
+ # it will not recalculate font sizes for chart and it's children.
17
+ # ...
18
+ # This is because at that point of iteration there is local params dictionary,
19
+ # and we update it with what comes from children. To update reference
20
+ # font format when children gets report.font_size parameter (reference parameters)
21
+ # we would need to reinitialize default dict with those values from the children,
22
+ # but we cant do it because we would loose the state of the local params,
23
+ # which already have some important settings preset for us.
24
+ # ...
25
+
26
+ local_params = copy.deepcopy(params)
27
+
28
+ parent_styles = create_nested_structure(obj.__class__.__name__, local_params.get("styles", {}))
29
+ parent_font_family = parent_styles.get("report", {}).get("font_family", DEFAULT_FONT_FAMILY)
30
+
31
+ object_styles = create_nested_structure(obj.__class__.__name__, obj.settings.__dict__.get("styles", {}))
32
+ obj_font_family = object_styles.get("font_family", parent_font_family)
33
+
34
+ # We update the default styles with styles coming from the previous parent (local_params["styles"]),
35
+ # preserving inherited values. This avoids overwriting/resetting them as before (line 40),
36
+ # where default_styles was directly assigned to local_params["styles"].
37
+
38
+ local_params["styles"] = update_nested_structure(
39
+ default_styles(obj_font_family),
40
+ create_nested_structure(obj.__class__.__name__,local_params.get("styles", {}))
41
+ )
42
+
43
+ for key in object_params.keys():
44
+ # Check if the key exists in params; if not, use its default value
45
+ parent_value = local_params.get(key, None)
46
+ # Check if obj.settings already has this key set
47
+ if hasattr(obj.settings, key) and getattr(obj.settings, key) is not None:
48
+ current_obj_value = getattr(obj.settings, key)
49
+
50
+ if key == "styles":
51
+ current_obj_value = update_nested_structure(parent_value, create_nested_structure(obj.__class__.__name__, current_obj_value))
52
+
53
+ setattr(obj.settings, key, current_obj_value)
54
+ local_params[key] = current_obj_value
55
+
56
+ elif parent_value is not None:
57
+ ultimate_owners = object_params.get(key, {}).get("ultimates", [])
58
+ if type(obj).__name__ in ultimate_owners:
59
+ setattr(obj.settings, key, parent_value)
60
+
61
+ # If no value is set yet, use the default from ultimate_setting_owner
62
+ elif getattr(obj.settings, key, None) is None:
63
+ ultimate_owners = object_params.get(key, {}).get("ultimates", [])
64
+ if type(obj).__name__ in ultimate_owners:
65
+ if key == "styles":
66
+ setattr(obj.settings, key, getattr(local_params, key))
67
+ else:
68
+ setattr(obj.settings, key, object_params[key].get("default_value", None))
69
+
70
+ if obj.__class__.__name__ == "Chart":
71
+ structured_styles = create_nested_structure(obj.__class__.__name__, obj.settings.styles) \
72
+ if hasattr(obj.settings, "styles") \
73
+ else {}
74
+ update_chart_properties(structured_styles.get("chart", {}).get("bar_color_order", None),
75
+ structured_styles.get("chart", {}).get("line_color_order", None),
76
+ structured_styles.get("chart", {}).get("line_styles_order", None),
77
+ structured_styles.get("chart", {}).get("line_width_order", None),
78
+ )
79
+
80
+ if hasattr(obj, "CHILDREN") and isinstance(obj.CHILDREN, list):
81
+ list_of_obj = [obj.CHILDREN[-1]] if obj.__class__.__name__ == "Report" and obj.CHILDREN else obj.CHILDREN
82
+ for child in list_of_obj:
83
+ if hasattr(child, "settings"):
84
+ merge_settings(child, local_params)
85
+ handle_object_properties(child, child.settings.styles)
86
+
87
+ """
88
+ This function process final styles dictionary and assigns colors to chartSeries based on series type, using predefined color order.
89
+ If the color is not user-defined, it assigns the next color from the global color order. (Non user defined colors coming as "None" by default)
90
+
91
+ When we call this function we do not need to ensure that the necessary dict structure
92
+ exists or that keys are present, because who calls this function should already have it set.
93
+ This function should not be called on styles dict, that was not processed (missing keys/values)
94
+ or that is not properly structured (dict that is flat / or not following the correct order)
95
+ """
96
+
97
+ # TODO: Implement multivariate series support with consistent color assignment
98
+ # Current Issue:
99
+ # When creating a single series object that contains multiple data components (multivariate),
100
+ # the color assignment system only applies a single color to the entire series.
101
+ # Implementation:
102
+ # Here we would detect if the series that is coming are multivariate or single,
103
+ # basically check if single, assign color, else create an array for the length of
104
+ # data columns in the series, populate it with get_bar_color() until it's full,
105
+ # and pass it to styles as "bar_face_color"
106
+
107
+ def handle_object_properties(obj, merged_styles):
108
+
109
+ if obj.__class__.__name__ == "ChartSeries":
110
+
111
+ is_multivariate = obj.data.num_variants > 1
112
+
113
+ series_type = getattr(obj.settings, "series_type")
114
+ series_styles = merged_styles["chart"]["series"]
115
+
116
+ def assign_style(key, generator_func):
117
+ if key not in series_styles or series_styles[key] is None:
118
+ if is_multivariate:
119
+ series_styles[key] = [generator_func() for _ in range(obj.data.num_variants)]
120
+ else:
121
+ series_styles[key] = generator_func()
122
+
123
+ if series_type in (
124
+ "bar", "contribution_bar", "barcon", "conbar", "bar_color_stack",
125
+ "bar_group", "bar_overlay", "bar_relative"
126
+ ):
127
+ assign_style("bar_face_color", get_bar_color)
128
+ else:
129
+ assign_style("line_color", get_line_color)
130
+ assign_style("line_style", get_line_style)
131
+ # line width: typically same for all?
132
+ assign_style("line_width", get_line_width)
133
+
134
+
@@ -0,0 +1,79 @@
1
+ import copy
2
+
3
+ from rephorm.dict.styles import default_styles
4
+
5
+ # Get the allowed Styles structure from default_styles
6
+ allowed_structure = default_styles()
7
+
8
+ def validate_value_type(obj_name, path, value, structure=allowed_structure):
9
+ keys = path.split('.')
10
+ final_structure = structure
11
+ current_path = []
12
+
13
+ for key in keys:
14
+ current_path.append(key)
15
+ if isinstance(final_structure, dict):
16
+ if key not in final_structure:
17
+ raise KeyError(f"({obj_name} object): Styles key '{key}' for '{'.'.join(current_path[:-1])}' does not exist. "
18
+ f"Available keys at this level: {list(final_structure.keys())}")
19
+ final_structure = final_structure.get(key, {})
20
+ else:
21
+ # If we've reached a non-dictionary node, we can't go further; this means wrong nesting
22
+ raise KeyError(f"Styles: Invalid path '{path}'. No further nesting allowed under '{keys[-2]}'.")
23
+
24
+ if isinstance(final_structure, dict):
25
+ if not isinstance(value, dict):
26
+ available_keys = list(final_structure.keys())
27
+ raise ValueError(
28
+ f"Styles key error: '{path}' expects a dictionary with keys: {available_keys}. Got {type(value).__name__} instead.")
29
+ else:
30
+ # Recursively validate each sub-key for dictionaries
31
+ for sub_key, sub_value in value.items():
32
+ new_path = f"{path}.{sub_key}"
33
+ validate_value_type(obj_name, new_path, sub_value)
34
+ else:
35
+ # Skips type validation if the type of Final structure's (edge parameter in default_styles)
36
+ # value was set to None.
37
+ if type(final_structure) is type(None):
38
+ return
39
+
40
+ expected_type = type(final_structure) if not isinstance(final_structure, type) else final_structure
41
+
42
+ if not isinstance(value, expected_type) and not (isinstance(value, (int, float)) and expected_type in (int, float)):
43
+ raise TypeError(
44
+ f"Styles: Invalid type for '{path}': Expected {expected_type.__name__}, got {type(value).__name__}")
45
+
46
+ def create_nested_structure(obj_name, flat_dict):
47
+
48
+ if flat_dict is None:
49
+ return flat_dict
50
+
51
+ nested_dict = {}
52
+
53
+ flat_dict = copy.deepcopy(flat_dict)
54
+
55
+ for key, value in flat_dict.items():
56
+ validate_value_type(obj_name, key, value, allowed_structure)
57
+ keys = key.split(".")
58
+ d = nested_dict
59
+
60
+ for k in keys[:-1]:
61
+ d = d.setdefault(k, {})
62
+
63
+ d[keys[-1]] = value
64
+
65
+ return nested_dict
66
+
67
+ def update_nested_structure(org_dict, new_dict):
68
+ if new_dict is None:
69
+ return org_dict
70
+
71
+ result = copy.deepcopy(org_dict)
72
+
73
+ for key, value in new_dict.items():
74
+ if isinstance(value, dict) and key in result and isinstance(result[key], dict):
75
+ result[key] = update_nested_structure(result[key], value)
76
+ else:
77
+ result[key] = value
78
+
79
+ return result
@@ -0,0 +1,29 @@
1
+ import fitz # PyMuPDF
2
+
3
+ # Takes units in PT (Therefore must be converted correctly)
4
+ def overlay_pdfs(base_pdf_path, pdf_data, output_pdf_path):
5
+ base_pdf = fitz.open(base_pdf_path)
6
+
7
+ for pdf_info in pdf_data:
8
+ page_num = pdf_info['page']-1
9
+ overlay_pdf = fitz.open(pdf_info['pdf_path'])
10
+ base_page = base_pdf[page_num]
11
+
12
+ x = (pdf_info['x']) # X coordinate of the bottom-left corner of the overlay
13
+ y = (pdf_info['y']) # Y coordinate of the bottom-left corner of the overlay
14
+ width = (pdf_info['width']) # Width of the overlay
15
+ height = (pdf_info['height']) # Height of the overlay
16
+
17
+ # Define the position rectangle for the overlay
18
+ position = fitz.Rect(x, y, x + width, y + height)
19
+
20
+ # Draw a rectangle for visual debugging
21
+ # red_color = (1, 0, 0)
22
+ # base_page.draw_rect(position, color=red_color, width=1.5, fill=None)
23
+
24
+ # Place the overlay PDF
25
+ base_page.show_pdf_page(position, overlay_pdf, 0)
26
+ overlay_pdf.close()
27
+
28
+ base_pdf.save(output_pdf_path)
29
+ print(f"Overlay PDF saved: {output_pdf_path}")
File without changes
@@ -0,0 +1,46 @@
1
+ from fpdf import enums
2
+
3
+ # Todo add some space after line, between line and first footnote
4
+
5
+ def add_footnotes_at_bottom(pdf, footnotes, reference_nr, last_y = None, **kwargs):
6
+
7
+ font_size = kwargs.get('font_size')
8
+ font_family = kwargs.get('font_family')
9
+ font_style = kwargs.get('font_style')
10
+ footer_padding = kwargs.get('footer_padding')
11
+ footer_top_margin = kwargs.get('footer_top_margin')
12
+
13
+
14
+ bottom_padding = 10
15
+ b_margin = pdf.b_margin
16
+
17
+ # prevent auto page break - this is a MUST!!!!!
18
+ # else it would push content and footnotes to new page if slightly overflows
19
+ pdf.set_auto_page_break(auto=False)
20
+
21
+ pdf.set_font(font_family, font_style, font_size)
22
+
23
+ #Sets line color above the footnotes
24
+ pdf.set_draw_color(0, 0, 0)
25
+
26
+ # current b_margin = page_margin_bottom + footer_height + footer_top_margin
27
+ footer_position = pdf.h - b_margin + footer_top_margin
28
+
29
+ if last_y is None:
30
+ footnote_y_position = pdf.h - b_margin + footer_padding + footer_top_margin
31
+ pdf.line(x1=pdf.l_margin, y1=footer_position, x2=pdf.w - pdf.r_margin, y2=footer_position)
32
+ else:
33
+ footnote_y_position = last_y
34
+
35
+ pdf.set_y(footnote_y_position)
36
+
37
+ for ref, footnote in zip(reference_nr, footnotes):
38
+ formatted_footnote = f"[{ref}] {footnote}"
39
+ pdf.multi_cell(w=pdf.w - pdf.l_margin - pdf.r_margin, h=None, text=formatted_footnote, align='L', new_x=enums.XPos.LEFT)
40
+ last_y = pdf.get_y() + bottom_padding
41
+
42
+ # Reset auto page break and b_margin
43
+ # Todo: drop footer height, rename footer padding to bottom_margin_padding, and footnotes pos y would be pdf.h - b.margin
44
+ pdf.set_auto_page_break(auto=True, margin = b_margin)
45
+
46
+ return last_y
@@ -0,0 +1,19 @@
1
+ # make util funct cleanup later on, so less code regarding cleanup stuff. but call it here.
2
+ import os
3
+ import shutil
4
+
5
+ from rephorm.object_mappers._globals import reset_figure_map
6
+
7
+ def perform_cleanup(cleanup: bool = False, directory_path: str = None, base_file_path: str = None):
8
+
9
+ reset_figure_map()
10
+
11
+ try:
12
+ # Remove tmp dir where pdf figures are stored
13
+ if cleanup:
14
+ shutil.rmtree(directory_path)
15
+ # Remove base (initial) pdf
16
+ os.remove(base_file_path)
17
+
18
+ except Exception as e:
19
+ print(f"Cleanup utility - error occurred: {e}")
@@ -0,0 +1,15 @@
1
+ from rephorm.utility.is_set import is_set
2
+ footnote_counter = 0
3
+
4
+ def generate_footnote_numbers(footnotes):
5
+ """
6
+ Generate list of numbers for the footnotes
7
+ Uses a global counter.
8
+ """
9
+ global footnote_counter
10
+ references = []
11
+ if is_set(footnotes):
12
+ for _ in footnotes:
13
+ footnote_counter += 1
14
+ references.append(str(footnote_counter))
15
+ return references
@@ -0,0 +1,14 @@
1
+ # Todo: Consider making part of ChartMapper method (that should be implemented from interface) | Rename to render_chart
2
+ import plotly.io as pio
3
+ pio.kaleido.scope.mathjax = None
4
+
5
+ def to_image(series, image_format="pdf", width=None, height=None):
6
+ """
7
+ Converts a chart to an image in the specified format and dimensions.
8
+ """
9
+ try:
10
+ return series.to_image(format=image_format, width=width, height=height)
11
+
12
+ except Exception as e:
13
+ print(f"Error generating image from chart: {e}")
14
+ return None
@@ -0,0 +1,59 @@
1
+ """
2
+ pdf.b_margin = b.margin + footer_height + footer_top_margin.
3
+ """
4
+ class LayoutManager:
5
+ def __init__(self, pdf, used_top_height = 0):
6
+ """
7
+ Initialize the layout manager with pdf instance, and header and footer height.
8
+ """
9
+ self.pdf = pdf
10
+ self.used_top_height = used_top_height
11
+
12
+ def calculate_position(self, layout, nrow, ncol):
13
+ """
14
+ Here we compute grid layout based on user inputs and return array with every element X Y W H "coordinates"
15
+ Result - array with coordinates of each element and key is (row_indx, col_indx)
16
+ """
17
+ result={}
18
+
19
+ cell_width = ((self.pdf.w - self.pdf.l_margin - self.pdf.r_margin) / ncol)
20
+ # b_margin = b_margin + footer_height + footer_top_margin
21
+ cell_height = (self.pdf.h - self.pdf.b_margin - self.used_top_height) / nrow
22
+
23
+ for row_indx in range (nrow):
24
+ for col_indx in range(ncol):
25
+ cell = layout[row_indx][col_indx]
26
+
27
+ if cell is None:
28
+ continue
29
+
30
+ rowspan = cell.get("rowspan", 1)
31
+ colspan = cell.get("colspan", 1)
32
+
33
+ width = colspan * cell_width
34
+ height = rowspan * cell_height
35
+
36
+ x = col_indx * cell_width + self.pdf.l_margin # sets from left
37
+ y = row_indx * cell_height + self.used_top_height # sets from top
38
+
39
+ result[(row_indx, col_indx)] = {
40
+ "x": x,
41
+ "y": y,
42
+ "width": width,
43
+ "height": height
44
+ }
45
+
46
+ return result
47
+
48
+ def get_default_layout(self):
49
+ """
50
+ Gets the position (x, y) and available size (width, height) for a chapter or report,
51
+ considering headers and footers to determine the exact placement of its content (child object).
52
+ """
53
+ available_width = self.pdf.w - self.pdf.l_margin - self.pdf.r_margin
54
+ available_height = self.pdf.h - self.pdf.t_margin - self.pdf.b_margin - self.used_top_height
55
+
56
+ x = self.pdf.l_margin
57
+ y = self.used_top_height + self.pdf.t_margin
58
+
59
+ return x, y, available_width, available_height
@@ -0,0 +1,94 @@
1
+ import warnings
2
+
3
+ def _get_max_span(obj, span, comparison):
4
+
5
+ if not hasattr(comparison, "frequency"):
6
+ return span
7
+
8
+ obj_frequency = (
9
+ getattr(obj.settings.span, "frequency", None)
10
+ or getattr(obj.settings, "frequency", None)
11
+ )
12
+
13
+ if comparison.frequency is not None and obj_frequency is not None:
14
+ if comparison.frequency != obj_frequency:
15
+ warnings.warn("Multiple frequencies are not allowed.")
16
+ return span
17
+
18
+ if span is None:
19
+ span = comparison
20
+ return span
21
+
22
+ start = min(span.start, comparison.start)
23
+ end = max(span.end, comparison.end)
24
+
25
+ return start >> end
26
+
27
+ # Method for Table and Chart
28
+ def get_span(obj):
29
+ span = None
30
+
31
+ def resolve_span_recursive(node, current_span):
32
+ if hasattr(node, "settings") and hasattr(node.settings, "span") and hasattr(node, "data") and hasattr(node.data, "span"):
33
+
34
+ comparison = node.settings.span.resolve(node.data.span)
35
+ current_span = _get_max_span(obj, current_span, comparison)
36
+
37
+ if hasattr(node, "CHILDREN"):
38
+ for child in node.CHILDREN:
39
+ current_span = resolve_span_recursive(child, current_span)
40
+
41
+ return current_span
42
+
43
+ span = resolve_span_recursive(obj, span)
44
+
45
+ # if hasattr(obj, "settings") and hasattr(obj.settings, "span"):
46
+ if ".start" in str(obj.settings.span.start):
47
+ obj.settings.span = span.start >> obj.settings.span.end
48
+
49
+ if ".end" in str(obj.settings.span.end):
50
+ obj.settings.span = obj.settings.span.start >> span.end
51
+
52
+ if obj.settings.span.start > obj.settings.span.end:
53
+ raise Exception(
54
+ f"Invalid highlight range for {obj.__class__.__name__}: Start date ({obj.settings.span.start}) is after the end date ({obj.settings.span.end}). "
55
+ f"Please provide a highlight range where start precedes the end. "
56
+ f"Current range: {obj.settings.span.start} >> {obj.settings.span.end}"
57
+ )
58
+
59
+ return obj.settings.span
60
+
61
+ # Method for Table and Chart
62
+ def get_highlight(obj):
63
+
64
+ if obj.settings.highlight is None:
65
+ return # Just exit the function
66
+
67
+ span = get_span(obj)
68
+
69
+ if span is None:
70
+ raise Exception(f"{obj.__class__.__name__} span is missing.")
71
+
72
+ start = obj.settings.highlight.start
73
+
74
+ if ".start" in str(start):
75
+ start = span.start
76
+
77
+ if start not in span:
78
+ # We just warn user. But this start not in span is not a problem, because we return min/max
79
+ # It is just for user to know of issue
80
+ warnings.warn(f"{obj.__class__.__name__} highlight start is outside the selected span.", UserWarning)
81
+
82
+ end = obj.settings.highlight.end
83
+
84
+ if ".end" in str(end):
85
+ end = span.end
86
+
87
+ if start > end:
88
+ raise Exception(
89
+ f"Invalid highlight range for {obj.__class__.__name__}: Start date ({start}) is after the end date ({end}). "
90
+ f"Please provide a highlight range where start precedes the end. "
91
+ f"Current range: {start} >> {end}"
92
+ )
93
+
94
+ return max(start, span.start), min(end, span.end)