pdfdancer-client-python 0.2.19__py3-none-any.whl → 0.2.21__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 pdfdancer-client-python might be problematic. Click here for more details.

@@ -1,108 +1,100 @@
1
1
  """
2
2
  ParagraphBuilder for the PDFDancer Python client.
3
- Closely mirrors the Java ParagraphBuilder class with Python conventions.
3
+ Mirrors the behaviour of the Java implementation while keeping Python conventions.
4
4
  """
5
5
 
6
+ from __future__ import annotations
7
+
8
+ from copy import deepcopy
6
9
  from pathlib import Path
7
- from typing import Optional, Union
10
+ from typing import List, Optional, Union
8
11
 
9
12
  from . import StandardFonts
10
13
  from .exceptions import ValidationException
11
- from .models import Paragraph, Font, Color, Position
14
+ from .models import (
15
+ Paragraph,
16
+ Font,
17
+ Color,
18
+ Position,
19
+ TextLine,
20
+ TextObjectRef,
21
+ Point,
22
+ ObjectRef,
23
+ )
24
+
25
+ DEFAULT_LINE_SPACING_FACTOR = 1.2
26
+ DEFAULT_TEXT_COLOR = Color(0, 0, 0)
27
+ _DEFAULT_BASE_FONT_SIZE = 12.0
12
28
 
13
29
 
14
30
  class ParagraphBuilder:
15
31
  """
16
- Builder class for constructing Paragraph objects with fluent interface.
17
- Mirrors the Java ParagraphBuilder class exactly.
32
+ Fluent builder used to assemble `Paragraph` instances.
33
+ Behaviour is aligned with the Java `ParagraphBuilder` so that mixed client/server
34
+ scenarios stay predictable.
18
35
  """
19
36
 
20
37
  def __init__(self, client: 'PDFDancer'):
21
- """
22
- Initialize the paragraph builder with a client reference.
23
-
24
- Args:
25
- client: The ClientV1 instance for font registration
26
- """
27
38
  if client is None:
28
39
  raise ValidationException("Client cannot be null")
29
40
 
30
41
  self._client = client
31
42
  self._paragraph = Paragraph()
32
- self._line_spacing = 1.2
33
- self._text_color = Color(0, 0, 0) # Black by default
43
+ self._line_spacing_factor: Optional[float] = None
44
+ self._text_color: Optional[Color] = None
34
45
  self._text: Optional[str] = None
35
46
  self._ttf_file: Optional[Path] = None
36
47
  self._font: Optional[Font] = None
48
+ self._font_explicitly_changed = False
49
+ self._original_paragraph_position: Optional[Position] = None
50
+ self._target_object_ref: Optional[ObjectRef] = None
51
+ self._original_font: Optional[Font] = None
52
+ self._original_color: Optional[Color] = None
53
+ self._position_changed = False
54
+
55
+ def only_text_changed(self) -> bool:
56
+ """Return True when only the text payload has been modified."""
57
+ return (
58
+ self._text is not None
59
+ and self._text_color is None
60
+ and self._ttf_file is None
61
+ and (self._font is None or not self._font_explicitly_changed)
62
+ and self._line_spacing_factor is None
63
+ )
37
64
 
38
65
  def text(self, text: str, color: Optional[Color] = None) -> 'ParagraphBuilder':
39
- """
40
- Set the text content for the paragraph.
41
- Equivalent to fromString() methods in Java ParagraphBuilder.
42
-
43
- Args:
44
- text: The text content for the paragraph
45
- color: Optional text color (uses default if not provided)
46
-
47
- Returns:
48
- Self for method chaining
49
-
50
- Raises:
51
- ValidationException: If text is None or empty
52
- """
53
66
  if text is None:
54
67
  raise ValidationException("Text cannot be null")
55
- if not text.strip():
56
- raise ValidationException("Text cannot be empty")
57
-
58
68
  self._text = text
