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.

pdfdancer/path_builder.py CHANGED
@@ -154,6 +154,38 @@ class PathBuilder:
154
154
  self._segments.append(bezier)
155
155
  return self
156
156
 
157
+ def add_rectangle(self, x: float, y: float, width: float, height: float) -> 'PathBuilder':
158
+ """
159
+ Convenient method to add a rectangle as four line segments to the path.
160
+
161
+ Args:
162
+ x: X coordinate of bottom-left corner
163
+ y: Y coordinate of bottom-left corner
164
+ width: Rectangle width
165
+ height: Rectangle height
166
+
167
+ Returns:
168
+ Self for method chaining
169
+ """
170
+ if width <= 0:
171
+ raise ValidationException("Rectangle width must be positive")
172
+ if height <= 0:
173
+ raise ValidationException("Rectangle height must be positive")
174
+
175
+ # Create rectangle as 4 line segments (clockwise from bottom-left)
176
+ bottom_left = Point(x, y)
177
+ bottom_right = Point(x + width, y)
178
+ top_right = Point(x + width, y + height)
179
+ top_left = Point(x, y + height)
180
+
181
+ # Add four lines forming the rectangle
182
+ self.add_line(bottom_left, bottom_right)
183
+ self.add_line(bottom_right, top_right)
184
+ self.add_line(top_right, top_left)
185
+ self.add_line(top_left, bottom_left)
186
+
187
+ return self
188
+
157
189
  def even_odd_fill(self, enabled: bool = True) -> 'PathBuilder':
