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/__init__.py +2 -0
- pdfdancer/image_builder.py +30 -0
- pdfdancer/models.py +545 -58
- pdfdancer/page_builder.py +92 -0
- pdfdancer/paragraph_builder.py +392 -177
- pdfdancer/path_builder.py +252 -0
- pdfdancer/pdfdancer_v1.py +61 -22
- pdfdancer/types.py +112 -103
- {pdfdancer_client_python-0.2.19.dist-info → pdfdancer_client_python-0.2.21.dist-info}/METADATA +1 -1
- pdfdancer_client_python-0.2.21.dist-info/RECORD +16 -0
- pdfdancer_client_python-0.2.19.dist-info/RECORD +0 -15
- {pdfdancer_client_python-0.2.19.dist-info → pdfdancer_client_python-0.2.21.dist-info}/WHEEL +0 -0
- {pdfdancer_client_python-0.2.19.dist-info → pdfdancer_client_python-0.2.21.dist-info}/licenses/LICENSE +0 -0
- {pdfdancer_client_python-0.2.19.dist-info → pdfdancer_client_python-0.2.21.dist-info}/licenses/NOTICE +0 -0
- {pdfdancer_client_python-0.2.19.dist-info → pdfdancer_client_python-0.2.21.dist-info}/top_level.txt +0 -0
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,
|
|
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
|
|
281
|
-
|
|
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
|
|
1303
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
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
|
|
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.
|
|
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
|
|
5
|
+
from typing import Optional
|
|
7
6
|
|
|
8
|
-
from . import ObjectType, Position, ObjectRef, Point,
|
|
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)
|
|
277
|
-
return
|
|
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
|