| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223 |
- """
- Dynamic page layout engine for the universal data report generator.
- Provides pre-defined layout templates and layout calculation utilities.
- """
- from pptx.util import Emu, Pt
- from pptx.dml.color import RGBColor
- from dataclasses import dataclass
- from typing import Optional
- SLIDE_WIDTH = 16256000
- SLIDE_HEIGHT = 9144000
- MARGIN_LEFT = Emu(762000)
- MARGIN_RIGHT = Emu(762000)
- MARGIN_TOP = Emu(254000)
- CONTENT_TOP_BASE = Emu(1600200)
- FOOTER_TOP = Emu(8824000)
- FOOTER_HEIGHT = Emu(320000)
- CONTENT_WIDTH = SLIDE_WIDTH - MARGIN_LEFT - MARGIN_RIGHT
- @dataclass
- class LayoutZone:
- x: int
- y: int
- width: int
- height: int
- zone_type: str
- def calculate_content_area(content_top_emu: int = None) -> LayoutZone:
- top = content_top_emu or int(CONTENT_TOP_BASE)
- height = FOOTER_TOP - top - Emu(100000)
- return LayoutZone(
- x=int(MARGIN_LEFT),
- y=top,
- width=int(CONTENT_WIDTH),
- height=int(height),
- zone_type='content_area',
- )
- def get_kpi_grid(content_top_emu: int = None, cols: int = 3, rows: int = 2,
- card_width_emu: int = 4699000, card_height_emu: int = 3048000,
- gap_x_emu: int = 444500, gap_y_emu: int = 381000) -> list[LayoutZone]:
- start_y = max(int(CONTENT_TOP_BASE), content_top_emu or int(CONTENT_TOP_BASE))
- zones = []
- for row in range(rows):
- for col in range(cols):
- x = int(MARGIN_LEFT) + col * (card_width_emu + gap_x_emu)
- y = start_y + row * (card_height_emu + gap_y_emu)
- zones.append(LayoutZone(x=x, y=y, width=card_width_emu, height=card_height_emu, zone_type='kpi_card'))
- return zones
- def get_chart_left_zone(content_top_emu: int = None, chart_ratio: float = 0.6) -> LayoutZone:
- content = calculate_content_area(content_top_emu)
- chart_w = int(content.width * chart_ratio) - Emu(200000)
- return LayoutZone(
- x=content.x,
- y=content.y,
- width=chart_w,
- height=content.height,
- zone_type='chart_left',
- )
- def get_insight_right_zone(content_top_emu: int = None, chart_ratio: float = 0.6) -> LayoutZone:
- content = calculate_content_area(content_top_emu)
- chart_w = int(content.width * chart_ratio)
- text_left = content.x + chart_w + Emu(200000)
- text_w = content.x + content.width - text_left
- return LayoutZone(
- x=text_left,
- y=content.y,
- width=text_w,
- height=content.height,
- zone_type='insight_right',
- )
- def get_full_width_zone(content_top_emu: int = None) -> LayoutZone:
- return calculate_content_area(content_top_emu)
- def get_two_column_zones(content_top_emu: int = None, gap_emu: int = 381000) -> tuple[LayoutZone, LayoutZone]:
- content = calculate_content_area(content_top_emu)
- half_w = (content.width - gap_emu) // 2
- left = LayoutZone(x=content.x, y=content.y, width=half_w, height=content.height, zone_type='column_left')
- right = LayoutZone(x=content.x + half_w + gap_emu, y=content.y, width=half_w, height=content.height, zone_type='column_right')
- return left, right
- def get_two_row_zones(content_top_emu: int = None, gap_emu: int = 381000,
- top_ratio: float = 0.55) -> tuple[LayoutZone, LayoutZone]:
- content = calculate_content_area(content_top_emu)
- top_h = int(content.height * top_ratio)
- top = LayoutZone(x=content.x, y=content.y, width=content.width, height=top_h, zone_type='row_top')
- bottom = LayoutZone(
- x=content.x,
- y=content.y + top_h + gap_emu,
- width=content.width,
- height=content.height - top_h - gap_emu,
- zone_type='row_bottom',
- )
- return top, bottom
- def get_card_grid(n: int, content_top_emu: int = None, max_cols: int = 3) -> list[LayoutZone]:
- content = calculate_content_area(content_top_emu)
- cols = min(max_cols, n)
- rows = (n + cols - 1) // cols
- card_w = (content.width - (cols - 1) * Emu(254000)) // cols
- card_h = (content.height - (rows - 1) * Emu(254000)) // rows
- zones = []
- for i in range(n):
- col = i % cols
- row = i // cols
- x = content.x + col * (card_w + Emu(254000))
- y = content.y + row * (card_h + Emu(254000))
- zones.append(LayoutZone(x=x, y=y, width=card_w, height=card_h, zone_type=f'card_{i}'))
- return zones
- def get_alert_card_zones(n: int, content_top_emu: int = None) -> list[LayoutZone]:
- content = calculate_content_area(content_top_emu)
- card_h = Emu(2286000)
- gap = Emu(254000)
- return get_card_grid(n, content_top_emu, max_cols=3)
- def get_issue_card_zones(n: int, content_top_emu: int = None) -> list[LayoutZone]:
- content = calculate_content_area(content_top_emu)
- card_h = Emu(2032000)
- gap = Emu(254000)
- start_y = content.y
- zones = []
- for i in range(min(n, 3)):
- y = start_y + i * (card_h + gap)
- zones.append(LayoutZone(x=content.x, y=y, width=content.width, height=card_h, zone_type=f'issue_{i}'))
- return zones
- def get_table_zone(content_top_emu: int = None, ratio: float = 0.5) -> LayoutZone:
- content = calculate_content_area(content_top_emu)
- return LayoutZone(
- x=content.x,
- y=content.y + int(content.height * ratio) + Emu(200000),
- width=content.width,
- height=int(content.height * (1 - ratio)),
- zone_type='table_bottom',
- )
- def detect_layout_slots(slide) -> dict:
- slots = {
- 'has_header': False,
- 'has_footer': False,
- 'has_page_title': False,
- 'content_top': int(CONTENT_TOP_BASE),
- 'content_width': int(CONTENT_WIDTH),
- 'content_height': FOOTER_TOP - int(CONTENT_TOP_BASE) - Emu(100000),
- }
- for shape in slide.shapes:
- if shape.has_text_frame:
- text = shape.text_frame.text
- if 'page_title' in text or '报告' in text:
- slots['has_page_title'] = True
- if '数据来源' in text:
- slots['has_footer'] = True
- slots['footer_top'] = int(shape.top)
- if shape.top + shape.height < Emu(1300000):
- slots['has_header'] = True
- return slots
- def ensure_safe_position(shape, slide_width: int, slide_height: int) -> bool:
- margin = Emu(254000)
- adjusted = False
- if shape.left < 0:
- shape.left = margin
- adjusted = True
- if shape.top < 0:
- shape.top = margin
- adjusted = True
- if shape.left + shape.width > slide_width:
- shape.left = slide_width - shape.width - margin
- adjusted = True
- if shape.top + shape.height > slide_height:
- shape.top = slide_height - shape.height - margin
- adjusted = True
- return adjusted
- def calculate_fill_ratio(slide, content_top_emu: int = None) -> float:
- content = calculate_content_area(content_top_emu)
- total_area = content.width * content.height
- if total_area <= 0:
- return 0.0
- filled_area = 0
- for shape in slide.shapes:
- sx = int(shape.left)
- sy = int(shape.top)
- sw = int(shape.width)
- sh = int(shape.height)
- if sy < content.y:
- continue
- if sy > content.y + content.height:
- continue
- overlap_x = max(0, min(sx + sw, content.x + content.width) - max(sx, content.x))
- overlap_y = max(0, min(sy + sh, content.y + content.height) - max(sy, content.y))
- filled_area += overlap_x * overlap_y
- return min(1.0, filled_area / total_area)
- if __name__ == '__main__':
- ca = calculate_content_area()
- print(f"Content area: {ca.width}x{ca.height}")
- kpis = get_kpi_grid()
- for i, z in enumerate(kpis):
- print(f"KPI {i}: x={z.x}, y={z.y}")
- print(f"Fill ratio test: hypothetical")
|