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'
# 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):
# Prepare list marker based on type
if list_type == 'ordered':
@@ -1654,21 +1657,37 @@ class PDFGeneratorService:
# Add default list item spacing if not specified
# This ensures consistent spacing between list items
if 'spacing_after' not in item.metadata or item.metadata.get('spacing_after', 0) == 0:
# Default list item spacing: 3 points between items
item.metadata['spacing_after'] = 3.0
desired_spacing_after = item.metadata.get('spacing_after', 0)
if desired_spacing_after == 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
item.metadata['_apply_spacing_after'] = True
# Draw the list item with cumulative Y offset
self._draw_text_element_direct(pdf_canvas, item, page_height, y_offset=cumulative_y_offset)
# Draw the list item using text element renderer
self._draw_text_element_direct(pdf_canvas, item, page_height)
# Calculate spacing to add after this item
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(
self,
pdf_canvas: canvas.Canvas,
element: 'DocumentElement',
page_height: float
page_height: float,
y_offset: float = 0
):
"""
Draw text element with Direct track rich formatting.
@@ -1679,6 +1698,7 @@ class PDFGeneratorService:
pdf_canvas: ReportLab canvas object
element: DocumentElement with text content
page_height: Page height for coordinate transformation
y_offset: Optional Y coordinate offset (for list spacing), in PDF coordinates
"""
try:
text_content = element.get_text()
@@ -1693,7 +1713,7 @@ class PDFGeneratorService:
# Transform coordinates (top-left origin → bottom-left origin)
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_height = bbox.y1 - bbox.y0
@@ -1743,10 +1763,9 @@ class PDFGeneratorService:
# Get paragraph spacing
# 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_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
lines = text_content.split('\n')
@@ -1761,13 +1780,6 @@ class PDFGeneratorService:
# Apply paragraph spacing before (shift starting position up)
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
for i, line in enumerate(lines):
if not line.strip():
@@ -1844,12 +1856,12 @@ class PDFGeneratorService:
actual_text_height = len(lines) * line_height
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)
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]}... "
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"actual_height={actual_text_height:.1f}, bbox_bottom_margin={bbox_bottom_margin:.1f})")