pdfdancer-client-python 0.3.13__py3-none-any.whl → 3.0.0__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.
pdfdancer/models.py CHANGED
@@ -2,6 +2,7 @@
2
2
  Model classes for the PDFDancer Python client.
3
3
  """
4
4
 
5
+ import math
5
6
  from dataclasses import dataclass
6
7
  from enum import Enum
7
8
  from typing import Any, ClassVar, Dict, List, Mapping, Optional, Tuple, Union
@@ -45,28 +46,51 @@ class PageSize:
45
46
  height: float
46
47
 
47
48
  _STANDARD_SIZES: ClassVar[Dict[str, Tuple[float, float]]] = {
49
+ "A0": (2384.0, 3370.0),
50
+ "A1": (1684.0, 2384.0),
51
+ "A2": (1191.0, 1684.0),
52
+ "A3": (842.0, 1191.0),
48
53
  "A4": (595.0, 842.0),
54
+ "A5": (420.0, 595.0),
55
+ "A6": (298.0, 420.0),
56
+ "B4": (709.0, 1001.0),
57
+ "B5": (499.0, 709.0),
49
58
  "LETTER": (612.0, 792.0),
50
59
  "LEGAL": (612.0, 1008.0),
51
60
  "TABLOID": (792.0, 1224.0),
52
- "A3": (842.0, 1191.0),
53
- "A5": (420.0, 595.0),
61
+ "EXECUTIVE": (522.0, 756.0),
62
+ "POSTCARD": (288.0, 432.0),
63
+ "INDEX_3X5": (216.0, 360.0),
54
64
  }
55
65
 
56
66
  # Convenience aliases populated after class definition; annotated for type checkers.
67
+ A0: ClassVar["PageSize"]
68
+ A1: ClassVar["PageSize"]
69
+ A2: ClassVar["PageSize"]
70
+ A3: ClassVar["PageSize"]
57
71
  A4: ClassVar["PageSize"]
72
+ A5: ClassVar["PageSize"]
73
+ A6: ClassVar["PageSize"]
74
+ B4: ClassVar["PageSize"]
75
+ B5: ClassVar["PageSize"]
58
76
  LETTER: ClassVar["PageSize"]
59
77
  LEGAL: ClassVar["PageSize"]
60
78
  TABLOID: ClassVar["PageSize"]
61
- A3: ClassVar["PageSize"]
62
- A5: ClassVar["PageSize"]
79
+ EXECUTIVE: ClassVar["PageSize"]
80
+ POSTCARD: ClassVar["PageSize"]
81
+ INDEX_3X5: ClassVar["PageSize"]
63
82
 
64
83
  def __post_init__(self) -> None:
65
84
  if not isinstance(self.width, (int, float)) or not isinstance(
66
85
  self.height, (int, float)
67
86
  ):
68
87
  raise TypeError("Page width and height must be numeric")
69
- if self.width <= 0 or self.height <= 0:
88
+ if (
89
+ not math.isfinite(self.width)
90
+ or not math.isfinite(self.height)
91
+ or self.width <= 0
92
+ or self.height <= 0
93
+ ):
70
94
  raise ValueError("Page width and height must be positive values")
71
95
 
72
96
  width = float(self.width)
@@ -82,7 +106,7 @@ class PageSize:
82
106
  self, "name", normalized_name if normalized_name else None
83
107
  )
84
108
 
85
- def to_dict(self) -> dict:
109
+ def to_dict(self) -> Dict[str, Any]:
86
110
  """Convert to dictionary for JSON serialization."""
87
111
  return {
88
112
  "name": self.name,
@@ -127,14 +151,29 @@ class PageSize:
127
151
  """Return a list of supported standard page size names."""
128
152
  return sorted(cls._STANDARD_SIZES.keys())
129
153
 
154
+ @classmethod
155
+ def from_dimensions(
156
+ cls, width: float, height: float, tolerance: float = 0.5
157
+ ) -> "PageSize":
158
+ """Recognize standard dimensions in portrait or rotated orientation."""
159
+ custom = cls(name=None, width=width, height=height)
160
+ for name, (standard_width, standard_height) in cls._STANDARD_SIZES.items():
161
+ direct = (
162
+ abs(standard_width - custom.width) < tolerance
163
+ and abs(standard_height - custom.height) < tolerance
164
+ )
165
+ rotated = (
166
+ abs(standard_width - custom.height) < tolerance
167
+ and abs(standard_height - custom.width) < tolerance
168
+ )
169
+ if direct or rotated:
170
+ return cls.from_name(name)
171
+ return custom
172
+
130
173
 
131
174
  # Populate convenience constants for standard sizes.
132
- PageSize.A4 = PageSize.from_name("A4")
133
- PageSize.LETTER = PageSize.from_name("LETTER")
134
- PageSize.LEGAL = PageSize.from_name("LEGAL")
135
- PageSize.TABLOID = PageSize.from_name("TABLOID")
136
- PageSize.A3 = PageSize.from_name("A3")
137
- PageSize.A5 = PageSize.from_name("A5")
175
+ for _page_size_name in PageSize._STANDARD_SIZES:
176
+ setattr(PageSize, _page_size_name, PageSize.from_name(_page_size_name))
138
177
 
139
178
 
140
179
  class Orientation(Enum):
@@ -191,19 +230,24 @@ class StandardFonts(Enum):
191
230
  class ObjectType(Enum):
192
231
  """Server object type discriminator used in refs, requests, and snapshots."""
193
232
 
194
- FORM_FIELD = "FORM_FIELD"
233
+ PDF = "PDF"
234
+ PAGE = "PAGE"
235
+ TEXT_ELEMENT = "TEXT_ELEMENT"
195
236
  IMAGE = "IMAGE"
196
- FORM_X_OBJECT = "FORM_X_OBJECT"
197
237
  PATH = "PATH"
198
- PARAGRAPH = "PARAGRAPH"
238
+ LINE = "LINE"
239
+ RECTANGLE = "RECTANGLE"
240
+ BEZIER = "BEZIER"
241
+ CLIPPING = "CLIPPING"
242
+ FORM_X_OBJECT = "FORM_X_OBJECT"
243
+ FORM_FIELD = "FORM_FIELD"
244
+ WORD = "WORD"
199
245
  TEXT_LINE = "TEXT_LINE"
200
- PAGE = "PAGE"
201
246
  TEXT_FIELD = "TEXT_FIELD"
202
- CHECK_BOX = "CHECK_BOX"
203
247
  RADIO_BUTTON = "RADIO_BUTTON"
204
248
  BUTTON = "BUTTON"
205
249
  DROPDOWN = "DROPDOWN"
206
- TEXT_ELEMENT = "TEXT_ELEMENT"
250
+ CHECKBOX = "CHECKBOX"
207
251
 
208
252
 
209
253
  class PositionMode(Enum):
@@ -280,8 +324,8 @@ class Position:
280
324
 
281
325
  Examples:
282
326
  ```python
283
- # A point on page 0
284
- pos = Position.at_page_coordinates(0, x=72, y=720)
327
+ # A point on page 1
328
+ pos = Position.at_page_coordinates(1, x=72, y=720)
285
329
 
286
330
  # Search by name (e.g. a form field) and then move down 12 points
287
331
  pos = Position.by_name("Email").move_y(-12)
@@ -343,13 +387,17 @@ class Position:
343
387
  def move_x(self, x_offset: float) -> "Position":
344
388
  """Move the position horizontally by the specified offset."""
345
389
  if self.bounding_rect:
346
- self.at_coordinates(Point(self.x() + x_offset, self.y()))
390
+ self.at_coordinates(
391
+ Point(self.bounding_rect.get_x() + x_offset, self.bounding_rect.get_y())
392
+ )
347
393
  return self
348
394
 
349
395
  def move_y(self, y_offset: float) -> "Position":
350
396
  """Move the position vertically by the specified offset."""
351
397
  if self.bounding_rect:
352
- self.at_coordinates(Point(self.x(), self.y() + y_offset))
398
+ self.at_coordinates(
399
+ Point(self.bounding_rect.get_x(), self.bounding_rect.get_y() + y_offset)
400
+ )
353
401
  return self
354
402
 
355
403
  def x(self) -> Optional[float]:
@@ -374,7 +422,7 @@ class ObjectRef:
374
422
  Usage:
375
423
  - Instances are typically returned in snapshots or find results.
376
424
  - Pass an `ObjectRef` to request objects such as `MoveRequest`, `DeleteRequest`,
377
- `ModifyRequest`, or `ModifyTextRequest`.
425
+ `DeleteRequest`, `MoveRequest`, or `ModifyRequest`.
378
426
 
379
427
  Example:
380
428
  ```python
@@ -404,17 +452,22 @@ class ObjectRef:
404
452
  """Returns the type classification of the referenced object."""
405
453
  return self.type
406
454
 
407
- def to_dict(self) -> dict:
408
- """Convert to dictionary for JSON serialization."""
409
- # Normalize type back to API format (API uses "CHECKBOX" not "CHECK_BOX")
410
- type_value = self.type.value
411
- if type_value == "CHECK_BOX":
412
- type_value = "CHECKBOX"
455
+ @property
456
+ def object_type(self) -> ObjectType:
457
+ """Return the object type using the public object naming convention."""
458
+ return self.type
459
+
460
+ @object_type.setter
461
+ def object_type(self, object_type: ObjectType) -> None:
462
+ """Update the object type using the public object naming convention."""
463
+ self.type = object_type
413
464
 
465
+ def to_dict(self) -> Dict[str, Any]:
466
+ """Convert to dictionary for JSON serialization."""
414
467
  return {
415
468
  "internalId": self.internal_id,
416
469
  "position": FindRequest._position_to_dict(self.position),
417
- "type": type_value,
470
+ "type": self.type.value,
418
471
  }
419
472
 
420
473
 
@@ -443,14 +496,27 @@ class Color:
443
496
  b: int
444
497
  a: int = 255 # Alpha channel, default fully opaque
445
498
 
446
- def __post_init__(self):
499
+ BLACK: ClassVar["Color"]
500
+ WHITE: ClassVar["Color"]
501
+ RED: ClassVar["Color"]
502
+
503
+ def __post_init__(self) -> None:
447
504
  for component in [self.r, self.g, self.b, self.a]:
448
- if not 0 <= component <= 255:
505
+ if (
506
+ isinstance(component, bool)
507
+ or not isinstance(component, int)
508
+ or not 0 <= component <= 255
509
+ ):
449
510
  raise ValueError(
450
511
  f"Color component must be between 0 and 255, got {component}"
451
512
  )
452
513
 
453
514
 
515
+ Color.BLACK = Color(0, 0, 0)
516
+ Color.WHITE = Color(255, 255, 255)
517
+ Color.RED = Color(255, 0, 0)
518
+
519
+
454
520
  @dataclass
455
521
  class Font:
456
522
  """Font face and size.
