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/__init__.py +79 -22
- pdfdancer/_runtime_version.py +30 -0
- pdfdancer/_version.py +24 -0
- pdfdancer/image_builder.py +23 -3
- pdfdancer/models.py +127 -470
- pdfdancer/page_builder.py +6 -17
- pdfdancer/path_builder.py +127 -6
- pdfdancer/{pdfdancer_v1.py → pdfdancer_v2.py} +850 -1316
- pdfdancer/text_editing.py +1472 -0
- pdfdancer/types.py +94 -399
- pdfdancer_client_python-3.0.0.dist-info/METADATA +521 -0
- pdfdancer_client_python-3.0.0.dist-info/RECORD +18 -0
- {pdfdancer_client_python-0.3.13.dist-info → pdfdancer_client_python-3.0.0.dist-info}/WHEEL +1 -1
- pdfdancer/paragraph_builder.py +0 -554
- pdfdancer/text_line_builder.py +0 -290
- pdfdancer_client_python-0.3.13.dist-info/METADATA +0 -685
- pdfdancer_client_python-0.3.13.dist-info/RECORD +0 -17
- {pdfdancer_client_python-0.3.13.dist-info → pdfdancer_client_python-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {pdfdancer_client_python-0.3.13.dist-info → pdfdancer_client_python-3.0.0.dist-info}/licenses/NOTICE +0 -0
- {pdfdancer_client_python-0.3.13.dist-info → pdfdancer_client_python-3.0.0.dist-info}/top_level.txt +0 -0
pdfdancer/text_line_builder.py
DELETED
|
@@ -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)
|