59
69
  if color is not None:
60
- self._text_color = color
61
-
70
+ self.color(color)
62
71
  return self
63
72
 
64
- def font(self, font_name: str | StandardFonts, font_size: float) -> 'ParagraphBuilder':
73
+ def font(
74
+ self,
75
+ font: Union[Font, str, StandardFonts],
76
+ font_size: Optional[float] = None
77
+ ) -> 'ParagraphBuilder':
65
78
  """
66
- Set the font for the paragraph using an existing Font object.
67
- Equivalent to withFont(Font) in Java ParagraphBuilder.
68
-
69
- Args:
70
- font_name: The Font to use
71
- font_size: The font size
72
-
73
- Returns:
74
- Self for method chaining
75
-
76
- Raises:
77
- ValidationException: If font is None
79
+ Configure the font either by providing a `Font` instance or name + size.
78
80
  """
79
- # If font_name is an enum member, use its value
80
- if isinstance(font_name, StandardFonts):
81
- font_name = font_name.value
82
-
83
- font = Font(font_name, font_size)
84
- if font is None:
85
- raise ValidationException("Font cannot be null")
86
-
87
- self._font = font
88
- self._ttf_file = None # Clear TTF file when using existing font
81
+ if isinstance(font, Font):
82
+ resolved_font = font
83
+ else:
84
+ if isinstance(font, StandardFonts):
85
+ font = font.value
86
+ if font is None:
87
+ raise ValidationException("Font name cannot be null")
88
+ if font_size is None:
89
+ raise ValidationException("Font size must be provided when setting font by name")
90
+ resolved_font = Font(str(font), font_size)
91
+
92
+ self._font = resolved_font
93
+ self._ttf_file = None
94
+ self._font_explicitly_changed = True
89
95
  return self
90
96
 
91
97
  def font_file(self, ttf_file: Union[Path, str], font_size: float) -> 'ParagraphBuilder':
92
- """
93
- Set the font for the paragraph using a TTF file.
94
- Equivalent to withFont(File, double) in Java ParagraphBuilder.
95
-
96
- Args:
97
- ttf_file: Path to the TTF font file
98
- font_size: Size of the font
99
-
100
- Returns:
101
- Self for method chaining
102
-
103
- Raises:
104
- ValidationException: If TTF file is invalid or font size is not positive
105
- """
106
98
  if ttf_file is None:
107
99
  raise ValidationException("TTF file cannot be null")
108
100
  if font_size <= 0:
@@ -110,170 +102,393 @@ class ParagraphBuilder:
110
102
 
111
103
  ttf_path = Path(ttf_file)
112
104
 
113
- # Strict validation like Java client
114
105
  if not ttf_path.exists():
115
106
  raise ValidationException(f"TTF file does not exist: {ttf_path}")
116
107
  if not ttf_path.is_file():
117
108
  raise ValidationException(f"TTF file is not a file: {ttf_path}")
118
- if not ttf_path.stat().st_size > 0:
109
+ if ttf_path.stat().st_size <= 0:
119
110
  raise ValidationException(f"TTF file is empty: {ttf_path}")
120
111
 
121
- # Check file permissions
122
112
  try:
123
- with open(ttf_path, 'rb') as f:
124
- f.read(1) # Try to read one byte to check readability
125
- except (IOError, OSError):
126
- raise ValidationException(f"TTF file is not readable: {ttf_path}")
113
+ with open(ttf_path, 'rb') as handle:
114
+ handle.read(1)
115
+ except (IOError, OSError) as exc:
116
+ raise ValidationException(f"TTF file is not readable: {ttf_path}") from exc
127
117
 
128
118
  self._ttf_file = ttf_path
129
119
  self._font = self._register_ttf(ttf_path, font_size)
120
+ self._font_explicitly_changed = True
130
121
  return self
131
122
 