@@ -474,7 +540,7 @@ class Font:
474
540
  name: str
475
541
  size: float
476
542
 
477
- def __post_init__(self):
543
+ def __post_init__(self) -> None:
478
544
  if self.size <= 0:
479
545
  raise ValueError(f"Font size must be positive, got {self.size}")
480
546
 
@@ -684,107 +750,6 @@ class Image:
684
750
  self.position = position
685
751
 
686
752
 
687
- @dataclass
688
- class TextLine:
689
- """
690
- One line of text to add to a page.
691
-
692
- Parameters:
693
- - position: Anchor position where the first line begins.
694
- - text: the text
695
- provide separate entries for multiple lines.
696
- - font: Font to use for all text elements unless overridden later.
697
- - color: Text color.
698
-
699
- """
700
-
701
- position: Optional[Position] = None
702
- font: Optional[Font] = None
703
- color: Optional[Color] = None
704
- line_spacing: float = 1.2
705
- text: str = ""
706
-
707
- def get_position(self) -> Optional[Position]:
708
- """Returns the position of this paragraph."""
709
- return self.position
710
-
711
- def set_position(self, position: Position) -> None:
712
- """Sets the position of this paragraph."""
713
- self.position = position
714
-
715
-
716
- @dataclass
717
- class Paragraph:
718
- """
719
- Multi-line text paragraph to add to a page.
720
-
721
- Parameters:
722
- - position: Anchor position where the first line begins.
723
- - text_lines: List of strings, one per line. Use `\n` within a string only if desired; normally
724
- provide separate entries for multiple lines.
725
- - font: Font to use for all text elements unless overridden later.
726
- - color: Text color.
727
- - line_spacing: Distance multiplier between lines. Server expects a list, handled for you by `AddRequest`.
728
-
729
- Example:
730
- ```python
731
- from pdfdancer.models import Paragraph, Position, Font, Color, StandardFonts, AddRequest
732
-
733
- para = Paragraph(
734
- position=Position.at_page_coordinates(0, 72, 700),
735
- text_lines=["Hello", "PDFDancer!"],
736
- font=Font(StandardFonts.HELVETICA.value, 12),
737
- color=Color(50, 50, 50),
738
- line_spacing=1.4,
739
- )
740
- payload = AddRequest(para).to_dict()
741
- ```
742
- """
743
-
744
- position: Optional[Position] = None
745
- text_lines: Optional[List[TextLine]] = None
746
- font: Optional[Font] = None
747
- color: Optional[Color] = None
748
- line_spacing: float = 1.2
749
- line_spacings: Optional[List[float]] = None
750
-
751
- def get_position(self) -> Optional[Position]:
752
- """Returns the position of this paragraph."""
753
- return self.position
754
-
755
- def set_position(self, position: Position) -> None:
756
- """Sets the position of this paragraph."""
757
- self.position = position
758
-
759
- def clear_lines(self) -> None:
760
- """Removes all text lines from this paragraph."""
761
- self.text_lines = []
762
-
763
- def add_line(self, text_line: TextLine) -> None:
764
- """Appends a text line to this paragraph."""
765
- if self.text_lines is None:
766
- self.text_lines = []
767
- self.text_lines.append(text_line)
768
-
769
- def get_lines(self) -> List[TextLine]:
770
- """Returns the list of text lines, defaulting to an empty list."""
771
- if self.text_lines is None:
772
- self.text_lines = []
773
- return self.text_lines
774
-
775
- def set_lines(self, lines: List[TextLine]) -> None:
776
- """Replaces the current text lines with the provided list."""
777
- self.text_lines = list(lines)
778
-
779
- def set_line_spacings(self, spacings: Optional[List[float]]) -> None:
780
- """Sets the per-line spacing factors for this paragraph."""
781
- self.line_spacings = list(spacings) if spacings else None
782
-
783
- def get_line_spacings(self) -> Optional[List[float]]:
784
- """Returns the per-line spacing factors if present."""
785
- return list(self.line_spacings) if self.line_spacings else None
786
-
787
-
788
753
  # Request classes for API communication
