kicad-sch-api 0.4.0__py3-none-any.whl → 0.4.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kicad-sch-api might be problematic. Click here for more details.

Files changed (57) hide show
  1. kicad_sch_api/__init__.py +2 -2
  2. kicad_sch_api/cli/__init__.py +45 -0
  3. kicad_sch_api/cli/base.py +302 -0
  4. kicad_sch_api/cli/bom.py +164 -0
  5. kicad_sch_api/cli/erc.py +229 -0
  6. kicad_sch_api/cli/export_docs.py +289 -0
  7. kicad_sch_api/cli/netlist.py +94 -0
  8. kicad_sch_api/cli/types.py +43 -0
  9. kicad_sch_api/core/collections/__init__.py +5 -0
  10. kicad_sch_api/core/collections/base.py +248 -0
  11. kicad_sch_api/core/component_bounds.py +5 -0
  12. kicad_sch_api/core/components.py +142 -47
  13. kicad_sch_api/core/config.py +85 -3
  14. kicad_sch_api/core/factories/__init__.py +5 -0
  15. kicad_sch_api/core/factories/element_factory.py +276 -0
  16. kicad_sch_api/core/formatter.py +22 -5
  17. kicad_sch_api/core/junctions.py +26 -75
  18. kicad_sch_api/core/labels.py +28 -52
  19. kicad_sch_api/core/managers/file_io.py +3 -2
  20. kicad_sch_api/core/managers/metadata.py +6 -5
  21. kicad_sch_api/core/managers/validation.py +3 -2
  22. kicad_sch_api/core/managers/wire.py +7 -1
  23. kicad_sch_api/core/nets.py +38 -43
  24. kicad_sch_api/core/no_connects.py +29 -53
  25. kicad_sch_api/core/parser.py +75 -1765
  26. kicad_sch_api/core/schematic.py +211 -148
  27. kicad_sch_api/core/texts.py +28 -55
  28. kicad_sch_api/core/types.py +59 -18
  29. kicad_sch_api/core/wires.py +27 -75
  30. kicad_sch_api/parsers/elements/__init__.py +22 -0
  31. kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
  32. kicad_sch_api/parsers/elements/label_parser.py +194 -0
  33. kicad_sch_api/parsers/elements/library_parser.py +165 -0
  34. kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
  35. kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
  36. kicad_sch_api/parsers/elements/symbol_parser.py +313 -0
  37. kicad_sch_api/parsers/elements/text_parser.py +250 -0
  38. kicad_sch_api/parsers/elements/wire_parser.py +242 -0
  39. kicad_sch_api/parsers/utils.py +80 -0
  40. kicad_sch_api/validation/__init__.py +25 -0
  41. kicad_sch_api/validation/erc.py +171 -0
  42. kicad_sch_api/validation/erc_models.py +203 -0
  43. kicad_sch_api/validation/pin_matrix.py +243 -0
  44. kicad_sch_api/validation/validators.py +391 -0
  45. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/METADATA +17 -9
  46. kicad_sch_api-0.4.2.dist-info/RECORD +87 -0
  47. kicad_sch_api/core/manhattan_routing.py +0 -430
  48. kicad_sch_api/core/simple_manhattan.py +0 -228
  49. kicad_sch_api/core/wire_routing.py +0 -380
  50. kicad_sch_api/parsers/label_parser.py +0 -254
  51. kicad_sch_api/parsers/symbol_parser.py +0 -222
  52. kicad_sch_api/parsers/wire_parser.py +0 -99
  53. kicad_sch_api-0.4.0.dist-info/RECORD +0 -67
  54. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/WHEEL +0 -0
  55. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/entry_points.txt +0 -0
  56. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/licenses/LICENSE +0 -0
  57. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/top_level.txt +0 -0
