Coverage for src / rtflite / pagination / processor.py: 94%
88 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-08 04:50 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-08 04:50 +0000
1from copy import deepcopy
2from typing import Any
4from ..attributes import BroadcastValue
5from .strategies.base import PageContext
8class PageFeatureProcessor:
9 """Processes page features like borders, headers, and footers for each page."""
11 def process(self, document: Any, page: PageContext) -> PageContext:
12 """Process a single page to apply all feature-specific logic."""
14 # Calculate final attributes for the table body on this page
15 # (This includes applying top/bottom borders correctly)
16 page.final_body_attrs = self._apply_pagination_borders(document, page)
18 return page
20 def _should_show_element(self, element_location: str, page: PageContext) -> bool:
21 """Determine if an element should be shown on a specific page."""
22 logic = {
23 "all": True,
24 "first": page.is_first_page,
25 "last": page.is_last_page,
26 }
27 return logic.get(element_location, False)
29 def _apply_pagination_borders(self, document, page: PageContext) -> Any:
30 """Apply proper borders for paginated context following r2rtf design."""
32 # Start with a deep copy of the page's table attributes (processed/sliced)
33 # or document's body attributes if not available
34 base_attrs = page.table_attrs or document.rtf_body
35 page_attrs = deepcopy(base_attrs)
37 page_df_height = page.data.height
38 page_df_width = page.data.width
39 page_shape = (page_df_height, page_df_width)
41 if page_df_height == 0:
42 return page_attrs
44 # Clear border_first and border_last from being broadcast to all rows
45 if hasattr(page_attrs, "border_first") and page_attrs.border_first:
46 page_attrs.border_first = None
48 if hasattr(page_attrs, "border_last") and page_attrs.border_last:
49 page_attrs.border_last = None
51 # Ensure border_top and border_bottom are properly sized for this page
52 if not page_attrs.border_top:
53 page_attrs.border_top = [
54 [""] * page_df_width for _ in range(page_df_height)
55 ]
56 if not page_attrs.border_bottom:
57 page_attrs.border_bottom = [
58 [""] * page_df_width for _ in range(page_df_height)
59 ]
61 # --- Logic from DocumentService.apply_pagination_borders ---
63 # 1. First Page Logic
64 has_column_headers = (
65 document.rtf_column_header and len(document.rtf_column_header) > 0
66 )
68 # If first page, NO headers, apply PAGE border_first to top of body
69 if (
70 page.is_first_page
71 and not has_column_headers
72 and document.rtf_page.border_first
73 ):
74 for col_idx in range(page_df_width):
75 page_attrs = self._apply_border_to_cell(
76 page_attrs,
77 0,
78 col_idx,
79 "top",
80 document.rtf_page.border_first,
81 page_shape,
82 )
84 # If first page, WITH headers, apply BODY border_first to top of body
85 if page.is_first_page and has_column_headers and document.rtf_body.border_first:
86 self._apply_body_border_first(
87 document, page_attrs, page_df_width, page_shape
88 )
90 # 2. Middle Page Logic (Non-First)
91 # Apply BODY border_first to top of body
92 if not page.is_first_page and document.rtf_body.border_first:
93 self._apply_body_border_first(
94 document, page_attrs, page_df_width, page_shape
95 )
97 # 3. Footnote/Source Logic
98 has_footnote_on_page = (
99 document.rtf_footnote
100 and document.rtf_footnote.text
101 and self._should_show_element(document.rtf_page.page_footnote, page)
102 )
103 has_source_on_page = (
104 document.rtf_source
105 and document.rtf_source.text
106 and self._should_show_element(document.rtf_page.page_source, page)
107 )
109 footnote_as_table_on_last = (
110 document.rtf_footnote
111 and document.rtf_footnote.text
112 and getattr(document.rtf_footnote, "as_table", True)
113 and document.rtf_page.page_footnote in ("last", "all")
114 )
115 source_as_table_on_last = (
116 document.rtf_source
117 and document.rtf_source.text
118 and getattr(document.rtf_source, "as_table", False)
119 and document.rtf_page.page_source in ("last", "all")
120 )
122 # 4. Bottom Border Logic
123 if not page.is_last_page:
124 # Not last page: use BODY border_last
125 if document.rtf_body.border_last:
126 border_style = (
127 document.rtf_body.border_last[0][0]
128 if isinstance(document.rtf_body.border_last, list)
129 else document.rtf_body.border_last
130 )
132 if not (has_footnote_on_page or has_source_on_page):
133 # Apply to last data row
134 for col_idx in range(page_df_width):
135 page_attrs = self._apply_border_to_cell(
136 page_attrs,
137 page_df_height - 1,
138 col_idx,
139 "bottom",
140 border_style,
141 page_shape,
142 )
143 else:
144 # Apply to component
145 self._apply_footnote_source_borders(
146 document,
147 page,
148 has_footnote_on_page,
149 has_source_on_page,
150 border_style,
151 )
152 else:
153 # Last page: use PAGE border_last
154 if document.rtf_page.border_last:
155 # Only if this is truly the end (not just last page of a section,
156 # but for now we assume 1 section or last section)
157 # The original code checked `page_info["end_row"] == total_rows - 1`.
158 # Here we rely on `is_last_page` flag which comes from strategy.
160 if not (footnote_as_table_on_last or source_as_table_on_last):
161 # Apply to last data row
162 for col_idx in range(page_df_width):
163 page_attrs = self._apply_border_to_cell(
164 page_attrs,
165 page_df_height - 1,
166 col_idx,
167 "bottom",
168 document.rtf_page.border_last,
169 page_shape,
170 )
171 else:
172 # Apply to component
173 self._apply_footnote_source_borders(
174 document,
175 page,
176 has_footnote_on_page,
177 has_source_on_page,
178 document.rtf_page.border_last,
179 )
181 return page_attrs
183 def _apply_body_border_first(self, document, page_attrs, page_df_width, page_shape):
184 """Helper to apply body border_first logic."""
185 if isinstance(document.rtf_body.border_first, list):
186 border_first_row = document.rtf_body.border_first[0]
187 has_border_top = (
188 document.rtf_body.border_top
189 and isinstance(document.rtf_body.border_top, list)
190 and len(document.rtf_body.border_top[0]) > len(border_first_row)
191 )
193 for col_idx in range(page_df_width):
194 if col_idx < len(border_first_row):
195 border_style = border_first_row[col_idx]
196 else:
197 border_style = border_first_row[0]
199 if (
200 has_border_top
201 and col_idx < len(document.rtf_body.border_top[0])
202 and document.rtf_body.border_top[0][col_idx]
203 ):
204 border_style = document.rtf_body.border_top[0][col_idx]
206 self._apply_border_to_cell(
207 page_attrs, 0, col_idx, "top", border_style, page_shape
208 )
209 else:
210 for col_idx in range(page_df_width):
211 self._apply_border_to_cell(
212 page_attrs,
213 0,
214 col_idx,
215 "top",
216 document.rtf_body.border_first,
217 page_shape,
218 )
220 def _apply_footnote_source_borders(
221 self,
222 document,
223 page: PageContext,
224 has_footnote: bool,
225 has_source: bool,
226 border_style: str,
227 ):
228 """Apply borders to footnote/source in the page context."""
229 target_component = None
231 footnote_as_table = None
232 if has_footnote:
233 footnote_as_table = getattr(document.rtf_footnote, "as_table", True)
235 source_as_table = None
236 if has_source:
237 source_as_table = getattr(document.rtf_source, "as_table", False)
239 if has_source and source_as_table:
240 target_component = "source"
241 elif has_footnote and footnote_as_table:
242 target_component = "footnote"
244 if target_component:
245 page.component_borders[target_component] = border_style
247 def _apply_border_to_cell(
248 self,
249 page_attrs,
250 row_idx: int,
251 col_idx: int,
252 border_side: str,
253 border_style: str,
254 page_shape: tuple,
255 ):
256 """Apply specified border style to a specific cell using BroadcastValue"""
257 border_attr = f"border_{border_side}"
259 if not hasattr(page_attrs, border_attr):
260 return page_attrs
262 current_borders = getattr(page_attrs, border_attr)
263 border_broadcast = BroadcastValue(value=current_borders, dimension=page_shape)
264 border_broadcast.update_cell(row_idx, col_idx, border_style)
265 setattr(page_attrs, border_attr, border_broadcast.value)
266 return page_attrs