789
754
  @dataclass
790
755
  class FindRequest:
@@ -799,7 +764,7 @@ class FindRequest:
799
764
  ```python
800
765
  req = FindRequest(
801
766
  object_type=ObjectType.TEXT_LINE,
802
- position=Position.at_page_coordinates(0, 72, 700).with_text_starts("Hello"),
767
+ position=Position.at_page_coordinates(1, 72, 700).with_text_starts("Hello"),
803
768
  )
804
769
  payload = req.to_dict()
805
770
  ```
@@ -809,7 +774,7 @@ class FindRequest:
809
774
  position: Optional[Position]
810
775
  hint: Optional[str] = None
811
776
 
812
- def to_dict(self) -> dict:
777
+ def to_dict(self) -> Dict[str, Any]:
813
778
  """Convert to dictionary for JSON serialization."""
814
779
  return {
815
780
  "objectType": self.object_type.value if self.object_type else None,
@@ -820,9 +785,9 @@ class FindRequest:
820
785
  }
821
786
 
822
787
  @staticmethod
823
- def _position_to_dict(position: Position) -> dict:
788
+ def _position_to_dict(position: Position) -> Dict[str, Any]:
824
789
  """Convert Position to dictionary for JSON serialization."""
825
- result = {
790
+ result: Dict[str, Any] = {
826
791
  "pageNumber": position.page_number,
827
792
  "textStartsWith": position.text_starts_with,
828
793
  "textPattern": position.text_pattern,
@@ -858,7 +823,7 @@ class DeleteRequest:
858
823
 
859
824
  object_ref: ObjectRef
860
825
 
861
- def to_dict(self) -> dict:
826
+ def to_dict(self) -> Dict[str, Any]:
862
827
  """Convert to dictionary for JSON serialization."""
863
828
  # Use ObjectRef.to_dict() to ensure proper type normalization
864
829
  return {"objectRef": self.object_ref.to_dict()}
@@ -883,7 +848,7 @@ class MoveRequest:
883
848
  object_ref: ObjectRef
884
849
  position: Position
885
850
 
886
- def to_dict(self) -> dict:
851
+ def to_dict(self) -> Dict[str, Any]:
887
852
  """Convert to dictionary for JSON serialization."""
888
853
  # Server API expects the new coordinates under 'newPosition'
889
854
  # Use ObjectRef.to_dict() to ensure proper type normalization
@@ -893,55 +858,6 @@ class MoveRequest:
893
858
  }
894
859
 
895
860
 
896
- @dataclass
897
- class RedactTarget:
898
- """A single redaction target identifying an object by its internal ID."""
899
-
900
- id: str
901
- replacement: str
902
-
903
- def to_dict(self) -> dict:
904
- return {"id": self.id, "replacement": self.replacement}
905
-
906
-
907
- @dataclass
908
- class RedactRequest:
909
- """Request for redacting content from a PDF document."""
910
-
911
- targets: List["RedactTarget"]
912
- default_replacement: str
913
- placeholder_color: "Color"
914
-
915
- def to_dict(self) -> dict:
916
- return {
917
- "targets": [t.to_dict() for t in self.targets],
918
- "defaultReplacement": self.default_replacement,
919
- "placeholderColor": {
920
- "r": self.placeholder_color.r,
921
- "g": self.placeholder_color.g,
922
- "b": self.placeholder_color.b,
923
- "a": self.placeholder_color.a,
924
- },
925
- }
926
-
927
-
928
- @dataclass
929
- class RedactResponse:
930
- """Response from a redaction operation."""
931
-
932
- count: int
933
- success: bool
934
- warnings: List[str]
935
-
936
- @classmethod
937
- def from_dict(cls, data: dict) -> "RedactResponse":
938
- return cls(
939
- count=data.get("count", 0),
940
- success=data.get("success", False),
941
- warnings=data.get("warnings", []),
942
- )
943
-
944
-
945
861
  @dataclass
946
862
  class PageMoveRequest:
947
863
  """Request to reorder pages.
@@ -961,7 +877,7 @@ class PageMoveRequest:
961
877
  from_page: int
962
878
  to_page: int
963
879
 
964
- def to_dict(self) -> dict:
880
+ def to_dict(self) -> Dict[str, Any]:
965
881
  return {
966
882
  "fromPage": self.from_page,
967
883
  "toPage": self.to_page,
@@ -985,7 +901,7 @@ class AddPageRequest:
985
901
  orientation: Optional[Orientation] = None
986
902
  page_size: Optional[PageSize] = None
987
903
 
988
- def to_dict(self) -> dict:
904
+ def to_dict(self) -> Dict[str, Any]:
989
905
  payload: Dict[str, Any] = {}
990
906
  if self.page_number is not None:
991
907
  payload["pageNumber"] = int(self.page_number)
@@ -1012,23 +928,16 @@ class AddRequest:
1012
928
  """Request to add a new object to the document.
1013
929
 
1014
930
  Parameters:
1015
- - pdf_object: The object to add (e.g. `Image`, `Paragraph`, or `Path`).
1016
-
1017
- Usage:
1018
- ```python
1019
- para = Paragraph(position=Position.at_page_coordinates(0, 72, 700), text_lines=["Hello"])
1020
- req = AddRequest(para)
1021
- payload = req.to_dict() # ready to send to the server API
1022
- ```
931
+ - pdf_object: The object to add (`Image` or `Path`).
1023
932
 
1024
933
  Notes:
1025
934
  - Serialization details (like base64 for image `data`, or per-segment position for paths)
1026
935
  are handled for you in `to_dict()`.
1027
936
  """
1028
937
 
1029
- pdf_object: Any # Can be Image, Paragraph, etc.
938
+ pdf_object: Any
1030
939
 
1031
- def to_dict(self) -> dict:
940
+ def to_dict(self) -> Dict[str, Any]:
1032
941
  """Convert to dictionary for JSON serialization matching server API.
1033
942
  Server expects an AddRequest with a nested 'object' containing the PDFObject
1034
943
  (with a 'type' discriminator).
@@ -1036,7 +945,7 @@ class AddRequest:
1036
945
  obj = self.pdf_object
1037
946
  return {"object": self._object_to_dict(obj)}
1038
947
 
1039
- def _object_to_dict(self, obj: Any) -> dict:
948
+ def _object_to_dict(self, obj: Any) -> Dict[str, Any]:
1040
949
  """Convert PDF object to dictionary for JSON serialization."""
1041
950
  import base64
1042
951
 
@@ -1084,128 +993,14 @@ class AddRequest:
1084
993
  "size": size,
1085
994
  "data": data_b64,
1086
995
  }
