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.
@@ -1,290 +0,0 @@
1
- """
2
- TextLineBuilder for the PDFDancer Python client.
3
- Mirrors the behaviour of ParagraphBuilder for single line text objects.
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- from copy import deepcopy
9
- from pathlib import Path
10
- from typing import TYPE_CHECKING, Optional, Union
11
-
12
- from . import StandardFonts
13
- from .exceptions import ValidationException
14
- from .models import (
15
- Color,
16
- Font,
17
- ObjectRef,
18
- Position,
19
- TextLine,
20
- TextObjectRef,
21
- )
22
-
23
- if TYPE_CHECKING:
24
- from .pdfdancer_v1 import PDFDancer
25
-
26
- DEFAULT_TEXT_COLOR = Color(0, 0, 0)
27
- _DEFAULT_BASE_FONT_SIZE = 12.0
28
-
29
-
30
- class TextLineBuilder:
31
- """
32
- Fluent builder used to assemble `TextLine` instances.
33
- Behaviour is aligned with ParagraphBuilder but simplified for single-line text.
34
- """
35
-
36
- def __init__(self, client: "PDFDancer"):
37
- if client is None:
38
- raise ValidationException("Client cannot be null")
39
-
40
- self._client = client
41
- self._text_line = TextLine()
42
- self._text_color: Optional[Color] = None
43
- self._text: Optional[str] = None
44
- self._ttf_file: Optional[Path] = None
45
- self._font: Optional[Font] = None
46
- self._font_explicitly_changed = False
47
- self._original_text_line_position: Optional[Position] = None
48
- self._target_object_ref: Optional[ObjectRef] = None
49
- self._original_font: Optional[Font] = None
50
- self._original_color: Optional[Color] = None
51
- self._position_changed = False
52
-
53
- def only_text_changed(self) -> bool:
54
- """Return True when only the text payload has been modified."""
55
- return (
56
- self._text is not None
57
- and self._text_color is None
58
- and self._ttf_file is None
59
- and (self._font is None or not self._font_explicitly_changed)
60
- )
61
-
62
- def text(self, text: str, color: Optional[Color] = None) -> "TextLineBuilder":
63
- if text is None:
64
- raise ValidationException("Text cannot be null")
65
- self._text = text
66
- if color is not None:
67
- self.color(color)
68
- return self
69
-
70
- def font(
71
- self, font: Union[Font, str, StandardFonts], font_size: Optional[float] = None
72
- ) -> "TextLineBuilder":
73
- """
74
- Configure the font either by providing a `Font` instance or name + size.
75
- """
76
- if isinstance(font, Font):
77
- resolved_font = font
78
- else:
79
- if isinstance(font, StandardFonts):
80
- font = font.value
81
- if font is None:
82
- raise ValidationException("Font name cannot be null")
83
- if font_size is None:
84
- raise ValidationException(
85
- "Font size must be provided when setting font by name"
86
- )
87
- resolved_font = Font(str(font), font_size)
88
-
89
- self._font = resolved_font
90
- self._ttf_file = None
91
- self._font_explicitly_changed = True
92
- return self
93
-
94
- def font_file(
95
- self, ttf_file: Union[Path, str], font_size: float
96
- ) -> "TextLineBuilder":
97
- if ttf_file is None:
98
- raise ValidationException("TTF file cannot be null")
99
- if font_size <= 0:
100
- raise ValidationException(f"Font size must be positive, got {font_size}")
101
-
102
- ttf_path = Path(ttf_file)
103
-
104
- if not ttf_path.exists():
105
- raise ValidationException(f"TTF file does not exist: {ttf_path}")
106
- if not ttf_path.is_file():
107
- raise ValidationException(f"TTF file is not a file: {ttf_path}")
108
- if ttf_path.stat().st_size <= 0:
109
- raise ValidationException(f"TTF file is empty: {ttf_path}")
110
-
111
- try:
112
- with open(ttf_path, "rb") as handle:
113
- handle.read(1)
114
- except (IOError, OSError) as exc:
115
- raise ValidationException(f"TTF file is not readable: {ttf_path}") from exc
116
-
117
- self._ttf_file = ttf_path
118
- self._font = self._register_ttf(ttf_path, font_size)
119
- self._font_explicitly_changed = True
120
- return self
121
-
122
- def set_font_explicitly_changed(self, changed: bool) -> None:
123
- self._font_explicitly_changed = bool(changed)
124
-
125
- def set_original_text_line_position(self, position: Position) -> None:
126
- self._original_text_line_position = position
127
- if position and self._text_line.position is None:
128
- self._text_line.position = deepcopy(position)
129
-
130
- def target(self, object_ref: ObjectRef) -> "TextLineBuilder":
131
- if object_ref is None:
132
- raise ValidationException("Object reference cannot be null")
133
- self._target_object_ref = object_ref
134
- return self
135
-
136
- def color(self, color: Color) -> "TextLineBuilder":
137
- if color is None:
138
- raise ValidationException("Color cannot be null")
139
- self._text_color = color
140
- return self
141
-
142
- def move_to(self, x: float, y: float) -> "TextLineBuilder":
143
- """
144
- Move the text line to new coordinates on the same page.
145
- """
146
- position = self._text_line.position
147
- if (
148
- position is None
149
- and self._target_object_ref
150
- and self._target_object_ref.position
151
- ):
152
- position = deepcopy(self._target_object_ref.position)
153
- self._text_line.position = position
154
-
155
- if position is None:
156
- raise ValidationException(
157
- "Cannot move text line without an existing position"
158
- )
159
-
160
- page_number = position.page_number
161
- if page_number is None:
162
- raise ValidationException(
163
- "Text line position must include a page number to move"
164
- )
165
-
166
- self._position_changed = True
167
- return self.at(page_number, x, y)
168
-
169
- def at_position(self, position: Position) -> "TextLineBuilder":
170
- if position is None:
171
- raise ValidationException("Position cannot be null")
172
- # Defensive copy so builder mutations do not alter original references
173
- self._text_line.position = deepcopy(position)
174
- self._position_changed = True
175
- return self
176
-
177
- def at(self, page_number: int, x: float, y: float) -> "TextLineBuilder":
178
- return self.at_position(Position.at_page_coordinates(page_number, x, y))
179
-
180
- def get_text(self) -> Optional[str]:
181
- return self._text
182
-
183
- def add(self) -> bool:
184
- """
185
- Add a new text line to the document.
186
- Note: Text lines are typically part of paragraphs. This method is not
187
- currently supported for standalone text lines.
188
- """
189
- raise NotImplementedError(
190
- "Adding standalone text lines is not supported. "
191
- "Text lines should be added as part of paragraphs."
192
- )
193
-
194
- def modify(self, object_ref: Optional[ObjectRef] = None):
195
- target_ref = object_ref or self._target_object_ref
196
- if target_ref is None:
197
- raise ValidationException(
198
- "Object reference must be provided to modify a text line"
199
- )
200
-
201
- if self.only_text_changed():
202
- # Backend accepts plain text updates for simple edits
203
- return self._client._modify_text_line(target_ref, self._text or "")
204
-
205
- text_line = self._finalize_text_line()
206
- # Use /pdf/modify endpoint for complex modifications
207
- return self._client._modify_text_line_full(target_ref, text_line)
208
-
209
- # ------------------------------------------------------------------ #
210
- # Internal helpers
211
- # ------------------------------------------------------------------ #
212
-
213
- def _finalize_text_line(self) -> TextLine:
214
- if self._text_line.position is None:
215
- raise ValidationException("Position must be set before building text line")
216
-
217
- if (
218
- self._target_object_ref is None
219
- and self._font is None
220
- and self._text_line.font is None
221
- ):
222
- raise ValidationException("Font must be set before building text line")
223
-
224
- if self._text is not None:
225
- self._text_line.text = self._text
226
- elif not self._text_line.text:
227
- raise ValidationException("Text must be provided for text line")
228
-
229
- final_font = self._font if self._font is not None else self._original_font
230
- if final_font is None:
231
- final_font = Font(StandardFonts.HELVETICA.value, _DEFAULT_BASE_FONT_SIZE)
232
- self._text_line.font = final_font
233
-
234
- if self._text_color is not None:
235
- final_color = self._text_color
236
- elif self._text is not None:
237
- final_color = self._original_color or DEFAULT_TEXT_COLOR
238
- else:
239
- final_color = self._original_color
240
-
241
- # Ensure color is never None
242
- if final_color is None:
243
- final_color = DEFAULT_TEXT_COLOR
244
- self._text_line.color = final_color
245
-
246
- return self._text_line
247
-
248
- def _register_ttf(self, ttf_file: Path, font_size: float) -> Font:
249
- try:
250
- font_name = self._client.register_font(ttf_file)
251
- return Font(font_name, font_size)
252
- except Exception as exc:
253
- raise ValidationException(
254
- f"Failed to register font file {ttf_file}: {exc}"
255
- ) from exc
256
-
257
- @classmethod
258
- def from_object_ref(
259
- cls, client: "PDFDancer", object_ref: TextObjectRef
260
- ) -> "TextLineBuilder":
261
- if object_ref is None:
262
- raise ValidationException("Object reference cannot be null")
263
-
264
- builder = cls(client)
265
- builder.target(object_ref)
266
-
267
- if object_ref.position:
268
- builder.at_position(object_ref.position)
269
- builder.set_original_text_line_position(object_ref.position)
270
-
271
- if object_ref.font_name and object_ref.font_size:
272
- builder._original_font = Font(object_ref.font_name, object_ref.font_size)
273
-
274
- if object_ref.color:
275
- builder._original_color = object_ref.color
276
-
277
- if object_ref.text:
278
- builder._text_line.text = object_ref.text
279
-
280
- return builder
281
-
282
-
283
- class TextLinePageBuilder(TextLineBuilder):
284
- def __init__(self, client: "PDFDancer", page_number: int):
285
- super().__init__(client)
286
- self._page_number: Optional[int] = page_number
287
-
288
- # noinspection PyMethodOverriding
289
- def at(self, x: float, y: float) -> "TextLineBuilder":
290
- return super().at(self._page_number, x, y)