158
190
  """
159
191
  Set the fill rule to even-odd (vs nonzero winding).
@@ -555,3 +587,223 @@ class BezierBuilder:
555
587
  # Add it using the client's internal method
556
588
  # noinspection PyProtectedMember
557
589
  return self._client._add_path(path)
590
+
591
+
592
+ class RectangleBuilder:
593
+ """
594
+ Builder class for constructing Rectangle objects with fluent interface.
595
+ Provides a convenient way to create a rectangle path with a single builder.
596
+ """
597
+
598
+ def __init__(self, client: 'PDFDancer', page_index: int):
599
+ """
600
+ Initialize the rectangle builder.
601
+
602
+ Args:
603
+ client: The PDFDancer instance for adding the rectangle
604
+ page_index: The page number (0-indexed)
605
+ """
606
+ if client is None:
607
+ raise ValidationException("Client cannot be null")
608
+
609
+ self._client = client
610
+ self._page_index = page_index
611
+ self._x: Optional[float] = None
612
+ self._y: Optional[float] = None
613
+ self._width: Optional[float] = None
614
+ self._height: Optional[float] = None
615
+ self._stroke_color: Optional[Color] = Color(0, 0, 0) # Black default
616
+ self._fill_color: Optional[Color] = None
617
+ self._stroke_width: float = 1.0
618
+ self._dash_array: Optional[List[float]] = None
619
+ self._dash_phase: Optional[float] = None
620
+ self._even_odd_fill: bool = False
621
+
622
+ def at_coordinates(self, x: float, y: float) -> 'RectangleBuilder':
623
+ """
624
+ Set the bottom-left corner position of the rectangle.
625
+
626
+ Args:
627
+ x: X coordinate on the page
628
+ y: Y coordinate on the page
629
+
630
+ Returns:
631
+ Self for method chaining
632
+ """
633
+ self._x = x
634
+ self._y = y
635
+ return self
636
+
637
+ def with_size(self, width: float, height: float) -> 'RectangleBuilder':
638
+ """
639
+ Set the dimensions of the rectangle.
640
+
641
+ Args:
642
+ width: Rectangle width
643
+ height: Rectangle height
644
+
645
+ Returns:
646
+ Self for method chaining
647
+ """
648
+ self._width = width
649
+ self._height = height
650
+ return self
651
+
652
+ def stroke_color(self, color: Color) -> 'RectangleBuilder':
653
+ """
654
+ Set the stroke color.
655
+
656
+ Args:
657
+ color: The stroke color
658
+
659
+ Returns:
660
+ Self for method chaining
661
+ """
662
+ self._stroke_color = color
663
+ return self
664
+
665
+ def fill_color(self, color: Color) -> 'RectangleBuilder':
666
+ """
667
+ Set the fill color.
668
+
669
+ Args:
670
+ color: The fill color
671
+
672
+ Returns:
673
+ Self for method chaining
674
+ """
675
+ self._fill_color = color
676
+ return self
677
+
678
+ def stroke_width(self, width: float) -> 'RectangleBuilder':
679
+ """
680
+ Set the stroke width.
681
+
682
+ Args:
683
+ width: The stroke width in points
684
+
685
+ Returns:
686
+ Self for method chaining
687
+ """
688
+ if width <= 0:
689
+ raise ValidationException("Stroke width must be positive")
690
+ self._stroke_width = width
691
+ return self
692
+
693
+ def dash_pattern(self, dash_array: List[float], dash_phase: float = 0.0) -> 'RectangleBuilder':
694
+ """
695
+ Set a dash pattern.
696
+
697
+ Args:
698
+ dash_array: List of on/off lengths (e.g., [10, 5] = 10pt on, 5pt off)
699
+ dash_phase: Offset into the pattern
700
+
701
+ Returns:
702
+ Self for method chaining
703
+ """
704
+ self._dash_array = dash_array
705
+ self._dash_phase = dash_phase
706
+ return self
707
+
708
+ def solid(self) -> 'RectangleBuilder':
709
+ """
710
+ Set rectangle to solid (no dash pattern).
711
+
712
+ Returns:
713
+ Self for method chaining
714
+ """
715
+ self._dash_array = None
716
+ self._dash_phase = None
717
+ return self
718
+
719
+ def even_odd_fill(self, enabled: bool = True) -> 'RectangleBuilder':
720
+ """
721
+ Set the fill rule to even-odd (vs nonzero winding).
722
+
723
+ Args:
724
+ enabled: True for even-odd, False for nonzero winding
725
+
726
+ Returns:
727
+ Self for method chaining
728
+ """
729
+ self._even_odd_fill = enabled
730
+ return self
731
+
732
+ def add(self) -> bool:
733
+ """
734
+ Build the rectangle and add it to the PDF document.
735
+
736
+ Returns:
737
+ True if successful
738
+
739
+ Raises:
740
+ ValidationException: If required properties are missing
741
+ """
742
+ if self._x is None or self._y is None:
743
+ raise ValidationException("Rectangle position must be set using at_coordinates()")
744
+ if self._width is None or self._height is None:
745
+ raise ValidationException("Rectangle dimensions must be set using with_size()")
746
+ if self._width <= 0:
747
+ raise ValidationException("Rectangle width must be positive")
748
+ if self._height <= 0:
749
+ raise ValidationException("Rectangle height must be positive")
750
+
751
+ # Create rectangle as 4 line segments
752
+ bottom_left = Point(self._x, self._y)
753
+ bottom_right = Point(self._x + self._width, self._y)
754
+ top_right = Point(self._x + self._width, self._y + self._height)
755
+ top_left = Point(self._x, self._y + self._height)
756
+
757
+ # Create four lines forming the rectangle
758
+ lines = [
759
+ Line(
760
+ p0=bottom_left,
761
+ p1=bottom_right,
762
+ stroke_color=self._stroke_color,
763
+ fill_color=self._fill_color,
764
+ stroke_width=self._stroke_width,
765
+ dash_array=self._dash_array,
766
+ dash_phase=self._dash_phase
767
+ ),
768
+ Line(
769
+ p0=bottom_right,
770
+ p1=top_right,
771
+ stroke_color=self._stroke_color,
772
+ fill_color=self._fill_color,
773
+ stroke_width=self._stroke_width,
774
+ dash_array=self._dash_array,
775
+ dash_phase=self._dash_phase
776
+ ),
777
+ Line(
778
+ p0=top_right,
779
+ p1=top_left,
780
+ stroke_color=self._stroke_color,
781
+ fill_color=self._fill_color,
782
+ stroke_width=self._stroke_width,
783
+ dash_array=self._dash_array,
784
+ dash_phase=self._dash_phase
785
+ ),
786
+ Line(
787
+ p0=top_left,
788
+ p1=bottom_left,
789
+ stroke_color=self._stroke_color,
790
+ fill_color=self._fill_color,
791
+ stroke_width=self._stroke_width,
792
+ dash_array=self._dash_array,
793
+ dash_phase=self._dash_phase
794
+ )
795
+ ]
796
+
797
+ # Create position with only page index set
798
+ position = Position.at_page_coordinates(self._page_index, 0, 0)
799
+
800
+ # Wrap in Path with four line segments
801
+ path = Path(
802
+ position=position,
803
+ path_segments=lines,
804
+ even_odd_fill=self._even_odd_fill
805
+ )
806
+
807
+ # Add it using the client's internal method
808
+ # noinspection PyProtectedMember
809
+ return self._client._add_path(path)
pdfdancer/pdfdancer_v1.py CHANGED
@@ -7,6 +7,7 @@ Provides session-based PDF manipulation operations with strict validation.
7
7
 
8
8
  import gzip
9
9
  import json
10
+ import logging
10
11
  import os
11
12
  import time
12
13
  from datetime import datetime, timezone
@@ -116,7 +117,7 @@ def _log_generated_at_header(response: httpx.Response, method: str, path: str) -
116
117
  print(f"{time.time()}|{method} {path} - Header parse error: {e}")
117
118
 
118
119
 
119
- from . import ParagraphBuilder
120
+ from . import ParagraphBuilder, BezierBuilder, PathBuilder, LineBuilder
120
121
  from .exceptions import (
121
122
  PdfDancerException,
122
123
  FontNotFoundException,
@@ -124,15 +125,17 @@ from .exceptions import (
124
125
  SessionException,
125
126
  ValidationException
126
127
  )
127
- from .image_builder import ImageBuilder
128
+ from .image_builder import ImageBuilder, ImageOnPageBuilder
128
129
  from .models import (
129
130
  ObjectRef, Position, ObjectType, Font, Image, Paragraph, FormFieldRef, TextObjectRef, PageRef,
130
- FindRequest, DeleteRequest, MoveRequest, PageMoveRequest, AddRequest, ModifyRequest, ModifyTextRequest,
131
+ FindRequest, DeleteRequest, MoveRequest, PageMoveRequest, AddPageRequest, AddRequest, ModifyRequest,
132
+ ModifyTextRequest,
131
133
  ChangeFormFieldRequest, CommandResult,
132
134
  ShapeType, PositionMode, PageSize, Orientation,
133
135
  PageSnapshot, DocumentSnapshot, FontRecommendation, FontType
134
136
  )
135
137
  from .paragraph_builder import ParagraphPageBuilder
138
+ from .page_builder import PageBuilder
136
139
  from .types import PathObject, ParagraphObject, TextLineObject, ImageObject, FormObject, FormFieldObject
137
140
 
138
141
 
@@ -270,21 +273,25 @@ class PageClient:
270
273
  def _ref(self):
271
274
  return ObjectRef(internal_id=self.internal_id, position=self.position, type=self.object_type)
272
275
 
273
- def new_paragraph(self):
276
+ def new_paragraph(self) -> ParagraphBuilder:
274
277
  return ParagraphPageBuilder(self.root, self.page_index)
275
278
 
276
- def new_path(self):
277
- from .path_builder import PathBuilder
279
+ def new_path(self) -> PathBuilder:
278
280
  return PathBuilder(self.root, self.page_index)
279
281
 
280
- def new_line(self):
281
- from .path_builder import LineBuilder
282
+ def new_image(self) -> ImageOnPageBuilder:
283
+ return ImageOnPageBuilder(self.root, self.page_index)
284
+
285
+ def new_line(self) -> LineBuilder:
282
286
  return LineBuilder(self.root, self.page_index)
283
287
 
284
- def new_bezier(self):
285
- from .path_builder import BezierBuilder
288
+ def new_bezier(self) -> BezierBuilder:
286
289
  return BezierBuilder(self.root, self.page_index)
287
290
 
291
+ def new_rectangle(self) -> 'RectangleBuilder':
292
+ from .path_builder import RectangleBuilder
293
+ return RectangleBuilder(self.root, self.page_index)
294
+
288
295
  def select_paths(self):
289
296
  # noinspection PyProtectedMember
290
297
  return self.root._to_path_objects(self.root._find_paths(Position.at_page(self.page_index)))
@@ -906,6 +913,20 @@ class PDFDancer:
906
913
  """