1087
- elif isinstance(obj, Paragraph):
1088
-
1089
- def _font_to_dict(font: Optional[Font]) -> Optional[dict]:
1090
- if font:
1091
- return {"name": font.name, "size": font.size}
1092
- return None
1093
-
1094
- def _color_to_dict(color: Optional[Color]) -> Optional[dict]:
1095
- if color:
1096
- return {
1097
- "red": color.r,
1098
- "green": color.g,
1099
- "blue": color.b,
1100
- "alpha": color.a,
1101
- }
1102
- return None
1103
-
1104
- lines_payload = []
1105
- if obj.text_lines:
1106
- for line in obj.text_lines:
1107
- if isinstance(line, TextLine):
1108
- line_text = line.text
1109
- line_font = line.font or obj.font
1110
- line_color = line.color or obj.color
1111
- line_position = line.position or obj.position
1112
- else:
1113
- line_text = str(line)
1114
- line_font = obj.font
1115
- line_color = obj.color
1116
- line_position = obj.position
1117
-
1118
- text_element = {
1119
- "text": line_text,
1120
- "font": _font_to_dict(line_font),
1121
- "color": _color_to_dict(line_color),
1122
- "position": (
1123
- FindRequest._position_to_dict(line_position)
1124
- if line_position
1125
- else None
1126
- ),
1127
- }
1128
- text_line = {"textElements": [text_element]}
1129
- if line_color:
1130
- text_line["color"] = _color_to_dict(line_color)
1131
- if line_position:
1132
- text_line["position"] = FindRequest._position_to_dict(
1133
- line_position
1134
- )
1135
- lines_payload.append(text_line)
1136
-
1137
- line_spacings = None
1138
- if getattr(obj, "line_spacings", None):
1139
- line_spacings = list(obj.line_spacings)
1140
- elif getattr(obj, "line_spacing", None) is not None:
1141
- line_spacings = [obj.line_spacing]
1142
-
1143
- return {
1144
- "type": "PARAGRAPH",
1145
- "position": (
1146
- FindRequest._position_to_dict(obj.position)
1147
- if obj.position
1148
- else None
1149
- ),
1150
- "lines": lines_payload if lines_payload else None,
1151
- "lineSpacings": line_spacings,
1152
- "font": _font_to_dict(obj.font),
1153
- }
1154
- elif isinstance(obj, TextLine):
1155
-
1156
- def _font_to_dict(font: Optional[Font]) -> Optional[dict]:
1157
- if font:
1158
- return {"name": font.name, "size": font.size}
1159
- return None
1160
-
1161
- def _color_to_dict(color: Optional[Color]) -> Optional[dict]:
1162
- if color:
1163
- return {
1164
- "red": color.r,
1165
- "green": color.g,
1166
- "blue": color.b,
1167
- "alpha": color.a,
1168
- }
1169
- return None
1170
-
1171
- # Build textElement with only non-null fields
1172
- text_element = {
1173
- "text": obj.text,
1174
- }
1175
-
1176
- if obj.font:
1177
- text_element["font"] = _font_to_dict(obj.font)
1178
- if obj.color:
1179
- text_element["color"] = _color_to_dict(obj.color)
1180
- if obj.position:
1181
- text_element["position"] = FindRequest._position_to_dict(obj.position)
1182
-
1183
- # TEXT_LINE structure matches paragraph line format (textElements only)
1184
- result = {
1185
- "type": "TEXT_LINE",
1186
- "position": (
1187
- FindRequest._position_to_dict(obj.position)
1188
- if obj.position
1189
- else None
1190
- ),
1191
- "textElements": [text_element],
1192
- }
1193
-
1194
- # Only include top-level font/color if they are not None
1195
- if obj.font:
1196
- result["font"] = _font_to_dict(obj.font)
1197
- if obj.color:
1198
- result["color"] = _color_to_dict(obj.color)
1199
-
1200
- return result
1201
996
  else:
1202
997
  raise ValueError(f"Unsupported object type: {type(obj)}")
1203
998
 
1204
- def _segment_to_dict(self, segment: "PathSegment") -> dict:
999
+ def _segment_to_dict(self, segment: "PathSegment") -> Dict[str, Any]:
1205
1000
  """Convert a PathSegment (Line or Bezier) to dictionary for JSON serialization."""
1206
1001
  from .models import Bezier, Line
1207
1002
 
1208
- result = {}
1003
+ result: Dict[str, Any] = {}
1209
1004
 
1210
1005
  # Add common PathSegment properties
1211
1006
  if segment.stroke_color:
@@ -1263,20 +1058,13 @@ class ModifyRequest:
1263
1058
 
1264
1059
  Parameters:
1265
1060
  - object_ref: The existing object to replace.
1266
- - new_object: The replacement object (e.g. `Paragraph`, `Image`, or `Path`).
1267
-
1268
- Example:
1269
- ```python
1270
- new_para = Paragraph(position=old.position, text_lines=["Updated text"])
1271
- req = ModifyRequest(object_ref=old, new_object=new_para)
1272
- payload = req.to_dict()
1273
- ```
1061
+ - new_object: The replacement object (`Image` or `Path`).
1274
1062
  """
1275
1063
 
1276
1064
  object_ref: ObjectRef
1277
1065
  new_object: Any
1278
1066
 
1279
- def to_dict(self) -> dict:
1067
+ def to_dict(self) -> Dict[str, Any]:
1280
1068
  """Convert to dictionary for JSON serialization."""
1281
1069
  # Use ObjectRef.to_dict() to ensure proper type normalization
1282
1070
  return {
@@ -1285,30 +1073,6 @@ class ModifyRequest:
1285
1073
  }
1286
1074
 
1287
1075
 
1288
- @dataclass
1289
- class ModifyTextRequest:
1290
- """Request to change the text content of a text object.
1291
-
1292
- Parameters:
1293
- - object_ref: The text object to modify (e.g. a `TextObjectRef`).
1294
- - new_text: Replacement text content.
1295
-
1296
- Example:
1297
- ```python
1298
- req = ModifyTextRequest(object_ref=text_ref, new_text="Hello world")
1299
- payload = req.to_dict()
1300
- ```
1301
- """
1302
-
1303
- object_ref: ObjectRef
1304
- new_text: str
1305
-
1306
- def to_dict(self) -> dict:
1307
- """Convert to dictionary for JSON serialization."""
1308
- # Use ObjectRef.to_dict() to ensure proper type normalization
1309
- return {"ref": self.object_ref.to_dict(), "newTextLine": self.new_text}
1310
-
1311
-
1312
1076
  @dataclass
1313
1077
  class ChangeFormFieldRequest:
1314
1078
  """Request to set a form field's value.