132
- def line_spacing(self, spacing: float) -> 'ParagraphBuilder':
133
- """
134
- Set the line spacing for the paragraph.
135
- Equivalent to withLineSpacing() in Java ParagraphBuilder.
123
+ def set_font_explicitly_changed(self, changed: bool) -> None:
124
+ self._font_explicitly_changed = bool(changed)
136
125
 
137
- Args:
138
- spacing: Line spacing value (typically 1.0 to 2.0)
126
+ def set_original_paragraph_position(self, position: Position) -> None:
127
+ self._original_paragraph_position = position
128
+ if position and self._paragraph.get_position() is None:
129
+ self._paragraph.set_position(deepcopy(position))
139
130
 
140
- Returns:
141
- Self for method chaining
131
+ def target(self, object_ref: ObjectRef) -> 'ParagraphBuilder':
132
+ if object_ref is None:
133
+ raise ValidationException("Object reference cannot be null")
134
+ self._target_object_ref = object_ref
135
+ return self
142
136
 
143
- Raises:
144
- ValidationException: If spacing is not positive
145
- """
137
+ def line_spacing(self, spacing: float) -> 'ParagraphBuilder':
146
138
  if spacing <= 0:
147
139
  raise ValidationException(f"Line spacing must be positive, got {spacing}")
148
-
149
- self._line_spacing = spacing
140
+ self._line_spacing_factor = spacing
150
141
  return self
151
142
 
152
143
  def color(self, color: Color) -> 'ParagraphBuilder':
153
- """
154
- Set the text color for the paragraph.
155
- Equivalent to withColor() in Java ParagraphBuilder.
156
-
157
- Args:
158
- color: The Color object for the text
159
-
160
- Returns:
161
- Self for method chaining
162
-
163
- Raises:
164
- ValidationException: If color is None
165
- """
166
144
  if color is None:
167
145
  raise ValidationException("Color cannot be null")
168
-
169
146
  self._text_color = color
170
147
  return self
171
148
 
172
- def at(self, page_index: int, x: float, y: float) -> 'ParagraphBuilder':
149
+ def move_to(self, x: float, y: float) -> 'ParagraphBuilder':
150
+ """
151
+ Move the paragraph anchor to new coordinates on the same page.
173
152
  """
174
- Set the position for the paragraph.
175
- Equivalent to withPosition() in Java ParagraphBuilder.
153
+ position = self._paragraph.get_position()
154
+ if position is None and self._target_object_ref and self._target_object_ref.position:
155
+ position = deepcopy(self._target_object_ref.position)
156
+ self._paragraph.set_position(position)
176
157
 
177
- Args:
178
- position: The Position object for the paragraph
158
+ if position is None:
159
+ raise ValidationException("Cannot move paragraph without an existing position")
179
160
 
180
- Returns:
181
- Self for method chaining
161
+ page_index = position.page_index
162
+ if page_index is None:
163
+ raise ValidationException("Paragraph position must include a page index to move")
182
164
 
183
- Raises:
184
- ValidationException: If position is None
185
- """
186
- position = Position.at_page_coordinates(page_index, x, y)
165
+ self._position_changed = True
166
+ return self.at(page_index, x, y)
167
+
168
+ def at_position(self, position: Position) -> 'ParagraphBuilder':
187
169
  if position is None:
188
170
  raise ValidationException("Position cannot be null")
171
+ # Defensive copy so builder mutations do not alter original references
172
+ self._paragraph.set_position(deepcopy(position))
173
+ self._position_changed = True
174
+ return self
189
175
 
190
- self._paragraph.set_position(position)
176
+ def at(self, page_index: int, x: float, y: float) -> 'ParagraphBuilder':
177
+ return self.at_position(Position.at_page_coordinates(page_index, x, y))
178
+
179
+ def add_text_line(self, text_line: Union[TextLine, TextObjectRef, str]) -> 'ParagraphBuilder':
180
+ self._paragraph.add_line(self._coerce_text_line(text_line))
191
181
  return self
192
182
 
193
- def _build(self) -> Paragraph:
194
- """
195
- Build and return the final Paragraph object.
196
- Equivalent to build() in Java ParagraphBuilder.
183
+ def get_text(self) -> Optional[str]:
184
+ return self._text
197
185
 