907
914
  return self._to_paragraph_objects(self._find_paragraphs(None))
908
915
 
916
+ def select_paragraphs_matching(self, pattern: str) -> List[ParagraphObject]:
917
+ """
918
+ Searches for paragraph objects matching a regex pattern.
919
+
920
+ Args:
921
+ pattern: Regex pattern to match against paragraph text
922
+
923
+ Returns:
924
+ List of ParagraphObject instances matching the pattern
925
+ """
926
+ position = Position()
927
+ position.text_pattern = pattern
928
+ return self._to_paragraph_objects(self._find_paragraphs(position))
929
+
909
930
  def _find_paragraphs(self, position: Optional[Position] = None, tolerance: float = DEFAULT_TOLERANCE) -> List[
910
931
  TextObjectRef]:
911
932
  """
@@ -1270,7 +1291,6 @@ class PDFDancer:
1270
1291
  """
1271
1292
  Internal method to add a path to the document after validation.
1272
1293
  """
1273
- from .models import Path as PathModel
1274
1294
 
1275
1295
  if path is None:
1276
1296
  raise ValidationException("Path cannot be null")
@@ -1299,11 +1319,16 @@ class PDFDancer:
1299
1319
 
1300
1320
  return result
1301
1321
 
1302
- def new_paragraph(self) -> ParagraphBuilder:
1303
- return ParagraphBuilder(self)
1322
+ def _add_page(self, request: Optional[AddPageRequest]) -> PageRef:
1323
+ """
1324
+ Internal helper to add a page with optional parameters.
1325
+ """
1326
+ request_data = None
1327
+ if request is not None:
1328
+ payload = request.to_dict()
1329
+ request_data = payload or None
1304
1330
 
1305
- def new_page(self):
1306
- response = self._make_request('POST', '/pdf/page/add', data=None)
1331
+ response = self._make_request('POST', '/pdf/page/add', data=request_data)
1307
1332
  result = self._parse_page_ref(response.json())
1308
1333
 
1309
1334
  # Invalidate snapshot caches after adding page
@@ -1311,6 +1336,17 @@ class PDFDancer:
1311
1336
 
1312
1337
  return result
1313
1338
 
1339
+ def new_paragraph(self) -> ParagraphBuilder:
1340
+ return ParagraphBuilder(self)
1341
+
1342
+ def new_page(self, orientation=Orientation.PORTRAIT, size=PageSize.A4) -> PageBuilder:
1343
+ builder = PageBuilder(self)
1344
+ if orientation is not None:
1345
+ builder.orientation(orientation)
1346
+ if size is not None:
1347
+ builder.page_size(size)
1348
+ return builder
1349
+
1314
1350
  def new_image(self) -> ImageBuilder:
1315
1351
  return ImageBuilder(self)
1316
1352
 
@@ -1816,11 +1852,14 @@ class PDFDancer:
1816
1852
  status=status
1817
1853
  )
1818
1854
 
1819
- if isinstance(obj_data.get('children'), list) and len(obj_data['children']) > 0:
1820
- text_object.children = [
1821
- self._parse_text_object_ref(child_data, f"{internal_id or 'child'}-{index}")
1822
- for index, child_data in enumerate(obj_data['children'])
1823
- ]
1855
+ try:
1856
+ if isinstance(obj_data.get('children'), list) and len(obj_data['children']) > 0:
1857
+ text_object.children = [
1858
+ self._parse_text_object_ref(child_data, f"{internal_id or 'child'}-{index}")
1859
+ for index, child_data in enumerate(obj_data['children'])
1860
+ ]
1861
+ except ValueError as e:
1862
+ logging.exception(f"Failed to parse children of {internal_id}", e)
1824
1863
 
1825
1864
  return text_object
1826
1865
 
@@ -1963,7 +2002,7 @@ class PDFDancer:
1963
2002
  even_odd_fill=even_odd_fill
1964
2003
  )
1965
2004
 
1966
- def _parse_font_recommendation(self, data: dict) -> FontRecommendation:
2005
+ def _parse_document_font_info(self, data: dict) -> FontRecommendation:
1967
2006
  """Parse JSON data into FontRecommendation instance."""
1968
2007
  font_type_str = data.get('fontType', 'SYSTEM')
1969
2008
  font_type = FontType(font_type_str)
@@ -2020,7 +2059,7 @@ class PDFDancer:
2020
2059
  def _parse_document_snapshot(self, data: dict) -> DocumentSnapshot:
2021
2060
  """Parse JSON data into DocumentSnapshot instance."""
2022
2061
  page_count = data.get('pageCount', 0)
2023
- fonts = [self._parse_font_recommendation(font_data) for font_data in data.get('fonts', [])]
2062
+ fonts = [self._parse_document_font_info(font_data) for font_data in data.get('fonts', [])]
2024
2063
  pages = [self._parse_page_snapshot(page_data) for page_data in data.get('pages', [])]
2025
2064
 
2026
2065
  return DocumentSnapshot(
pdfdancer/types.py CHANGED
@@ -1,12 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
- import statistics
4
3
  import sys
5
4
  from dataclasses import dataclass
6
- from typing import Optional, List
5
+ from typing import Optional
7
6
 
8
- from . import ObjectType, Position, ObjectRef, Point, Paragraph, Font, Color, FormFieldRef, TextObjectRef
9
- from .models import CommandResult
7
+ from . import ObjectType, Position, ObjectRef, Point, FormFieldRef, TextObjectRef
10
8
 
11
9
 
12
10
  @dataclass
@@ -95,41 +93,6 @@ class FormObject(PDFObjectBase):
95
93
  self.position == other.position)
96
94
 
97
95
 
98
- def _process_text_lines(text: str) -> List[str]:
99
- """
100
- Process text into lines for the paragraph.
101
- This is a simplified version - the full implementation would handle
102
- word wrapping, line breaks, and other text formatting based on the font
103
- and paragraph width. TODO
104
-
105
- Args:
106
- text: The input text to process
107
-
108
- Returns:
109
- List of text lines for the paragraph
110
- """
111
- # Handle escaped newlines (\\n) as actual newlines
112
- processed_text = text.replace('\\n', '\n')
113
-
114
- # Simple implementation - split on newlines
115
- # In the full version, this would implement proper text layout
116
- lines = processed_text.split('\n')
117
-
118
- # Remove empty lines at the end but preserve intentional line breaks
119
- while lines and not lines[-1].strip():
120
- lines.pop()
121
-
122
- # Ensure at least one line
123
- if not lines:
124
- lines = ['']
125
-
126
- return lines
127
-
128
-
129
- DEFAULT_LINE_SPACING = 1.2
130
- DEFAULT_COLOR = Color(0, 0, 0)
131
-
132
-
133
96
  class BaseTextEdit:
134
97
  """Common base for text-like editable objects (Paragraph, TextLine, etc.)"""
135
98
 
@@ -178,68 +141,6 @@ class BaseTextEdit:
178
141
  raise NotImplementedError("Subclasses must implement apply()")
179
142
 
180
143
 
181
- class ParagraphEdit(BaseTextEdit):
182
- def apply(self) -> CommandResult:
183
- if (
184
- self._position is None
185
- and self._line_spacing is None
186
- and self._font_size is None
187
- and self._font_name is None
188
- and self._color is None
189
- ):
190
- # noinspection PyProtectedMember
191
- result = self._target_obj._client._modify_paragraph(self._object_ref, self._new_text)
192
- if result.warning:
193
- print(f"WARNING: {result.warning}", file=sys.stderr)
194
- return result
195
- else:
196
- new_paragraph = Paragraph(
197
- position=self._position if self._position is not None else self._object_ref.position,
198
- line_spacing=self._get_line_spacing(),
199
- font=self._get_font(),
200
- text_lines=self._get_text_lines(),
201
- color=self._get_color(),
202
- )
203
- # noinspection PyProtectedMember
204
- result = self._target_obj._client._modify_paragraph(self._object_ref, new_paragraph)
205
- if result.warning:
206
- print(f"WARNING: {result.warning}", file=sys.stderr)
207
- return result
208
-
209
- def _get_line_spacing(self) -> float:
210
- if self._line_spacing is not None:
211
- return self._line_spacing
212
- elif self._object_ref.line_spacings is not None:
213
- return statistics.mean(self._object_ref.line_spacings)
214
- else:
215
- return DEFAULT_LINE_SPACING
216
-
217
- def _get_font(self):
218
- if self._font_name is not None and self._font_size is not None:
219
- return Font(name=self._font_name, size=self._font_size)
220
- elif self._object_ref.font_name is not None and self._object_ref.font_size is not None:
221
- return Font(name=self._object_ref.font_name, size=self._object_ref.font_size)
222
- else:
223
- raise Exception("Font is none")
224
-
225
- def _get_text_lines(self):
226
- if self._new_text is not None:
227
- return _process_text_lines(self._new_text)
228
- elif self._object_ref.text is not None:
229
- # TODO this actually messes up existing text line internals
230
- return _process_text_lines(self._object_ref.text)
231
- else:
232
- raise Exception("Paragraph has no text")
233
-
234
- def _get_color(self):
235
- if self._color is not None:
236
- return self._color
237
- elif self._object_ref.color is not None:
238
- return self._object_ref.color
239
- else:
240
- return DEFAULT_COLOR
241
-
242
-
243
144
  class TextLineEdit(BaseTextEdit):
244
145
  def apply(self) -> bool:
245
146
  if (
@@ -273,8 +174,8 @@ class ParagraphObject(PDFObjectBase):
273
174
  """
