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.
- rephorm/__init__.py +19 -0
- rephorm/decorators/__init__.py +0 -0
- rephorm/decorators/settings_validation.py +49 -0
- rephorm/dict/__init__.py +1 -0
- rephorm/dict/colors.py +22 -0
- rephorm/dict/legend_positions.py +136 -0
- rephorm/dict/styles.py +206 -0
- rephorm/dict/update_layout.py +129 -0
- rephorm/dict/update_traces.py +90 -0
- rephorm/object_mappers/__init__.py +0 -0
- rephorm/object_mappers/_globals.py +16 -0
- rephorm/object_mappers/_utilities/__init__.py +0 -0
- rephorm/object_mappers/_utilities/chart_mapper_utility.py +34 -0
- rephorm/object_mappers/_utilities/grid_mapper_utility.py +11 -0
- rephorm/object_mappers/_utilities/table_mapper_utility.py +59 -0
- rephorm/object_mappers/chapter_mapper.py +82 -0
- rephorm/object_mappers/chart_mapper.py +120 -0
- rephorm/object_mappers/chart_series_mapper.py +52 -0
- rephorm/object_mappers/grid_mapper.py +106 -0
- rephorm/object_mappers/page_break_mapper.py +8 -0
- rephorm/object_mappers/report_mapper.py +76 -0
- rephorm/object_mappers/table_mapper.py +191 -0
- rephorm/object_mappers/table_section_mapper.py +41 -0
- rephorm/object_mappers/table_series_mapper.py +79 -0
- rephorm/object_mappers/text_mapper.py +36 -0
- rephorm/object_params/__init__.py +0 -0
- rephorm/object_params/settings.py +184 -0
- rephorm/objects/__init__.py +0 -0
- rephorm/objects/_utilities/__init__.py +0 -0
- rephorm/objects/_utilities/settings_container.py +8 -0
- rephorm/objects/chapter.py +44 -0
- rephorm/objects/chart.py +70 -0
- rephorm/objects/chart_series.py +40 -0
- rephorm/objects/footnote.py +13 -0
- rephorm/objects/grid.py +57 -0
- rephorm/objects/page_break.py +13 -0
- rephorm/objects/report.py +196 -0
- rephorm/objects/table.py +76 -0
- rephorm/objects/table_section.py +48 -0
- rephorm/objects/table_series.py +36 -0
- rephorm/objects/text.py +35 -0
- rephorm/utility/PDF.py +80 -0
- rephorm/utility/__init__.py +0 -0
- rephorm/utility/add_style_prefix.py +30 -0
- rephorm/utility/fonts/OpenSans-Bold.ttf +0 -0
- rephorm/utility/fonts/OpenSans-BoldItalic.ttf +0 -0
- rephorm/utility/fonts/OpenSans-Italic.ttf +0 -0
- rephorm/utility/fonts/OpenSans.ttf +0 -0
- rephorm/utility/fonts/__init__.py +0 -0
- rephorm/utility/fonts/font_loading.py +74 -0
- rephorm/utility/is_set.py +9 -0
- rephorm/utility/merge/__init__.py +0 -0
- rephorm/utility/merge/chart_properties_manager.py +80 -0
- rephorm/utility/merge/merge_settings.py +134 -0
- rephorm/utility/merge/merge_styles.py +79 -0
- rephorm/utility/merge/pdf_merger.py +29 -0
- rephorm/utility/report/__init__.py +0 -0
- rephorm/utility/report/add_footnotes.py +46 -0
- rephorm/utility/report/cleanup_utility.py +19 -0
- rephorm/utility/report/footnotes_counter.py +15 -0
- rephorm/utility/report/image_utility.py +14 -0
- rephorm/utility/report/layout_utility.py +59 -0
- rephorm/utility/report/range_utility.py +94 -0
- rephorm/utility/report/report_utility.py +93 -0
- rephorm/utility/report/resolve_color.py +39 -0
- rephorm/utility/report/table_utility.py +71 -0
- rephorm/utility/report/title_utility.py +49 -0
- rephorm/utility/unit_converter.py +12 -0
- rephorm-1.0.1.dist-info/METADATA +41 -0
- rephorm-1.0.1.dist-info/RECORD +73 -0
- rephorm-1.0.1.dist-info/WHEEL +5 -0
- rephorm-1.0.1.dist-info/licenses/LICENSE +21 -0
- 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', ''))
|
|
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)
|