198
- This method validates all required fields and constructs the final paragraph
199
- with text processing similar to ParagraphUtil.finalizeText() in Java.
186
+ def add(self) -> bool:
187
+ # noinspection PyProtectedMember
188
+ return self._client._add_paragraph(self._finalize_paragraph())
200
189
 
201
- Returns:
202
- The constructed Paragraph object
190
+ def modify(self, object_ref: Optional[ObjectRef] = None):
191
+ target_ref = object_ref or self._target_object_ref
192
+ if target_ref is None:
193
+ raise ValidationException("Object reference must be provided to modify a paragraph")
203
194
 
204
- Raises:
205
- ValidationException: If required fields are missing or invalid
206
- """
207
- # Validate required fields
208
- if self._text is None:
209
- raise ValidationException("Text must be set before building paragraph")
210
- if self._font is None:
211
- raise ValidationException("Font must be set before building paragraph")
195
+ if self.only_text_changed():
196
+ # Backend accepts plain text updates for simple edits
197
+ return self._client._modify_paragraph(target_ref, self._text or "")
198
+
199
+ paragraph = self._finalize_paragraph()
200
+ return self._client._modify_paragraph(target_ref, paragraph)
201
+
202
+ # ------------------------------------------------------------------ #
203
+ # Internal helpers
204
+ # ------------------------------------------------------------------ #
205
+
206
+ def _finalize_paragraph(self) -> Paragraph:
212
207
  if self._paragraph.get_position() is None:
213
208
  raise ValidationException("Position must be set before building paragraph")
214
209
 
215
- # Set paragraph properties
216
- self._paragraph.font = self._font
217
- self._paragraph.color = self._text_color
218
- self._paragraph.line_spacing = self._line_spacing
219
-
220
- # Process text into lines (simplified version of ParagraphUtil.finalizeText)
221
- # In the full implementation, this would handle text wrapping, line breaks, etc.
222
- self._paragraph.text_lines = self._process_text_lines(self._text)
210
+ if self._target_object_ref is None and self._font is None and self._paragraph.font is None:
211
+ raise ValidationException("Font must be set before building paragraph")
223
212
 
213
+ if self._text is not None:
214
+ self._finalize_lines_from_text()
215
+ elif not self._paragraph.text_lines:
216
+ raise ValidationException("Either text must be provided or existing lines supplied")
217
+ else:
218
+ self._finalize_existing_lines()
219
+
220
+ self._reposition_lines()
221
+
222
+ should_skip_lines = (
223
+ self._position_changed
224
+ and self._text is None
225
+ and self._text_color is None
226
+ and (self._font is None or not self._font_explicitly_changed)
227
+ and self._line_spacing_factor is None
228
+ )
229
+ if should_skip_lines:
230
+ self._paragraph.text_lines = None
231
+ self._paragraph.set_line_spacings(None)
232
+
233
+ final_font = self._font if self._font is not None else self._original_font
234
+ if final_font is None:
235
+ final_font = Font(StandardFonts.HELVETICA.value, _DEFAULT_BASE_FONT_SIZE)
236
+ self._paragraph.font = final_font
237
+
238
+ if self._text_color is not None:
239
+ final_color = self._text_color
240
+ elif self._text is not None:
241
+ final_color = self._original_color or DEFAULT_TEXT_COLOR
242
+ else:
243
+ final_color = self._original_color
244
+ self._paragraph.color = final_color
224
245
  return self._paragraph
225
246
 
226
- def _register_ttf(self, ttf_file: Path, font_size: float) -> Font:
227
- """
228
- Register a TTF font with the client and return a Font object.
229
- Equivalent to registerTTF() private method in Java ParagraphBuilder.
230
-
231
- Args:
232
- ttf_file: Path to the TTF font file
233
- font_size: Size of the font
247
+ def _finalize_lines_from_text(self) -> None:
248
+ base_font = self._font or self._original_font
249
+ base_color = self._text_color or self._original_color or DEFAULT_TEXT_COLOR
250
+ color = base_color
251
+
252
+ if self._line_spacing_factor is not None:
253
+ spacing = self._line_spacing_factor
254
+ else:
255
+ existing_spacings = self._paragraph.get_line_spacings()
256
+ if existing_spacings:
257
+ spacing = existing_spacings[0]
258
+ elif self._paragraph.line_spacing:
259
+ spacing = self._paragraph.line_spacing
260
+ else:
261
+ spacing = DEFAULT_LINE_SPACING_FACTOR
262
+
263
+ self._paragraph.clear_lines()
264
+ lines: List[TextLine] = []
265
+ for index, line_text in enumerate(self._split_text(self._text or "")):
266
+ line_position = self._calculate_line_position(index, spacing)
267
+ lines.append(
268
+ TextLine(
269
+ position=line_position,
270
+ font=base_font,
271
+ color=color,
272
+ line_spacing=spacing,
273
+ text=line_text,
274
+ )
275
+ )
276
+ self._paragraph.set_lines(lines)
277
+ self._paragraph.set_line_spacings([spacing] * (len(lines) - 1) if len(lines) > 1 else None)
278
+ self._paragraph.line_spacing = spacing
279
+
280
+ def _finalize_existing_lines(self) -> None:
281
+ lines = self._paragraph.text_lines or []
282
+ spacing_override = self._line_spacing_factor
283
+ spacing_for_calc = spacing_override
284
+
285
+ if spacing_for_calc is None:
286
+ existing_spacings = self._paragraph.get_line_spacings()
287
+ if existing_spacings:
288
+ spacing_for_calc = existing_spacings[0]
289
+ if spacing_for_calc is None:
290
+ spacing_for_calc = self._paragraph.line_spacing or DEFAULT_LINE_SPACING_FACTOR
291
+
292
+ updated_lines: List[TextLine] = []
293
+ for index, line in enumerate(lines):
294
+ if isinstance(line, TextLine):
295
+ if spacing_override is not None:
296
+ line.line_spacing = spacing_override
297
+ if self._text_color is not None:
298
+ line.color = self._text_color
299
+ if self._font is not None and self._font_explicitly_changed:
300
+ line.font = self._font
301
+ updated_lines.append(line)
302
+ else:
303
+ line_position = self._calculate_line_position(index, spacing_for_calc)
304
+ updated_lines.append(
305
+ TextLine(
306
+ position=line_position,
307
+ font=self._font if self._font is not None else self._original_font,
308
+ color=self._text_color or self._original_color or DEFAULT_TEXT_COLOR,
309
+ line_spacing=spacing_override if spacing_override is not None else spacing_for_calc,
310
+ text=str(line),
311
+ )
312
+ )
313
+
314
+ self._paragraph.set_lines(updated_lines)
315
+
316
+ if spacing_override is not None:
317
+ self._paragraph.set_line_spacings(
318
+ [spacing_override] * (len(updated_lines) - 1) if len(updated_lines) > 1 else None
319
+ )
320
+ self._paragraph.line_spacing = spacing_override
321
+
322
+
323
+ def _reposition_lines(self) -> None:
324
+ if self._text is not None:
325
+ # Newly generated text lines already align with the updated paragraph position.
326
+ return
327
+ paragraph_pos = self._paragraph.get_position()
328
+ lines = self._paragraph.text_lines or []
329
+ if not paragraph_pos or not lines:
330
+ return
331
+
332
+ base_position = self._original_paragraph_position
333
+ if base_position is None:
334
+ for line in lines:
335
+ if isinstance(line, TextLine) and line.position is not None:
336
+ base_position = line.position
337
+ break
338
+
339
+ if base_position is None:
340
+ return
341
+
342
+ target_x = paragraph_pos.x()
343
+ target_y = paragraph_pos.y()
344
+ base_x = base_position.x()
345
+ base_y = base_position.y()
346
+ if None in (target_x, target_y, base_x, base_y):
347
+ return
348
+
349
+ dx = target_x - base_x
350
+ dy = target_y - base_y
351
+ if dx == 0 and dy == 0:
352
+ return
353
+
354
+ for line in lines:
355
+ if isinstance(line, TextLine) and line.position is not None:
356
+ current_x = line.position.x()
357
+ current_y = line.position.y()
358
+ if current_x is None or current_y is None:
359
+ continue
360
+ line.position.at_coordinates(Point(current_x + dx, current_y + dy))
361
+
362
+ def _coerce_text_line(self, source: Union[TextLine, TextObjectRef, str]) -> TextLine:
363
+ if isinstance(source, TextLine):
364
+ return source
365
+
366
+ if isinstance(source, TextObjectRef):
367
+ font = None
368
+ if source.font_name and source.font_size:
369
+ font = Font(source.font_name, source.font_size)
370
+ elif getattr(source, "children", None):
371
+ for child in source.children:
372
+ if child.font_name and child.font_size:
373
+ font = Font(child.font_name, child.font_size)
374
+ break
375
+ if font is None:
376
+ font = self._original_font
377
+
378
+ spacing = self._line_spacing_factor
379
+ if spacing is None and source.line_spacings:
380
+ spacing = source.line_spacings[0]
381
+ if spacing is None:
382
+ spacing = self._paragraph.line_spacing or DEFAULT_LINE_SPACING_FACTOR
383
+
384
+ color = source.color or self._original_color
385
+
386
+ line = TextLine(
387
+ position=deepcopy(source.position) if source.position else None,
388
+ font=font,
389
+ color=color,
390
+ line_spacing=spacing,
391
+ text=source.text or "",
392
+ )
393
+ if self._original_font is None and font is not None:
394
+ self._original_font = font
395
+ if self._original_color is None and color is not None:
396
+ self._original_color = color
397
+ return line
398
+
399
+ if isinstance(source, str):
400
+ current_index = len(self._paragraph.get_lines())
401
+ spacing = self._line_spacing_factor if self._line_spacing_factor is not None else (
402
+ self._paragraph.line_spacing or DEFAULT_LINE_SPACING_FACTOR
403
+ )
404
+ line_position = self._calculate_line_position(current_index, spacing)
405
+ return TextLine(
406
+ position=line_position,
407
+ font=self._font or self._original_font,
408
+ color=self._text_color or self._original_color or DEFAULT_TEXT_COLOR,
409
+ line_spacing=spacing,
410
+ text=source,
411
+ )
412
+
413
+ raise ValidationException(f"Unsupported text line type: {type(source)}")
414
+
415
+ def _split_text(self, text: str) -> List[str]:
416
+ processed = text.replace('\r\n', '\n').replace('\r', '\n').replace('\\n', '\n')
417
+ parts = processed.split('\n')
418
+ while parts and parts[-1] == '':
419
+ parts.pop()
420
+ if not parts:
421
+ parts = ['']
422
+ return parts
423
+
424
+ def _calculate_line_position(self, line_index: int, spacing_factor: float) -> Optional[Position]:
425
+ paragraph_position = self._paragraph.get_position()
426
+ if paragraph_position is None:
427
+ return None
428
+
429
+ page_index = paragraph_position.page_index
430
+ base_x = paragraph_position.x()
431
+ base_y = paragraph_position.y()
432
+ if page_index is None or base_x is None or base_y is None:
433
+ return None
434
+
435
+ offset = line_index * self._calculate_baseline_distance(spacing_factor)
436
+ return Position.at_page_coordinates(page_index, base_x, base_y + offset)
437
+
438
+ def _calculate_baseline_distance(self, spacing_factor: float) -> float:
439
+ factor = spacing_factor if spacing_factor > 0 else DEFAULT_LINE_SPACING_FACTOR
440
+ return self._baseline_font_size() * factor
441
+
442
+ def _baseline_font_size(self) -> float:
443
+ if self._font and self._font.size:
444
+ return self._font.size
445
+ if self._original_font and self._original_font.size:
446
+ return self._original_font.size
447
+ return _DEFAULT_BASE_FONT_SIZE
234
448
 