274
175
  return getattr(self._object_ref, name)
275
176
 
276
- def edit(self) -> ParagraphEdit:
277
- return ParagraphEdit(self, self.object_ref())
177
+ def edit(self):
178
+ return ParagraphEditSession(self._client, self.object_ref())
278
179
 
279
180
  def object_ref(self) -> TextObjectRef:
280
181
  return self._object_ref
@@ -320,6 +221,114 @@ class TextLineObject(PDFObjectBase):
320
221
  self._object_ref.children == other._object_ref.children)
321
222
 
322
223
 
224
+ class ParagraphEditSession:
225
+ """
226
+ Fluent editing helper that reuses ParagraphBuilder for modifications while preserving
227
+ the legacy context-manager workflow (replace/font/color/etc.).
228
+ """
229
+
230
+ def __init__(self, client: 'PDFDancer', object_ref: TextObjectRef):
231
+ self._client = client
232
+ self._object_ref = object_ref
233
+ self._new_text = None
234
+ self._font_name = None
235
+ self._font_size = None
236
+ self._color = None
237
+ self._line_spacing = None
238
+ self._new_position = None
239
+ self._has_changes = False
240
+
241
+ def __enter__(self):
242
+ return self
243
+
244
+ def __exit__(self, exc_type, exc_val, exc_tb):
245
+ if exc_type:
246
+ return False
247
+ self.apply()
248
+ return False
249
+
250
+ def replace(self, text: str):
251
+ self._new_text = text
252
+ self._has_changes = True
253
+ return self
254
+
255
+ def font(self, font_name, font_size: float):
256
+ self._font_name = font_name
257
+ self._font_size = font_size
258
+ self._has_changes = True
259
+ return self
260
+
261
+ def color(self, color):
262
+ self._color = color
263
+ self._has_changes = True
264
+ return self
265
+
266
+ def line_spacing(self, spacing: float):
267
+ self._line_spacing = spacing
268
+ self._has_changes = True
269
+ return self
270
+
271
+ def move_to(self, x: float, y: float):
272
+ self._new_position = (x, y)
273
+ self._has_changes = True
274
+ return self
275
+
276
+ def apply(self):
277
+ if not self._has_changes:
278
+ return self._client._modify_paragraph(self._object_ref, None)
279
+
280
+ only_text_changed = (
281
+ self._new_text is not None and
282
+ self._font_name is None and
283
+ self._font_size is None and
284
+ self._color is None and
285
+ self._line_spacing is None and
286
+ self._new_position is None
287
+ )
288
+
289
+ if only_text_changed:
290
+ result = self._client._modify_paragraph(self._object_ref, self._new_text)
291
+ self._has_changes = False
292
+ return result
293
+
294
+ only_move = (
295
+ self._new_position is not None and
296
+ self._new_text is None and
297
+ self._font_name is None and
298
+ self._font_size is None and
299
+ self._color is None and
300
+ self._line_spacing is None
301
+ )
302
+
303
+ if only_move:
304
+ page_index = self._object_ref.position.page_index if self._object_ref.position else None
305
+ if page_index is None:
306
+ raise ValidationException("Paragraph position must include a page index to move")
307
+ position = Position.at_page_coordinates(page_index, *self._new_position)
308
+ result = self._client._move(self._object_ref, position)
309
+ self._has_changes = False
310
+ return result
311
+
312
+ from .paragraph_builder import ParagraphBuilder
313
+
314
+ builder = ParagraphBuilder.from_object_ref(self._client, self._object_ref)
315
+
316
+ if self._new_text is not None:
317
+ builder.text(self._new_text)
318
+ if self._font_name is not None and self._font_size is not None:
319
+ builder.font(self._font_name, self._font_size)
320
+ if self._color is not None:
321
+ builder.color(self._color)
322
+ if self._line_spacing is not None:
323
+ builder.line_spacing(self._line_spacing)
324
+ if self._new_position is not None:
325
+ builder.move_to(*self._new_position)
326
+
327
+ result = builder.modify(self._object_ref)
328
+ self._has_changes = False
329
+ return result
330
+
331
+
323
332
  class FormFieldEdit:
324
333
  def __init__(self, form_field: 'FormFieldObject', object_ref: FormFieldRef):
325
334
  self.form_field = form_field
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdfdancer-client-python
3
- Version: 0.2.19
3
+ Version: 0.2.21
4
4
  Summary: Python client for PDFDancer API
5
5
  Author-email: "The Famous Cat Ltd." <hi@thefamouscat.com>
6
6
  License: