返回 ppt-master
dimensions.py
1 """Slide dimensions, format detection, EMU conversion, and constants."""
2
3 from __future__ import annotations
4
5 import re
6 import sys
7 from pathlib import Path
8 from xml.etree import ElementTree as ET
9
10 from ..canvas_contract import (
11 CanvasContractError,
12 ProjectViewBox,
13 parse_project_viewbox,
14 read_project_viewbox,
15 require_consistent_project_viewboxes,
16 )
17
18 # Import project utility modules
19 sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
20 try:
21 from project_utils import get_project_info
22 from config import CANVAS_FORMATS
23 except ImportError:
24 CANVAS_FORMATS = {
25 'ppt169': {'name': 'PPT 16:9', 'dimensions': '1280×720', 'viewbox': '0 0 1280 720'},
26 }
27
28 def get_project_info(path: str) -> dict:
29 return {'format': 'unknown', 'name': Path(path).name}
30
31 # EMU conversion constants
32 EMU_PER_INCH = 914400
33 EMU_PER_PIXEL = EMU_PER_INCH / 96
34
35 # XML namespaces
36 NAMESPACES = {
37 'a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
38 'r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
39 'p': 'http://schemas.openxmlformats.org/presentationml/2006/main',
40 'asvg': 'http://schemas.microsoft.com/office/drawing/2016/SVG/main',
41 }
42
43 # Register namespaces for ElementTree output
44 for prefix, uri in NAMESPACES.items():
45 ET.register_namespace(prefix, uri)
46
47
48 def get_slide_dimensions(
49 canvas_format: str,
50 custom_pixels: tuple[int, int] | None = None,
51 ) -> tuple[int, int]:
52 """Get slide dimensions in EMU units.
53
54 Args:
55 canvas_format: Canvas format key (e.g. 'ppt169').
56 custom_pixels: Optional custom pixel dimensions override.
57
58 Returns:
59 (width_emu, height_emu) tuple.
60 """
61 if custom_pixels:
62 width_px, height_px = custom_pixels
63 else:
64 if canvas_format not in CANVAS_FORMATS:
65 canvas_format = 'ppt169'
66
67 dimensions = CANVAS_FORMATS[canvas_format]['dimensions']
68 match = re.match(r'(\d+)[×x](\d+)', dimensions)
69 if match:
70 width_px = int(match.group(1))
71 height_px = int(match.group(2))
72 else:
73 width_px, height_px = 1280, 720
74
75 return int(width_px * EMU_PER_PIXEL), int(height_px * EMU_PER_PIXEL)
76
77
78 def get_pixel_dimensions(
79 canvas_format: str,
80 custom_pixels: tuple[int, int] | None = None,
81 ) -> tuple[int, int]:
82 """Get canvas pixel dimensions.
83
84 Args:
85 canvas_format: Canvas format key.
86 custom_pixels: Optional custom pixel dimensions override.
87
88 Returns:
89 (width_px, height_px) tuple.
90 """
91 if custom_pixels:
92 return custom_pixels
93
94 if canvas_format not in CANVAS_FORMATS:
95 canvas_format = 'ppt169'
96
97 dimensions = CANVAS_FORMATS[canvas_format]['dimensions']
98 match = re.match(r'(\d+)[×x](\d+)', dimensions)
99 if match:
100 return int(match.group(1)), int(match.group(2))
101 return 1280, 720
102
103
104 def get_viewbox_dimensions(svg_path: Path) -> tuple[float, float]:
105 """Extract pixel dimensions from SVG viewBox.
106
107 Args:
108 svg_path: Path to the SVG file.
109
110 Returns:
111 (width, height) in SVG pixels.
112
113 Raises:
114 CanvasContractError: The root canvas is missing or invalid.
115 """
116 return read_project_viewbox(svg_path).pixel_dimensions
117
118
119 def detect_format_from_svg(svg_path: Path) -> str | None:
120 """Detect canvas format from an SVG file's viewBox.
121
122 Args:
123 svg_path: Path to the SVG file.
124
125 Returns:
126 Canvas format key (e.g. 'ppt169'), or None if not detected.
127 """
128 viewbox = read_project_viewbox(svg_path)
129 for fmt_key, fmt_info in CANVAS_FORMATS.items():
130 expected = parse_project_viewbox(
131 fmt_info['viewbox'],
132 context=f"registered canvas {fmt_key!r}",
133 )
134 if viewbox == expected:
135 return fmt_key
136 return None
137
138
139 def resolve_svg_canvas(
140 svg_files: list[Path],
141 *,
142 canvas_format: str | None = None,
143 expected_viewbox: str | None = None,
144 ) -> tuple[ProjectViewBox, str | None]:
145 """Resolve one fail-closed canvas for every public/internal SVG."""
146 format_viewbox: str | None = None
147 if canvas_format is not None:
148 if canvas_format not in CANVAS_FORMATS:
149 raise CanvasContractError(f"Unsupported canvas format: {canvas_format}")
150 format_viewbox = CANVAS_FORMATS[canvas_format]['viewbox']
151
152 if expected_viewbox is not None and format_viewbox is not None:
153 locked = parse_project_viewbox(
154 expected_viewbox,
155 context="locked canvas viewBox",
156 )
157 selected = parse_project_viewbox(
158 format_viewbox,
159 context=f"canvas format {canvas_format!r}",
160 )
161 if locked != selected:
162 raise CanvasContractError(
163 f"canvas format {canvas_format!r} ({selected.canonical}) conflicts "
164 f"with the locked canvas ({locked.canonical})"
165 )
166
167 required = expected_viewbox if expected_viewbox is not None else format_viewbox
168 if expected_viewbox is not None:
169 expected_label = "the locked canvas"
170 elif canvas_format is not None:
171 expected_label = f"canvas format {canvas_format!r}"
172 else:
173 expected_label = "the first SVG canvas"
174 viewbox = require_consistent_project_viewboxes(
175 svg_files,
176 expected_viewbox=required,
177 expected_label=expected_label,
178 )
179 detected_format = canvas_format
180 if detected_format is None:
181 for fmt_key, fmt_info in CANVAS_FORMATS.items():
182 registered = parse_project_viewbox(
183 fmt_info['viewbox'],
184 context=f"registered canvas {fmt_key!r}",
185 )
186 if viewbox == registered:
187 detected_format = fmt_key
188 break
189 return viewbox, detected_format
190
190 lines PYTHON