235
- Returns:
236
- Font object with the registered font name and size
237
- """
449
+ def _register_ttf(self, ttf_file: Path, font_size: float) -> Font:
238
450
  try:
239
451
  font_name = self._client.register_font(ttf_file)
240
452
  return Font(font_name, font_size)
241
- except Exception as e:
242
- raise ValidationException(f"Failed to register font file {ttf_file}: {e}")
453
+ except Exception as exc:
454
+ raise ValidationException(f"Failed to register font file {ttf_file}: {exc}") from exc
243
455
 
244
- def _process_text_lines(self, text: str) -> list[str]:
456
+ def _build(self) -> Paragraph:
457
+ """
458
+ Backwards-compatible alias for callers that invoked the previous `_build`.
245
459
  """
246
- Process text into lines for the paragraph.
247
- This is a simplified version - the full implementation would handle
248
- word wrapping, line breaks, and other text formatting based on the font
249
- and paragraph width.
460
+ return self._finalize_paragraph()
250
461
 
251
- Args:
252
- text: The input text to process
462
+ @classmethod
463
+ def from_object_ref(cls, client: 'PDFDancer', object_ref: TextObjectRef) -> 'ParagraphBuilder':
464
+ if object_ref is None:
465
+ raise ValidationException("Object reference cannot be null")
253
466
 
254
- Returns:
255
- List of text lines for the paragraph
256
- """
257
- # Handle escaped newlines (\\n) as actual newlines
258
- processed_text = text.replace('\\n', '\n')
467
+ builder = cls(client)
468
+ builder.target(object_ref)
259
469
 