@@ -1328,7 +1092,7 @@ class ChangeFormFieldRequest:
1328
1092
  object_ref: ObjectRef
1329
1093
  value: str
1330
1094
 
1331
- def to_dict(self) -> dict:
1095
+ def to_dict(self) -> Dict[str, Any]:
1332
1096
  """Convert to dictionary for JSON serialization."""
1333
1097
  # Use ObjectRef.to_dict() to ensure proper type normalization
1334
1098
  return {"ref": self.object_ref.to_dict(), "value": self.value}
@@ -1342,7 +1106,7 @@ class FormFieldRef(ObjectRef):
1342
1106
  Parameters (usually provided by the server):
1343
1107
  - internal_id: Identifier of the form field object.
1344
1108
  - position: Position of the field.
1345
- - type: One of `ObjectType.TEXT_FIELD`, `ObjectType.CHECK_BOX`, etc.
1109
+ - type: One of `ObjectType.TEXT_FIELD`, `ObjectType.CHECKBOX`, etc.
1346
1110
  - name: Field name (as defined inside the PDF).
1347
1111
  - value: Current field value (string representation).
1348
1112
 
@@ -1401,7 +1165,7 @@ class TextStatus:
1401
1165
  modified: bool
1402
1166
  encodable: bool
1403
1167
  font_type: FontType
