fix: implement actual list item spacing with Y offset adjustment

Previous implementation only expanded bbox_height which had no visual effect.
New implementation properly applies spacing_after between list items.

Changes:
1. Track cumulative Y offset in _draw_list_elements_direct
2. Calculate actual gap between adjacent list items
3. If actual gap < desired spacing_after, add offset to push next item down
4. Pass y_offset parameter to _draw_text_element_direct
5. Apply y_offset when calculating pdf_y coordinate

Implementation details:
- Default 3pt spacing_after for list items (except last item in group)
- Compare actual_gap (next.bbox.y0 - current.bbox.y1) with desired spacing
- Cumulative offset ensures spacing compounds across multiple items
- Negative offset in PDF coordinates (Y increases upward)
- Debug logging shows when additional spacing is applied

This now creates actual visual spacing between list items in the PDF output.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
egg
2025-11-24 11:35:58 +08:00
parent 1ac8e82f47
commit b1de7616e4
2 changed files with 38 additions and 25 deletions

View File

@@ -1636,6 +1636,9 @@ class PDFGeneratorService:
list_type = 'unordered' list_type = 'unordered'
# Draw each item in the group with proper spacing # Draw each item in the group with proper spacing
# Track cumulative Y offset to apply spacing_after between items
cumulative_y_offset = 0
for item_idx, item in enumerate(group): for item_idx, item in enumerate(group):
# Prepare list marker based on type # Prepare list marker based on type
if list_type == 'ordered': if list_type == 'ordered':
@@ -1654,21 +1657,37 @@ class PDFGeneratorService:
# Add default list item spacing if not specified # Add default list item spacing if not specified
# This ensures consistent spacing between list items # This ensures consistent spacing between list items
if 'spacing_after' not in item.metadata or item.metadata.get('spacing_after', 0) == 0: desired_spacing_after = item.metadata.get('spacing_after', 0)
# Default list item spacing: 3 points between items if desired_spacing_after == 0:
item.metadata['spacing_after'] = 3.0 # Default list item spacing: 3 points between items (except last item)
if item_idx < len(group) - 1:
desired_spacing_after = 3.0
item.metadata['spacing_after'] = desired_spacing_after
# Mark this as requiring spacing application # Draw the list item with cumulative Y offset
item.metadata['_apply_spacing_after'] = True self._draw_text_element_direct(pdf_canvas, item, page_height, y_offset=cumulative_y_offset)
# Draw the list item using text element renderer # Calculate spacing to add after this item
self._draw_text_element_direct(pdf_canvas, item, page_height) if item_idx < len(group) - 1 and desired_spacing_after > 0:
next_item = group[item_idx + 1]
# Calculate actual vertical gap between items (in document coordinates)
# Note: Y increases downward in document coordinates
actual_gap = next_item.bbox.y0 - item.bbox.y1
# If actual gap is less than desired spacing, add offset to push next item down
if actual_gap < desired_spacing_after:
additional_spacing = desired_spacing_after - actual_gap
cumulative_y_offset -= additional_spacing # Negative because PDF Y increases upward
logger.debug(f"Adding {additional_spacing:.1f}pt spacing after list item {item.element_id} "
f"(actual_gap={actual_gap:.1f}pt, desired={desired_spacing_after:.1f}pt)")
def _draw_text_element_direct( def _draw_text_element_direct(
self, self,
pdf_canvas: canvas.Canvas, pdf_canvas: canvas.Canvas,
element: 'DocumentElement', element: 'DocumentElement',
page_height: float page_height: float,
y_offset: float = 0
): ):
""" """
Draw text element with Direct track rich formatting. Draw text element with Direct track rich formatting.
@@ -1679,6 +1698,7 @@ class PDFGeneratorService:
pdf_canvas: ReportLab canvas object pdf_canvas: ReportLab canvas object
element: DocumentElement with text content element: DocumentElement with text content
page_height: Page height for coordinate transformation page_height: Page height for coordinate transformation
y_offset: Optional Y coordinate offset (for list spacing), in PDF coordinates
""" """
try: try:
text_content = element.get_text() text_content = element.get_text()
@@ -1693,7 +1713,7 @@ class PDFGeneratorService:
# Transform coordinates (top-left origin → bottom-left origin) # Transform coordinates (top-left origin → bottom-left origin)
pdf_x = bbox.x0 pdf_x = bbox.x0
pdf_y = page_height - bbox.y1 # Use bottom of bbox pdf_y = page_height - bbox.y1 + y_offset # Use bottom of bbox + apply offset
bbox_width = bbox.x1 - bbox.x0 bbox_width = bbox.x1 - bbox.x0
bbox_height = bbox.y1 - bbox.y0 bbox_height = bbox.y1 - bbox.y0
@@ -1743,10 +1763,9 @@ class PDFGeneratorService:
# Get paragraph spacing # Get paragraph spacing
# spacing_before: Applied by adjusting starting Y position (pdf_y) # spacing_before: Applied by adjusting starting Y position (pdf_y)
# spacing_after: Applied for list items marked with _apply_spacing_after # spacing_after: Applied via y_offset in _draw_list_elements_direct for list items
paragraph_spacing_before = element.metadata.get('spacing_before', 0) if element.metadata else 0 paragraph_spacing_before = element.metadata.get('spacing_before', 0) if element.metadata else 0
paragraph_spacing_after = element.metadata.get('spacing_after', 0) if element.metadata else 0 paragraph_spacing_after = element.metadata.get('spacing_after', 0) if element.metadata else 0
apply_spacing_after = element.metadata.get('_apply_spacing_after', False) if element.metadata else False
# Handle line breaks # Handle line breaks
lines = text_content.split('\n') lines = text_content.split('\n')
@@ -1761,13 +1780,6 @@ class PDFGeneratorService:
# Apply paragraph spacing before (shift starting position up) # Apply paragraph spacing before (shift starting position up)
pdf_y += paragraph_spacing_before pdf_y += paragraph_spacing_before
# Apply list item spacing after by expanding bbox height
# This creates visual space between list items
if apply_spacing_after and paragraph_spacing_after > 0:
# Adjust bbox to include spacing_after
# This is done by conceptually expanding the element's vertical space
bbox_height += paragraph_spacing_after
# Draw each line with alignment # Draw each line with alignment
for i, line in enumerate(lines): for i, line in enumerate(lines):
if not line.strip(): if not line.strip():
@@ -1844,12 +1856,12 @@ class PDFGeneratorService:
actual_text_height = len(lines) * line_height actual_text_height = len(lines) * line_height
bbox_bottom_margin = bbox_height - actual_text_height - paragraph_spacing_before bbox_bottom_margin = bbox_height - actual_text_height - paragraph_spacing_before
# Note: For list items with _apply_spacing_after, spacing_after is added to bbox_height # Note: For list items, spacing_after is applied via y_offset in _draw_list_elements_direct
# For other elements, spacing is inherent in element positioning (bbox-based layout) # For other elements, spacing is inherent in element positioning (bbox-based layout)
list_info = f", list={list_type}, level={list_level}" if is_list_item else "" list_info = f", list={list_type}, level={list_level}" if is_list_item else ""
spacing_applied = f", spacing_after_applied={apply_spacing_after}" if is_list_item else "" y_offset_info = f", y_offset={y_offset:.1f}pt" if y_offset != 0 else ""
logger.debug(f"Drew text element: {text_content[:30]}... " logger.debug(f"Drew text element: {text_content[:30]}... "
f"({len(lines)} lines, align={alignment}, indent={indent}{list_info}{spacing_applied}, " f"({len(lines)} lines, align={alignment}, indent={indent}{list_info}{y_offset_info}, "
f"spacing_before={paragraph_spacing_before}, spacing_after={paragraph_spacing_after}, " f"spacing_before={paragraph_spacing_before}, spacing_after={paragraph_spacing_after}, "
f"actual_height={actual_text_height:.1f}, bbox_bottom_margin={bbox_bottom_margin:.1f})") f"actual_height={actual_text_height:.1f}, bbox_bottom_margin={bbox_bottom_margin:.1f})")