@@ -1,380 +0,0 @@
1
- """
2
- Simple wire routing and connectivity detection for KiCAD schematics.
3
-
4
- Provides basic wire routing between component pins and pin connectivity detection.
5
- All positioning follows KiCAD's 1.27mm grid alignment rules.
6
- """
7
-
8
- from typing import List, Optional, Tuple, Union
9
-
10
- from .types import Point
11
-
12
-
13
- def snap_to_kicad_grid(
14
- position: Union[Point, Tuple[float, float]], grid_size: float = 1.27
15
- ) -> Point:
16
- """
17
- Snap position to KiCAD's standard 1.27mm grid.
18
-
19
- KiCAD uses a 1.27mm (0.05 inch) grid for precise electrical connections.
20
- ALL components, wires, and labels must be exactly on grid points.
21
-
22
- Args:
23
- position: Point or (x, y) tuple to snap
24
- grid_size: Grid size in mm (default 1.27mm)
25
-
26
- Returns:
27
- Point snapped to grid
28
- """
29
- if isinstance(position, Point):
30
- x, y = position.x, position.y
31
- else:
32
- x, y = position
33
-
34
- # Round to nearest grid point
35
- snapped_x = round(x / grid_size) * grid_size
36
- snapped_y = round(y / grid_size) * grid_size
37
-
38
- return Point(snapped_x, snapped_y)
39
-
40
-
41
- def calculate_component_position_for_grid_pins(
42
- pin_offsets: List[Tuple[float, float]], target_pin_grid_pos: Point, target_pin_index: int = 0
43
- ) -> Point:
44
- """
45
- Calculate component position so that pins land exactly on grid points.
46
-
47
- Args:
48
- pin_offsets: List of (x_offset, y_offset) tuples for each pin relative to component center
49
- target_pin_grid_pos: Desired grid position for the target pin
50
- target_pin_index: Index of pin to align to grid (default: pin 0)
51
-
52
- Returns:
53
- Component center position that places target pin on grid
54
- """
55
- target_offset_x, target_offset_y = pin_offsets[target_pin_index]
56
-
57
- # Component center = target pin position - pin offset
58
- comp_center_x = target_pin_grid_pos.x - target_offset_x
59
- comp_center_y = target_pin_grid_pos.y - target_offset_y
60
-
61
- return Point(comp_center_x, comp_center_y)
62
-
63
-
64
- def get_resistor_grid_position(target_pin1_grid: Point) -> Point:
65
- """
66
- Get component position for Device:R resistor so pin 1 is at target grid position.
67
-
68
- Device:R resistor pin offsets: Pin 1 = (0, +3.81), Pin 2 = (0, -3.81)
69
-
70
- Args:
71
- target_pin1_grid: Desired grid position for pin 1
72
-
73
- Returns:
74
- Component center position
75
- """
76
- # Device:R pin offsets (standard KiCAD library values)
77
- pin_offsets = [(0.0, 3.81), (0.0, -3.81)] # Pin 1 and Pin 2 offsets
78
- return calculate_component_position_for_grid_pins(pin_offsets, target_pin1_grid, 0)
79
-
80
-
81
- def route_pins_direct(pin1_position: Point, pin2_position: Point) -> Point:
82
- """
83
- Simple direct routing between two pins.
84
- Just draws a straight wire - ok to go through other components.
85
-
86
- Args:
87
- pin1_position: Position of first pin
88
- pin2_position: Position of second pin
89
-
90
- Returns:
91
- End point (pin2_position) for wire creation
92
- """
93
- return pin2_position
94
-
95
-
96
- def are_pins_connected(
97
- schematic, comp1_ref: str, pin1_num: str, comp2_ref: str, pin2_num: str
98
- ) -> bool:
99
- """
100
- Detect if two component pins are connected via wires or labels (local or hierarchical).
101
-
102
- Checks for electrical connectivity through:
103
- 1. Direct wire connections
104
- 2. Indirect wire network connections
105
- 3. Local labels on connected nets
106
- 4. Hierarchical labels for inter-sheet connections
107
-
108
- Args:
109
- schematic: The schematic object to analyze
110
- comp1_ref: Reference of first component (e.g., 'R1')
111
- pin1_num: Pin number on first component (e.g., '1')
112
- comp2_ref: Reference of second component (e.g., 'R2')
113
- pin2_num: Pin number on second component (e.g., '2')
114
-
115
- Returns:
116
- True if pins are electrically connected, False otherwise
117
- """
118
- # Get pin positions
119
- pin1_pos = schematic.get_component_pin_position(comp1_ref, pin1_num)
120
- pin2_pos = schematic.get_component_pin_position(comp2_ref, pin2_num)
121
-
122
- if not pin1_pos or not pin2_pos:
123
- return False
124
-
125
- # 1. Check for direct wire connection between the pins
126
- for wire in schematic.wires:
127
- wire_start = wire.points[0]
128
- wire_end = wire.points[-1] # Last point for multi-segment wires
129
-
130
- # Check if wire directly connects the two pins
131
- if (
132
- wire_start.x == pin1_pos.x
133
- and wire_start.y == pin1_pos.y
134
- and wire_end.x == pin2_pos.x
135
- and wire_end.y == pin2_pos.y
136
- ) or (
137
- wire_start.x == pin2_pos.x
138
- and wire_start.y == pin2_pos.y
139
- and wire_end.x == pin1_pos.x
140
- and wire_end.y == pin1_pos.y
141
- ):
142
- return True
143
-
144
- # 2. Check for indirect connection through wire network
145
- visited_wires = set()
146
- if _pins_connected_via_wire_network(schematic, pin1_pos, pin2_pos, visited_wires):
147
- return True
148
-
149
- # 3. Check for connection via local labels
150
- if _pins_connected_via_labels(schematic, pin1_pos, pin2_pos):
151
- return True
152
-
153
- # 4. Check for connection via hierarchical labels
154
- if _pins_connected_via_hierarchical_labels(schematic, pin1_pos, pin2_pos):
155
- return True
156
-
157
- return False
158
-
159
-
160
- def _pins_connected_via_wire_network(
161
- schematic, pin1_pos: Point, pin2_pos: Point, visited_wires: set
162
- ) -> bool:
163
- """
164
- Check if pins are connected through wire network tracing.
165
-
166
- Args:
167
- schematic: The schematic object
168
- pin1_pos: First pin position
169
- pin2_pos: Second pin position
170
- visited_wires: Set to track visited wires
171
-
172
- Returns:
173
- True if pins connected via wire network
174
- """
175
- # Find all wires connected to pin1
176
- pin1_wires = []
177
- for wire in schematic.wires:
178
- for point in wire.points:
179
- if point.x == pin1_pos.x and point.y == pin1_pos.y:
180
- pin1_wires.append(wire)
181
- break
182
-
183
- # For each wire connected to pin1, trace the network to see if it reaches pin2
184
- visited_wire_uuids = set() # Use UUIDs instead of Wire objects
185
- for start_wire in pin1_wires:
186
- if _trace_wire_network(schematic, start_wire, pin2_pos, visited_wire_uuids):
187
- return True
188
-
189
- return False
190
-
191
-
192
- def _pins_connected_via_labels(schematic, pin1_pos: Point, pin2_pos: Point) -> bool:
193
- """
194
- Check if pins are connected via local labels (net names).
195
-
196
- Two pins are connected if they're on nets with the same label name.
197
-
198
- Args:
199
- schematic: The schematic object
200
- pin1_pos: First pin position
201
- pin2_pos: Second pin position
202
-
203
- Returns:
204
- True if pins connected via same local label
205
- """
206
- # Get labels connected to pin1's net
207
- pin1_labels = _get_net_labels_at_position(schematic, pin1_pos)
208
- if not pin1_labels:
209
- return False
210
-
211
- # Get labels connected to pin2's net
212
- pin2_labels = _get_net_labels_at_position(schematic, pin2_pos)
213
- if not pin2_labels:
214
- return False
215
-
216
- # Check if any label names match (case-insensitive)
217
- pin1_names = {label.text.upper() for label in pin1_labels}
218
- pin2_names = {label.text.upper() for label in pin2_labels}
219
-
220
- return bool(pin1_names.intersection(pin2_names))
221
-
222
-
223
- def _pins_connected_via_hierarchical_labels(schematic, pin1_pos: Point, pin2_pos: Point) -> bool:
224
- """
225
- Check if pins are connected via hierarchical labels.
226
-
227
- Hierarchical labels create connections between different sheets in a hierarchy.
228
-
229
- Args:
230
- schematic: The schematic object
231
- pin1_pos: First pin position
232
- pin2_pos: Second pin position
233
-
234
- Returns:
235
- True if pins connected via hierarchical labels
236
- """
237
- # Get hierarchical labels connected to pin1's net
238
- pin1_hier_labels = _get_hierarchical_labels_at_position(schematic, pin1_pos)
239
- if not pin1_hier_labels:
240
- return False
241
-
242
- # Get hierarchical labels connected to pin2's net
243
- pin2_hier_labels = _get_hierarchical_labels_at_position(schematic, pin2_pos)
244
- if not pin2_hier_labels:
245
- return False
246
-
247
- # Check if any hierarchical label names match (case-insensitive)
248
- pin1_names = {label.text.upper() for label in pin1_hier_labels}
249
- pin2_names = {label.text.upper() for label in pin2_hier_labels}
250
-
251
- return bool(pin1_names.intersection(pin2_names))
252
-
253
-
254
- def _get_net_labels_at_position(schematic, position: Point) -> List:
255
- """
256
- Get all local labels connected to the wire network at the given position.
257
-
258
- Uses coordinate proximity matching like kicad-skip (0.6mm tolerance).
259
-
260
- Args:
261
- schematic: The schematic object
262
- position: Pin position to check
263
-
264
- Returns:
265
- List of labels connected to this position's net
266
- """
267
- connected_labels = []
268
- tolerance = 0.0 # Zero tolerance - KiCAD requires exact coordinate matching
269
-
270
- # Find all wires connected to this position
271
- connected_wires = []
272
- for wire in schematic.wires:
273
- for point in wire.points:
274
- if point.x == position.x and point.y == position.y:
275
- connected_wires.append(wire)
276
- break
277
-
278
- # Find labels near any connected wire points
279
- labels_data = schematic._data.get("labels", [])
280
- for label_dict in labels_data:
281
- label_pos_dict = label_dict.get("position", {})
282
- label_pos = Point(label_pos_dict.get("x", 0), label_pos_dict.get("y", 0))
283
-
284
- # Check if label is near any connected wire
285
- for wire in connected_wires:
286
- for wire_point in wire.points:
287
- if label_pos.x == wire_point.x and label_pos.y == wire_point.y:
288
- # Create a simple object with text attribute
289
- class SimpleLabel:
290
- def __init__(self, text):
291
- self.text = text
292
-
293
- connected_labels.append(SimpleLabel(label_dict.get("text", "")))
294
- break
295
-
296
- return connected_labels
297
-
298
-
299
- def _get_hierarchical_labels_at_position(schematic, position: Point) -> List:
300
- """
301
- Get all hierarchical labels connected to the wire network at the given position.
302
-
303
- Args:
304
- schematic: The schematic object
305
- position: Pin position to check
306
-
307
- Returns:
308
- List of hierarchical labels connected to this position's net
309
- """
310
- connected_labels = []
311
- tolerance = 0.0 # Zero tolerance - KiCAD requires exact coordinate matching
312
-
313
- # Find all wires connected to this position
314
- connected_wires = []
315
- for wire in schematic.wires:
316
- for point in wire.points:
317
- if point.x == position.x and point.y == position.y:
318
- connected_wires.append(wire)
319
- break
320
-
321
- # Find hierarchical labels near any connected wire points
322
- hier_labels_data = schematic._data.get("hierarchical_labels", [])
323
- for label_dict in hier_labels_data:
324
- label_pos_dict = label_dict.get("position", {})
325
- label_pos = Point(label_pos_dict.get("x", 0), label_pos_dict.get("y", 0))
326
-
327
- # Check if hierarchical label is near any connected wire
328
- for wire in connected_wires:
329
- for wire_point in wire.points:
330
- if label_pos.x == wire_point.x and label_pos.y == wire_point.y:
331
- # Create a simple object with text attribute
332
- class SimpleLabel:
333
- def __init__(self, text):
334
- self.text = text
335
-
336
- connected_labels.append(SimpleLabel(label_dict.get("text", "")))
337
- break
338
-
339
- return connected_labels
340
-
341
-
342
- def _trace_wire_network(
343
- schematic, current_wire, target_position: Point, visited_uuids: set
344
- ) -> bool:
345
- """
346
- Recursively trace wire network to find if it reaches target position.
347
-
348
- Args:
349
- schematic: The schematic object
350
- current_wire: Current wire being traced
351
- target_position: Target pin position we're looking for
352
- visited_uuids: Set of already visited wire UUIDs to prevent infinite loops
353
-
354
- Returns:
355
- True if network reaches target position
356
- """
357
- wire_uuid = current_wire.uuid
358
- if wire_uuid in visited_uuids:
359
- return False
360
-
361
- visited_uuids.add(wire_uuid)
362
-
363
- # Check if current wire reaches target
364
- for point in current_wire.points:
365
- if point.x == target_position.x and point.y == target_position.y:
366
- return True
367
-
368
- # Find other wires connected to this wire's endpoints and trace them
369
- for wire_point in current_wire.points:
370
- for other_wire in schematic.wires:
371
- if other_wire.uuid == wire_uuid or other_wire.uuid in visited_uuids:
372
- continue
373
-
374
- # Check if other wire shares an endpoint with current wire
375
- for other_point in other_wire.points:
376
- if wire_point.x == other_point.x and wire_point.y == other_point.y:
377
- if _trace_wire_network(schematic, other_wire, target_position, visited_uuids):
378
- return True
379
-
380
- return False
@@ -1,254 +0,0 @@
1
- """
2
- Label element parser for S-expression label definitions.
3
-
4
- Handles parsing of text labels, hierarchical labels, and other text elements.
5
- """
6
-
7
- import logging
8
- from typing import Any, Dict, List, Optional
9
-
10
- import sexpdata
11
-
12
- from .base import BaseElementParser
13
-
14
- logger = logging.getLogger(__name__)
15
-
16
-
17
- class LabelParser(BaseElementParser):
18
- """Parser for label S-expression elements."""
19
-
20
- def __init__(self):
21
- """Initialize label parser."""
22
- super().__init__("label")
23
-
24
- def parse_element(self, element: List[Any]) -> Optional[Dict[str, Any]]:
25
- """
26
- Parse a label S-expression element.
27
-
28
- Expected format:
29
- (label "text" (at x y angle) (effects (font (size s s))))
30
-
31
- Args:
32
- element: Label S-expression element
33
-
34
- Returns:
35
- Parsed label data with text, position, and formatting
36
- """
37
- if len(element) < 2:
38
- return None
39
-
40
- label_data = {
41
- "text": str(element[1]),
42
- "position": {"x": 0, "y": 0, "angle": 0},
43
- "effects": {
44
- "font_size": 1.27,
45
- "font_thickness": 0.15,
46
- "bold": False,
47
- "italic": False,
48
- "hide": False,
49
- "justify": [],
50
- },
51
- "uuid": None,
52
- }
53
-
54
- for elem in element[2:]:
55
- if not isinstance(elem, list):
56
- continue
57
-
58
- elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
59
-
60
- if elem_type == "at":
61
- self._parse_position(elem, label_data)
62
- elif elem_type == "effects":
63
- label_data["effects"] = self._parse_effects(elem)
64
- elif elem_type == "uuid":
65
- label_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
66
-
67
- return label_data
68
-
69
- def _parse_position(self, at_element: List[Any], label_data: Dict[str, Any]) -> None:
70
- """
71
- Parse position from at element.
72
-
73
- Args:
74
- at_element: (at x y [angle])
75
- label_data: Label data dictionary to update
76
- """
77
- try:
78
- if len(at_element) >= 3:
79
- label_data["position"]["x"] = float(at_element[1])
80
- label_data["position"]["y"] = float(at_element[2])
81
- if len(at_element) >= 4:
82
- label_data["position"]["angle"] = float(at_element[3])
83
- except (ValueError, IndexError) as e:
84
- self._logger.warning(f"Invalid position coordinates: {at_element}, error: {e}")
85
-
86
- def _parse_effects(self, effects_element: List[Any]) -> Dict[str, Any]:
87
- """
88
- Parse effects element for text formatting.
89
-
90
- Args:
91
- effects_element: (effects (font ...) (justify ...) ...)
92
-
93
- Returns:
94
- Parsed effects data
95
- """
96
- effects = {
97
- "font_size": 1.27,
98
- "font_thickness": 0.15,
99
- "bold": False,
100
- "italic": False,
101
- "hide": False,
102
- "justify": [],
103
- }
104
-
105
- for elem in effects_element[1:]:
106
- if isinstance(elem, list) and len(elem) > 0:
107
- elem_type = str(elem[0])
108
- if elem_type == "font":
109
- self._parse_font(elem, effects)
110
- elif elem_type == "justify":
111
- effects["justify"] = [str(j) for j in elem[1:]]
112
- elif elem_type == "hide":
113
- effects["hide"] = True
114
-
115
- return effects
116
-
117
- def _parse_font(self, font_element: List[Any], effects: Dict[str, Any]) -> None:
118
- """
119
- Parse font element within effects.
120
-
121
- Args:
122
- font_element: (font (size w h) (thickness t) ...)
123
- effects: Effects dictionary to update
124
- """
125
- for elem in font_element[1:]:
126
- if isinstance(elem, list) and len(elem) > 0:
127
- elem_type = str(elem[0])
128
- if elem_type == "size":
129
- try:
130
- if len(elem) >= 3:
131
- # Usually (size width height), use width for font_size
132
- effects["font_size"] = float(elem[1])
133
- except (ValueError, IndexError):
134
- pass
135
- elif elem_type == "thickness":
136
- try:
137
- effects["font_thickness"] = float(elem[1])
138
- except (ValueError, IndexError):
139
- pass
140
- elif elem_type == "bold":
141
- effects["bold"] = True
142
- elif elem_type == "italic":
143
- effects["italic"] = True
144
-
145
-
146
- class HierarchicalLabelParser(BaseElementParser):
147
- """Parser for hierarchical label S-expression elements."""
148
-
149
- def __init__(self):
150
- """Initialize hierarchical label parser."""
151
- super().__init__("hierarchical_label")
152
-
153
- def parse_element(self, element: List[Any]) -> Optional[Dict[str, Any]]:
154
- """
155
- Parse a hierarchical label S-expression element.
156
-
157
- Expected format:
158
- (hierarchical_label "text" (shape input) (at x y angle) ...)
159
-
160
- Args:
161
- element: Hierarchical label S-expression element
162
-
163
- Returns:
164
- Parsed hierarchical label data
165
- """
166
- if len(element) < 2:
167
- return None
168
-
169
- label_data = {
170
- "text": str(element[1]),
171
- "shape": "input", # Default shape
172
- "position": {"x": 0, "y": 0, "angle": 0},
173
- "effects": {
174
- "font_size": 1.27,
175
- "font_thickness": 0.15,
176
- "bold": False,
177
- "italic": False,
178
- "hide": False,
179
- "justify": [],
180
- },
181
- "uuid": None,
182
- }
183
-
184
- for elem in element[2:]:
185
- if not isinstance(elem, list):
186
- continue
187
-
188
- elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
189
-
190
- if elem_type == "shape":
191
- label_data["shape"] = str(elem[1]) if len(elem) > 1 else "input"
192
- elif elem_type == "at":
193
- self._parse_position(elem, label_data)
194
- elif elem_type == "effects":
195
- label_data["effects"] = self._parse_effects(elem)
196
- elif elem_type == "uuid":
197
- label_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
198
-
199
- return label_data
200
-
201
- def _parse_position(self, at_element: List[Any], label_data: Dict[str, Any]) -> None:
202
- """Parse position from at element."""
203
- try:
204
- if len(at_element) >= 3:
205
- label_data["position"]["x"] = float(at_element[1])
206
- label_data["position"]["y"] = float(at_element[2])
207
- if len(at_element) >= 4:
208
- label_data["position"]["angle"] = float(at_element[3])
209
- except (ValueError, IndexError) as e:
210
- self._logger.warning(f"Invalid position coordinates: {at_element}, error: {e}")
211
-
212
- def _parse_effects(self, effects_element: List[Any]) -> Dict[str, Any]:
213
- """Parse effects element for text formatting."""
214
- effects = {
215
- "font_size": 1.27,
216
- "font_thickness": 0.15,
217
- "bold": False,
218
- "italic": False,
219
- "hide": False,
220
- "justify": [],
221
- }
222
-
223
- for elem in effects_element[1:]:
224
- if isinstance(elem, list) and len(elem) > 0:
225
- elem_type = str(elem[0])
226
- if elem_type == "font":
227
- self._parse_font(elem, effects)
228
- elif elem_type == "justify":
229
- effects["justify"] = [str(j) for j in elem[1:]]
230
- elif elem_type == "hide":
231
- effects["hide"] = True
232
-
233
- return effects
234
-
235
- def _parse_font(self, font_element: List[Any], effects: Dict[str, Any]) -> None:
236
- """Parse font element within effects."""
237
- for elem in font_element[1:]:
238
- if isinstance(elem, list) and len(elem) > 0:
239
- elem_type = str(elem[0])
240
- if elem_type == "size":
241
- try:
242
- if len(elem) >= 3:
243
- effects["font_size"] = float(elem[1])
244
- except (ValueError, IndexError):
245
- pass
246
- elif elem_type == "thickness":
247
- try:
248
- effects["font_thickness"] = float(elem[1])
249
- except (ValueError, IndexError):
250
- pass
251
- elif elem_type == "bold":
252
- effects["bold"] = True
253
- elif elem_type == "italic":
254
- effects["italic"] = True