260
- # Simple implementation - split on newlines
261
- # In the full version, this would implement proper text layout
262
- lines = processed_text.split('\n')
470
+ if object_ref.position:
471
+ builder.at_position(object_ref.position)
472
+ builder.set_original_paragraph_position(object_ref.position)
263
473
 
264
- # Remove empty lines at the end but preserve intentional line breaks
265
- while lines and not lines[-1].strip():
266
- lines.pop()
474
+ if object_ref.line_spacings:
475
+ builder._paragraph.set_line_spacings(object_ref.line_spacings)
476
+ builder._paragraph.line_spacing = object_ref.line_spacings[0] if object_ref.line_spacings else builder._paragraph.line_spacing
267
477
 
268
- # Ensure at least one line
269
- if not lines:
270
- lines = ['']
478
+ if object_ref.font_name and object_ref.font_size:
479
+ builder._original_font = Font(object_ref.font_name, object_ref.font_size)
271
480
 
272
- return lines
481
+ if object_ref.color:
482
+ builder._original_color = object_ref.color
273
483
 
274
- def add(self):
275
- # noinspection PyProtectedMember
276
- return self._client._add_paragraph(self._build())
484
+ if object_ref.children:
485
+ for child in object_ref.children:
486
+ builder.add_text_line(child)
487
+ elif object_ref.text:
488
+ for segment in object_ref.text.split('\n'):
489
+ builder.add_text_line(segment)
490
+
491
+ return builder
277
492
 
278
493
 
279
494
  class ParagraphPageBuilder(ParagraphBuilder):