View File

@@ -115,10 +115,11 @@
- [x] Calculate marker width before rendering (line 1758) - [x] Calculate marker width before rendering (line 1758)
- [x] Add marker_width to subsequent line indentation (lines 1770-1772) - [x] Add marker_width to subsequent line indentation (lines 1770-1772)
- [x] 6.2.5 Remove original markers from text content (lines 1716-1723) - [x] 6.2.5 Remove original markers from text content (lines 1716-1723)
- [x] 6.2.6 Dedicated list item spacing (lines 1655-1662, 1764-1769) - [x] 6.2.6 Dedicated list item spacing (lines 1658-1683)
- [x] Default 3pt spacing_after for list items - [x] Default 3pt spacing_after for list items (except last item)
- [x] Applied by expanding bbox_height (line 1769) - [x] Calculate actual gap between adjacent items (line 1676)
- [x] Marked with _apply_spacing_after flag - [x] Apply cumulative Y offset to push items down if gap < desired (lines 1678-1683)
- [x] Pass y_offset to _draw_text_element_direct (line 1668, 1690, 1716)
- [x] 6.2.7 Maintain list grouping via proximity (max_gap=30pt, lines 1597-1607) - [x] 6.2.7 Maintain list grouping via proximity (max_gap=30pt, lines 1597-1607)
### 7. Span-Level Rendering (Advanced) ### 7. Span-Level Rendering (Advanced)