1404
- font_recommendation: FontRecommendation
1168
+ font_recommendation: Optional[FontRecommendation]
1405
1169
 
1406
1170
  def is_modified(self) -> bool:
1407
1171
  """Check if the text has been modified."""
@@ -1415,7 +1179,7 @@ class TextStatus:
1415
1179
  """Get the font type."""
1416
1180
  return self.font_type
1417
1181
 
1418
- def get_font_recommendation(self) -> FontRecommendation:
1182
+ def get_font_recommendation(self) -> Optional[FontRecommendation]:
1419
1183
  """Get the font recommendation."""
1420
1184
  return self.font_recommendation
1421
1185
 
@@ -1438,7 +1202,7 @@ class TextObjectRef(ObjectRef):
1438
1202
  Usage:
1439
1203
  - Instances are returned by find/snapshot APIs. You generally should not instantiate
1440
1204
  them manually, but you may read their properties or pass their `ObjectRef`-like
1441
- identity to modification requests (e.g., `ModifyTextRequest`).
1205
+ identity to generic object operations.
1442
1206
  """
1443
1207
 
1444
1208
  def __init__(
@@ -1548,14 +1312,14 @@ class CommandResult:
1548
1312
  warning: str | None
1549
1313
 
1550
1314
  @classmethod
1551
- def from_dict(cls, data: dict) -> "CommandResult":
1315
+ def from_dict(cls, data: Dict[str, Any]) -> "CommandResult":
1552
1316
  """Create a CommandResult from a dictionary response."""
1553
1317
  return cls(
1554
1318
  command_name=data.get("commandName", ""),
1555
- element_id=data.get("elementId", ""),
1556
- message=data.get("message", ""),
1319
+ element_id=data.get("elementId"),
1320
+ message=data.get("message"),
1557
1321
  success=data.get("success", False),
1558
- warning=data.get("warning", ""),
1322
+ warning=data.get("warning"),
1559
1323
  )
1560
1324
 
1561
1325
  @classmethod
@@ -1639,7 +1403,7 @@ class Size:
1639
1403
  width: float
1640
1404
  height: float
1641
1405
 
1642
- def to_dict(self) -> dict:
1406
+ def to_dict(self) -> Dict[str, Any]:
1643
1407
  return {"width": self.width, "height": self.height}
1644
1408
 
1645
1409
 
@@ -1682,113 +1446,6 @@ class ImageFlipDirection(Enum):
1682
1446
  BOTH = "BOTH"
1683
1447
 
1684
1448
 
1685
- class ReflowPreset(Enum):
1686
- """Reflow preset for template replacement operations.
1687
-
1688
- Controls how text reflow is handled when replacement text differs in length
1689
- from the original placeholder.
1690
-
1691
- Values:
1692
- - BEST_EFFORT: Attempt to reflow text, but proceed even if it doesn't fit perfectly.
1693
- - FIT_OR_FAIL: Reflow must succeed or the operation fails.
1694
- - NONE: No reflow - replacement text is placed as-is.
1695
- """
1696
-
1697
- BEST_EFFORT = "BEST_EFFORT"
1698
- FIT_OR_FAIL = "FIT_OR_FAIL"
1699
- NONE = "NONE"
1700
-
1701
-
1702
- @dataclass
1703
- class TemplateReplacement:
1704
- """A single template placeholder replacement.
1705
-
1706
- Parameters:
1707
- - placeholder: The exact text to find and replace in the PDF.
1708
- - text: The text to replace the placeholder with. None for image replacements.
1709
- - font: Optional font for the replacement text.
1710
- - color: Optional color for the replacement text.
1711
- - image: Optional Image to replace the placeholder with. When set, text should be None.
1712
- """
1713
-
1714
- placeholder: str
1715
- text: Optional[str] = None
1716
- font: Optional[Font] = None
1717
- color: Optional[Color] = None
1718
- image: Optional[Image] = None
1719
-
1720
- def to_dict(self) -> dict:
1721
- """Convert to dictionary for JSON serialization."""
1722
- import base64
1723
-
1724
- result: Dict[str, Any] = {
1725
- "placeholder": self.placeholder,
1726
- "text": self.text,
1727
- }
1728
- if self.font:
1729
- result["font"] = {"name": self.font.name, "size": self.font.size}
1730
- if self.color:
1731
- result["color"] = {
1732
- "red": self.color.r,
1733
- "green": self.color.g,
1734
- "blue": self.color.b,
1735
- "alpha": self.color.a,
1736
- }
1737
- if self.image:
1738
- image_dict: Dict[str, Any] = {}
1739
- if self.image.data:
1740
- image_dict["data"] = base64.b64encode(self.image.data).decode("utf-8")
1741
- if self.image.format:
1742
- image_dict["format"] = self.image.format
1743
- if self.image.width is not None or self.image.height is not None:
1744
- size: Dict[str, float] = {}
1745
- if self.image.width is not None:
1746
- size["width"] = self.image.width
1747
- if self.image.height is not None:
1748
- size["height"] = self.image.height
1749
- image_dict["size"] = size
1750
- result["image"] = image_dict
1751
- return result
1752
-
1753
-
1754
- @dataclass
1755
- class TemplateReplaceRequest:
1756
- """Request for batch template placeholder replacements.
1757
-
1758
- Parameters:
1759
- - replacements: List of TemplateReplacement objects.
1760
- - page_index: Optional 0-based page index. If None, applies to all pages.
1761
- - reflow_preset: Optional ReflowPreset for text reflow behavior.
1762
-
1763
- Example:
1764
- ```python
1765
- request = TemplateReplaceRequest(
1766
- replacements=[
1767
- TemplateReplacement("{{NAME}}", "John Doe"),
1768
- TemplateReplacement("{{DATE}}", "2025-01-15"),
1769
- ],
1770
- page_index=0, # First page (0-based)
1771
- reflow_preset=ReflowPreset.BEST_EFFORT,
1772
- )
1773
- ```
1774
- """
1775
-
1776
- replacements: List[TemplateReplacement]
1777
- page_index: Optional[int] = None
1778
- reflow_preset: Optional[ReflowPreset] = None
1779
-
1780
- def to_dict(self) -> dict:
1781
- """Convert to dictionary for JSON serialization."""
1782
- result: Dict[str, Any] = {
1783
- "replacements": [r.to_dict() for r in self.replacements],
1784
- }
1785
- if self.page_index is not None:
1786
- result["pageIndex"] = self.page_index
1787
- if self.reflow_preset is not None:
1788
- result["reflowPreset"] = self.reflow_preset.value
1789
- return result
1790
-
1791
-
1792
1449
  @dataclass
1793
1450
  class ImageTransformRequest:
1794
1451
  """Request to transform an image in the PDF document.
@@ -1825,7 +1482,7 @@ class ImageTransformRequest:
1825
1482
  fill_region_height: Optional[int] = None
1826
1483
  fill_color: Optional[int] = None
1827
1484
 
1828
- def to_dict(self) -> dict:
1485
+ def to_dict(self) -> Dict[str, Any]:
1829
1486
  """Convert to dictionary for JSON serialization."""
1830
1487
  import base64
1831
1488
 
@@ -1917,7 +1574,7 @@ class ModifyPathRequest:
1917
1574
  stroke_color: Optional[Color] = None
1918
1575
  fill_color: Optional[Color] = None
1919
1576
 
1920
- def to_dict(self) -> dict:
1577
+ def to_dict(self) -> Dict[str, Any]:
1921
1578
  """Convert to dictionary for JSON serialization."""
1922
1579
  result: Dict[str, Any] = {
1923
1580
  "ref": self.object_ref.to_dict(),