pdfdancer-client-python 0.1.1__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 +40 -0
- pdfdancer/client_v1.py +675 -0
- pdfdancer/exceptions.py +57 -0
- pdfdancer/models.py +417 -0
- pdfdancer/paragraph_builder.py +267 -0
- pdfdancer_client_python-0.1.1.dist-info/METADATA +308 -0
- pdfdancer_client_python-0.1.1.dist-info/RECORD +9 -0
- pdfdancer_client_python-0.1.1.dist-info/WHEEL +5 -0
- pdfdancer_client_python-0.1.1.dist-info/top_level.txt +1 -0
pdfdancer/client_v1.py
ADDED
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PDFDancer Python Client V1
|
|
3
|
+
|
|
4
|
+
A Python client that closely mirrors the Java Client class structure and functionality.
|
|
5
|
+
Provides session-based PDF manipulation operations with strict validation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Optional, Union, BinaryIO
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
|
|
14
|
+
from .exceptions import (
|
|
15
|
+
PdfDancerException,
|
|
16
|
+
FontNotFoundException,
|
|
17
|
+
HttpClientException,
|
|
18
|
+
SessionException,
|
|
19
|
+
ValidationException
|
|
20
|
+
)
|
|
21
|
+
from .models import (
|
|
22
|
+
ObjectRef, Position, ObjectType, Font, Image, Paragraph,
|
|
23
|
+
FindRequest, DeleteRequest, MoveRequest, AddRequest, ModifyRequest, ModifyTextRequest, ShapeType, PositionMode
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ClientV1:
|
|
28
|
+
"""
|
|
29
|
+
REST API client for interacting with the PDFDancer PDF manipulation service.
|
|
30
|
+
This client provides a convenient Python interface for performing PDF operations
|
|
31
|
+
including session management, object searching, manipulation, and retrieval.
|
|
32
|
+
Handles authentication, session lifecycle, and HTTP communication transparently.
|
|
33
|
+
|
|
34
|
+
Mirrors the Java Client class functionality exactly.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, token: str, pdf_data: Union[bytes, Path, str, BinaryIO],
|
|
38
|
+
base_url: str = "http://localhost:8080", read_timeout: float = 30.0):
|
|
39
|
+
"""
|
|
40
|
+
Creates a new client with PDF data.
|
|
41
|
+
This constructor initializes the client, uploads the PDF data to create
|
|
42
|
+
a new session, and prepares the client for PDF manipulation operations.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
token: Authentication token for API access
|
|
46
|
+
pdf_data: PDF file data as bytes, Path, filename string, or file-like object
|
|
47
|
+
base_url: Base URL of the PDFDancer API server
|
|
48
|
+
read_timeout: Timeout in seconds for HTTP requests (default: 30.0)
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
ValidationException: If token is empty or PDF data is invalid
|
|
52
|
+
SessionException: If session creation fails
|
|
53
|
+
HttpClientException: If HTTP communication fails
|
|
54
|
+
"""
|
|
55
|
+
# Strict validation like Java client
|
|
56
|
+
if not token or not token.strip():
|
|
57
|
+
raise ValidationException("Authentication token cannot be null or empty")
|
|
58
|
+
|
|
59
|
+
self._token = token.strip()
|
|
60
|
+
self._base_url = base_url.rstrip('/')
|
|
61
|
+
self._read_timeout = read_timeout
|
|
62
|
+
|
|
63
|
+
# Process PDF data with validation
|
|
64
|
+
self._pdf_bytes = self._process_pdf_data(pdf_data)
|
|
65
|
+
|
|
66
|
+
# Create HTTP session for connection reuse
|
|
67
|
+
self._session = requests.Session()
|
|
68
|
+
self._session.headers.update({
|
|
69
|
+
'Authorization': f'Bearer {self._token}'
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
# Create session - equivalent to Java constructor behavior
|
|
73
|
+
self._session_id = self._create_session()
|
|
74
|
+
|
|
75
|
+
def _process_pdf_data(self, pdf_data: Union[bytes, Path, str, BinaryIO]) -> bytes:
|
|
76
|
+
"""
|
|
77
|
+
Process PDF data from various input types with strict validation.
|
|
78
|
+
Equivalent to readFile() method in Java client.
|
|
79
|
+
"""
|
|
80
|
+
if pdf_data is None:
|
|
81
|
+
raise ValidationException("PDF data cannot be null")
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
if isinstance(pdf_data, bytes):
|
|
85
|
+
if len(pdf_data) == 0:
|
|
86
|
+
raise ValidationException("PDF data cannot be empty")
|
|
87
|
+
return pdf_data
|
|
88
|
+
|
|
89
|
+
elif isinstance(pdf_data, (Path, str)):
|
|
90
|
+
file_path = Path(pdf_data)
|
|
91
|
+
if not file_path.exists():
|
|
92
|
+
raise ValidationException(f"PDF file does not exist: {file_path}")
|
|
93
|
+
if not file_path.is_file():
|
|
94
|
+
raise ValidationException(f"Path is not a file: {file_path}")
|
|
95
|
+
if not file_path.stat().st_size > 0:
|
|
96
|
+
raise ValidationException(f"PDF file is empty: {file_path}")
|
|
97
|
+
|
|
98
|
+
with open(file_path, 'rb') as f:
|
|
99
|
+
return f.read()
|
|
100
|
+
|
|
101
|
+
elif hasattr(pdf_data, 'read'):
|
|
102
|
+
# File-like object
|
|
103
|
+
data = pdf_data.read()
|
|
104
|
+
if isinstance(data, str):
|
|
105
|
+
data = data.encode('utf-8')
|
|
106
|
+
if len(data) == 0:
|
|
107
|
+
raise ValidationException("PDF data from file-like object is empty")
|
|
108
|
+
return data
|
|
109
|
+
|
|
110
|
+
else:
|
|
111
|
+
raise ValidationException(f"Unsupported PDF data type: {type(pdf_data)}")
|
|
112
|
+
|
|
113
|
+
except (IOError, OSError) as e:
|
|
114
|
+
raise PdfDancerException(f"Failed to read PDF data: {e}", cause=e)
|
|
115
|
+
|
|
116
|
+
def _extract_error_message(self, response: Optional[requests.Response]) -> str:
|
|
117
|
+
"""
|
|
118
|
+
Extract meaningful error messages from API response.
|
|
119
|
+
Parses JSON error responses with _embedded.errors structure.
|
|
120
|
+
"""
|
|
121
|
+
if response is None:
|
|
122
|
+
return "Unknown error"
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
# Try to parse JSON response
|
|
126
|
+
error_data = response.json()
|
|
127
|
+
|
|
128
|
+
# Check for embedded errors structure
|
|
129
|
+
if "_embedded" in error_data and "errors" in error_data["_embedded"]:
|
|
130
|
+
errors = error_data["_embedded"]["errors"]
|
|
131
|
+
if errors and isinstance(errors, list):
|
|
132
|
+
# Extract all error messages
|
|
133
|
+
messages = []
|
|
134
|
+
for error in errors:
|
|
135
|
+
if isinstance(error, dict) and "message" in error:
|
|
136
|
+
messages.append(error["message"])
|
|
137
|
+
|
|
138
|
+
if messages:
|
|
139
|
+
return "; ".join(messages)
|
|
140
|
+
|
|
141
|
+
# Check for top-level message
|
|
142
|
+
if "message" in error_data:
|
|
143
|
+
return error_data["message"]
|
|
144
|
+
|
|
145
|
+
# Fallback to response content
|
|
146
|
+
return response.text or f"HTTP {response.status_code}"
|
|
147
|
+
|
|
148
|
+
except (json.JSONDecodeError, KeyError, TypeError):
|
|
149
|
+
# If JSON parsing fails, return response content or status
|
|
150
|
+
return response.text or f"HTTP {response.status_code}"
|
|
151
|
+
|
|
152
|
+
def _create_session(self) -> str:
|
|
153
|
+
"""
|
|
154
|
+
Creates a new PDF processing session by uploading the PDF data.
|
|
155
|
+
Equivalent to createSession() method in Java client.
|
|
156
|
+
"""
|
|
157
|
+
try:
|
|
158
|
+
files = {
|
|
159
|
+
'pdf': ('document.pdf', self._pdf_bytes, 'application/pdf')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
response = self._session.post(
|
|
163
|
+
f"{self._base_url}/session/create",
|
|
164
|
+
files=files,
|
|
165
|
+
timeout=self._read_timeout if self._read_timeout > 0 else None
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
response.raise_for_status()
|
|
169
|
+
session_id = response.text.strip()
|
|
170
|
+
|
|
171
|
+
if not session_id:
|
|
172
|
+
raise SessionException("Server returned empty session ID")
|
|
173
|
+
|
|
174
|
+
return session_id
|
|
175
|
+
|
|
176
|
+
except requests.exceptions.RequestException as e:
|
|
177
|
+
error_message = self._extract_error_message(getattr(e, 'response', None))
|
|
178
|
+
raise HttpClientException(f"Failed to create session: {error_message}",
|
|
179
|
+
response=getattr(e, 'response', None), cause=e) from None
|
|
180
|
+
|
|
181
|
+
def _make_request(self, method: str, path: str, data: Optional[dict] = None,
|
|
182
|
+
params: Optional[dict] = None) -> requests.Response:
|
|
183
|
+
"""
|
|
184
|
+
Make HTTP request with session headers and error handling.
|
|
185
|
+
Equivalent to retrieve() method pattern in Java client.
|
|
186
|
+
"""
|
|
187
|
+
headers = {
|
|
188
|
+
'X-Session-Id': self._session_id,
|
|
189
|
+
'Content-Type': 'application/json'
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
response = self._session.request(
|
|
194
|
+
method=method,
|
|
195
|
+
url=f"{self._base_url}{path}",
|
|
196
|
+
json=data,
|
|
197
|
+
params=params,
|
|
198
|
+
headers=headers,
|
|
199
|
+
timeout=self._read_timeout if self._read_timeout > 0 else None
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Handle FontNotFoundException specifically like Java client
|
|
203
|
+
if response.status_code == 404:
|
|
204
|
+
try:
|
|
205
|
+
error_data = response.json()
|
|
206
|
+
if error_data.get('error') == 'FontNotFoundException':
|
|
207
|
+
raise FontNotFoundException(error_data.get('message', 'Font not found'))
|
|
208
|
+
except (json.JSONDecodeError, KeyError):
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
response.raise_for_status()
|
|
212
|
+
return response
|
|
213
|
+
|
|
214
|
+
except requests.exceptions.RequestException as e:
|
|
215
|
+
error_message = self._extract_error_message(getattr(e, 'response', None))
|
|
216
|
+
raise HttpClientException(f"API request failed: {error_message}", response=getattr(e, 'response', None),
|
|
217
|
+
cause=e) from None
|
|
218
|
+
|
|
219
|
+
# Search Operations - matching Java client exactly
|
|
220
|
+
|
|
221
|
+
def find(self, object_type: Optional[ObjectType] = None, position: Optional[Position] = None) -> List[ObjectRef]:
|
|
222
|
+
"""
|
|
223
|
+
Searches for PDF objects matching the specified criteria.
|
|
224
|
+
This method provides flexible search capabilities across all PDF content,
|
|
225
|
+
allowing filtering by object type and position constraints.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
object_type: The type of objects to find (None for all types)
|
|
229
|
+
position: Positional constraints for the search (None for all positions)
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
List of object references matching the search criteria
|
|
233
|
+
"""
|
|
234
|
+
request_data = FindRequest(object_type, position).to_dict()
|
|
235
|
+
response = self._make_request('POST', '/pdf/find', data=request_data)
|
|
236
|
+
|
|
237
|
+
# Parse response into ObjectRef objects
|
|
238
|
+
objects_data = response.json()
|
|
239
|
+
return [self._parse_object_ref(obj_data) for obj_data in objects_data]
|
|
240
|
+
|
|
241
|
+
def find_paragraphs(self, position: Optional[Position] = None) -> List[ObjectRef]:
|
|
242
|
+
"""
|
|
243
|
+
Searches for paragraph objects at the specified position.
|
|
244
|
+
Equivalent to findParagraphs() in Java client.
|
|
245
|
+
"""
|
|
246
|
+
return self.find(ObjectType.PARAGRAPH, position)
|
|
247
|
+
|
|
248
|
+
def find_images(self, position: Optional[Position] = None) -> List[ObjectRef]:
|
|
249
|
+
"""
|
|
250
|
+
Searches for image objects at the specified position.
|
|
251
|
+
Equivalent to findImages() in Java client.
|
|
252
|
+
"""
|
|
253
|
+
return self.find(ObjectType.IMAGE, position)
|
|
254
|
+
|
|
255
|
+
def find_forms(self, position: Optional[Position] = None) -> List[ObjectRef]:
|
|
256
|
+
"""
|
|
257
|
+
Searches for form field objects at the specified position.
|
|
258
|
+
Equivalent to findForms() in Java client.
|
|
259
|
+
"""
|
|
260
|
+
return self.find(ObjectType.FORM, position)
|
|
261
|
+
|
|
262
|
+
def find_paths(self, position: Optional[Position] = None) -> List[ObjectRef]:
|
|
263
|
+
"""
|
|
264
|
+
Searches for vector path objects at the specified position.
|
|
265
|
+
Equivalent to findPaths() in Java client.
|
|
266
|
+
"""
|
|
267
|
+
return self.find(ObjectType.PATH, position)
|
|
268
|
+
|
|
269
|
+
def find_text_lines(self, position: Optional[Position] = None) -> List[ObjectRef]:
|
|
270
|
+
"""
|
|
271
|
+
Searches for text line objects at the specified position.
|
|
272
|
+
Equivalent to findTextLines() in Java client.
|
|
273
|
+
"""
|
|
274
|
+
return self.find(ObjectType.TEXT_LINE, position)
|
|
275
|
+
|
|
276
|
+
# Page Operations
|
|
277
|
+
|
|
278
|
+
def get_pages(self) -> List[ObjectRef]:
|
|
279
|
+
"""
|
|
280
|
+
Retrieves references to all pages in the PDF document.
|
|
281
|
+
Equivalent to getPages() in Java client.
|
|
282
|
+
"""
|
|
283
|
+
response = self._make_request('POST', '/pdf/page/find')
|
|
284
|
+
pages_data = response.json()
|
|
285
|
+
return [self._parse_object_ref(page_data) for page_data in pages_data]
|
|
286
|
+
|
|
287
|
+
def get_page(self, page_index: int) -> Optional[ObjectRef]:
|
|
288
|
+
"""
|
|
289
|
+
Retrieves a reference to a specific page by its page index.
|
|
290
|
+
Equivalent to getPage() in Java client.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
page_index: The page index to retrieve (1-based indexing)
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
Object reference for the specified page, or None if not found
|
|
297
|
+
"""
|
|
298
|
+
if page_index < 0:
|
|
299
|
+
raise ValidationException(f"Page index must be >= 0, got {page_index}")
|
|
300
|
+
|
|
301
|
+
params = {'pageIndex': page_index}
|
|
302
|
+
response = self._make_request('POST', '/pdf/page/find', params=params)
|
|
303
|
+
|
|
304
|
+
pages_data = response.json()
|
|
305
|
+
if not pages_data:
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
return self._parse_object_ref(pages_data[0])
|
|
309
|
+
|
|
310
|
+
def delete_page(self, page_ref: ObjectRef) -> bool:
|
|
311
|
+
"""
|
|
312
|
+
Deletes a page from the PDF document.
|
|
313
|
+
Equivalent to deletePage() in Java client.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
page_ref: Reference to the page to be deleted
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
True if the page was successfully deleted
|
|
320
|
+
"""
|
|
321
|
+
if page_ref is None:
|
|
322
|
+
raise ValidationException("Page reference cannot be null")
|
|
323
|
+
|
|
324
|
+
request_data = page_ref.to_dict()
|
|
325
|
+
|
|
326
|
+
response = self._make_request('DELETE', '/pdf/page/delete', data=request_data)
|
|
327
|
+
return response.json()
|
|
328
|
+
|
|
329
|
+
# Manipulation Operations
|
|
330
|
+
|
|
331
|
+
def delete(self, object_ref: ObjectRef) -> bool:
|
|
332
|
+
"""
|
|
333
|
+
Deletes the specified PDF object from the document.
|
|
334
|
+
Equivalent to delete() in Java client.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
object_ref: Reference to the object to be deleted
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
True if the object was successfully deleted
|
|
341
|
+
"""
|
|
342
|
+
if object_ref is None:
|
|
343
|
+
raise ValidationException("Object reference cannot be null")
|
|
344
|
+
|
|
345
|
+
request_data = DeleteRequest(object_ref).to_dict()
|
|
346
|
+
response = self._make_request('DELETE', '/pdf/delete', data=request_data)
|
|
347
|
+
return response.json()
|
|
348
|
+
|
|
349
|
+
def move(self, object_ref: ObjectRef, position: Position) -> bool:
|
|
350
|
+
"""
|
|
351
|
+
Moves a PDF object to a new position within the document.
|
|
352
|
+
Equivalent to move() in Java client.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
object_ref: Reference to the object to be moved
|
|
356
|
+
position: New position for the object
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
True if the object was successfully moved
|
|
360
|
+
"""
|
|
361
|
+
if object_ref is None:
|
|
362
|
+
raise ValidationException("Object reference cannot be null")
|
|
363
|
+
if position is None:
|
|
364
|
+
raise ValidationException("Position cannot be null")
|
|
365
|
+
|
|
366
|
+
request_data = MoveRequest(object_ref, position).to_dict()
|
|
367
|
+
response = self._make_request('PUT', '/pdf/move', data=request_data)
|
|
368
|
+
return response.json()
|
|
369
|
+
|
|
370
|
+
# Add Operations
|
|
371
|
+
|
|
372
|
+
def add_image(self, image: Image, position: Optional[Position] = None) -> bool:
|
|
373
|
+
"""
|
|
374
|
+
Adds an image to the PDF document.
|
|
375
|
+
Equivalent to addImage() methods in Java client.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
image: The image object to add
|
|
379
|
+
position: Optional position override
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
True if the image was successfully added
|
|
383
|
+
"""
|
|
384
|
+
if image is None:
|
|
385
|
+
raise ValidationException("Image cannot be null")
|
|
386
|
+
|
|
387
|
+
if position is not None:
|
|
388
|
+
image.set_position(position)
|
|
389
|
+
|
|
390
|
+
if image.get_position() is None:
|
|
391
|
+
raise ValidationException("Image position is null")
|
|
392
|
+
|
|
393
|
+
return self._add_object(image)
|
|
394
|
+
|
|
395
|
+
def add_paragraph(self, paragraph: Paragraph) -> bool:
|
|
396
|
+
"""
|
|
397
|
+
Adds a paragraph to the PDF document.
|
|
398
|
+
Equivalent to addParagraph() in Java client with validation.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
paragraph: The paragraph object to add
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
True if the paragraph was successfully added
|
|
405
|
+
"""
|
|
406
|
+
if paragraph is None:
|
|
407
|
+
raise ValidationException("Paragraph cannot be null")
|
|
408
|
+
if paragraph.get_position() is None:
|
|
409
|
+
raise ValidationException("Paragraph position is null")
|
|
410
|
+
if paragraph.get_position().page_index is None:
|
|
411
|
+
raise ValidationException("Paragraph position page index is null")
|
|
412
|
+
if paragraph.get_position().page_index < 0:
|
|
413
|
+
raise ValidationException("Paragraph position page index is less than 0")
|
|
414
|
+
|
|
415
|
+
return self._add_object(paragraph)
|
|
416
|
+
|
|
417
|
+
def _add_object(self, pdf_object) -> bool:
|
|
418
|
+
"""
|
|
419
|
+
Internal method to add any PDF object.
|
|
420
|
+
Equivalent to addObject() in Java client.
|
|
421
|
+
"""
|
|
422
|
+
request_data = AddRequest(pdf_object).to_dict()
|
|
423
|
+
response = self._make_request('POST', '/pdf/add', data=request_data)
|
|
424
|
+
return response.json()
|
|
425
|
+
|
|
426
|
+
# Modify Operations
|
|
427
|
+
|
|
428
|
+
def modify_paragraph(self, object_ref: ObjectRef, new_paragraph: Union[Paragraph, str]) -> bool:
|
|
429
|
+
"""
|
|
430
|
+
Modifies a paragraph object or its text content.
|
|
431
|
+
Equivalent to modifyParagraph() methods in Java client.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
object_ref: Reference to the paragraph to modify
|
|
435
|
+
new_paragraph: New paragraph object or text string
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
True if the paragraph was successfully modified
|
|
439
|
+
"""
|
|
440
|
+
if object_ref is None:
|
|
441
|
+
raise ValidationException("Object reference cannot be null")
|
|
442
|
+
if new_paragraph is None:
|
|
443
|
+
raise ValidationException("New paragraph cannot be null")
|
|
444
|
+
|
|
445
|
+
if isinstance(new_paragraph, str):
|
|
446
|
+
# Text modification
|
|
447
|
+
request_data = ModifyTextRequest(object_ref, new_paragraph).to_dict()
|
|
448
|
+
response = self._make_request('PUT', '/pdf/text/paragraph', data=request_data)
|
|
449
|
+
else:
|
|
450
|
+
# Object modification
|
|
451
|
+
request_data = ModifyRequest(object_ref, new_paragraph).to_dict()
|
|
452
|
+
response = self._make_request('PUT', '/pdf/modify', data=request_data)
|
|
453
|
+
|
|
454
|
+
return response.json()
|
|
455
|
+
|
|
456
|
+
def modify_text_line(self, object_ref: ObjectRef, new_text: str) -> bool:
|
|
457
|
+
"""
|
|
458
|
+
Modifies a text line object.
|
|
459
|
+
Equivalent to modifyTextLine() in Java client.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
object_ref: Reference to the text line to modify
|
|
463
|
+
new_text: New text content
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
True if the text line was successfully modified
|
|
467
|
+
"""
|
|
468
|
+
if object_ref is None:
|
|
469
|
+
raise ValidationException("Object reference cannot be null")
|
|
470
|
+
if new_text is None:
|
|
471
|
+
raise ValidationException("New text cannot be null")
|
|
472
|
+
|
|
473
|
+
request_data = ModifyTextRequest(object_ref, new_text).to_dict()
|
|
474
|
+
response = self._make_request('PUT', '/pdf/text/line', data=request_data)
|
|
475
|
+
return response.json()
|
|
476
|
+
|
|
477
|
+
# Font Operations
|
|
478
|
+
|
|
479
|
+
def find_fonts(self, font_name: str, font_size: int) -> List[Font]:
|
|
480
|
+
"""
|
|
481
|
+
Finds available fonts matching the specified name and size.
|
|
482
|
+
Equivalent to findFonts() in Java client.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
font_name: Name of the font to search for
|
|
486
|
+
font_size: Size of the font
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
List of matching Font objects
|
|
490
|
+
"""
|
|
491
|
+
if not font_name or not font_name.strip():
|
|
492
|
+
raise ValidationException("Font name cannot be null or empty")
|
|
493
|
+
if font_size <= 0:
|
|
494
|
+
raise ValidationException(f"Font size must be positive, got {font_size}")
|
|
495
|
+
|
|
496
|
+
params = {'fontName': font_name.strip()}
|
|
497
|
+
response = self._make_request('GET', '/font/find', params=params)
|
|
498
|
+
|
|
499
|
+
font_names = response.json()
|
|
500
|
+
return [Font(name, font_size) for name in font_names]
|
|
501
|
+
|
|
502
|
+
def register_font(self, ttf_file: Union[Path, str, bytes, BinaryIO]) -> str:
|
|
503
|
+
"""
|
|
504
|
+
Registers a custom font for use in PDF operations.
|
|
505
|
+
Equivalent to registerFont() in Java client.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
ttf_file: TTF font file as Path, filename, bytes, or file-like object
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
Font registration result
|
|
512
|
+
|
|
513
|
+
Raises:
|
|
514
|
+
ValidationException: If font file is invalid
|
|
515
|
+
HttpClientException: If registration fails
|
|
516
|
+
"""
|
|
517
|
+
if ttf_file is None:
|
|
518
|
+
raise ValidationException("TTF file cannot be null")
|
|
519
|
+
|
|
520
|
+
# Process font file with validation similar to PDF processing
|
|
521
|
+
try:
|
|
522
|
+
if isinstance(ttf_file, bytes):
|
|
523
|
+
if len(ttf_file) == 0:
|
|
524
|
+
raise ValidationException("Font data cannot be empty")
|
|
525
|
+
font_data = ttf_file
|
|
526
|
+
filename = 'font.ttf'
|
|
527
|
+
|
|
528
|
+
elif isinstance(ttf_file, (Path, str)):
|
|
529
|
+
font_path = Path(ttf_file)
|
|
530
|
+
if not font_path.exists():
|
|
531
|
+
raise ValidationException(f"TTF file does not exist: {font_path}")
|
|
532
|
+
if not font_path.is_file():
|
|
533
|
+
raise ValidationException(f"TTF file is not a file: {font_path}")
|
|
534
|
+
if not font_path.stat().st_size > 0:
|
|
535
|
+
raise ValidationException(f"TTF file is empty: {font_path}")
|
|
536
|
+
|
|
537
|
+
with open(font_path, 'rb') as f:
|
|
538
|
+
font_data = f.read()
|
|
539
|
+
filename = font_path.name
|
|
540
|
+
|
|
541
|
+
elif hasattr(ttf_file, 'read'):
|
|
542
|
+
font_data = ttf_file.read()
|
|
543
|
+
if isinstance(font_data, str):
|
|
544
|
+
font_data = font_data.encode('utf-8')
|
|
545
|
+
if len(font_data) == 0:
|
|
546
|
+
raise ValidationException("Font data from file-like object is empty")
|
|
547
|
+
filename = getattr(ttf_file, 'name', 'font.ttf')
|
|
548
|
+
|
|
549
|
+
else:
|
|
550
|
+
raise ValidationException(f"Unsupported font file type: {type(ttf_file)}")
|
|
551
|
+
|
|
552
|
+
# Upload font file
|
|
553
|
+
files = {
|
|
554
|
+
'ttfFile': (filename, font_data, 'font/ttf')
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
headers = {'X-Session-Id': self._session_id}
|
|
558
|
+
response = self._session.post(
|
|
559
|
+
f"{self._base_url}/font/register",
|
|
560
|
+
files=files,
|
|
561
|
+
headers=headers,
|
|
562
|
+
timeout=30
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
response.raise_for_status()
|
|
566
|
+
return response.text.strip()
|
|
567
|
+
|
|
568
|
+
except (IOError, OSError) as e:
|
|
569
|
+
raise PdfDancerException(f"Failed to read font file: {e}", cause=e)
|
|
570
|
+
except requests.exceptions.RequestException as e:
|
|
571
|
+
error_message = self._extract_error_message(getattr(e, 'response', None))
|
|
572
|
+
raise HttpClientException(f"Font registration failed: {error_message}",
|
|
573
|
+
response=getattr(e, 'response', None), cause=e) from None
|
|
574
|
+
|
|
575
|
+
# Document Operations
|
|
576
|
+
|
|
577
|
+
def get_pdf_file(self) -> bytes:
|
|
578
|
+
"""
|
|
579
|
+
Downloads the current state of the PDF document with all modifications applied.
|
|
580
|
+
Equivalent to getPDFFile() in Java client.
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
PDF file data as bytes with all session modifications applied
|
|
584
|
+
"""
|
|
585
|
+
response = self._make_request('GET', f'/session/{self._session_id}/pdf')
|
|
586
|
+
return response.content
|
|
587
|
+
|
|
588
|
+
def save_pdf(self, file_path: Union[str, Path]) -> None:
|
|
589
|
+
"""
|
|
590
|
+
Saves the current PDF to a file.
|
|
591
|
+
Equivalent to savePDF() in Java client.
|
|
592
|
+
|
|
593
|
+
Args:
|
|
594
|
+
file_path: Path where to save the PDF file
|
|
595
|
+
|
|
596
|
+
Raises:
|
|
597
|
+
ValidationException: If file path is invalid
|
|
598
|
+
PdfDancerException: If file writing fails
|
|
599
|
+
"""
|
|
600
|
+
if not file_path:
|
|
601
|
+
raise ValidationException("File path cannot be null or empty")
|
|
602
|
+
|
|
603
|
+
try:
|
|
604
|
+
pdf_data = self.get_pdf_file()
|
|
605
|
+
output_path = Path(file_path)
|
|
606
|
+
|
|
607
|
+
# Create parent directories if they don't exist
|
|
608
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
609
|
+
|
|
610
|
+
with open(output_path, 'wb') as f:
|
|
611
|
+
f.write(pdf_data)
|
|
612
|
+
|
|
613
|
+
except (IOError, OSError) as e:
|
|
614
|
+
raise PdfDancerException(f"Failed to save PDF file: {e}", cause=e)
|
|
615
|
+
|
|
616
|
+
# Utility Methods
|
|
617
|
+
|
|
618
|
+
def _parse_object_ref(self, obj_data: dict) -> ObjectRef:
|
|
619
|
+
"""Parse JSON object data into ObjectRef instance."""
|
|
620
|
+
position_data = obj_data.get('position', {})
|
|
621
|
+
position = self._parse_position(position_data) if position_data else None
|
|
622
|
+
|
|
623
|
+
object_type = ObjectType(obj_data['type'])
|
|
624
|
+
|
|
625
|
+
return ObjectRef(
|
|
626
|
+
internal_id=obj_data['internalId'],
|
|
627
|
+
position=position,
|
|
628
|
+
type=object_type
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
def _parse_position(self, pos_data: dict) -> Position:
|
|
632
|
+
"""Parse JSON position data into Position instance."""
|
|
633
|
+
position = Position()
|
|
634
|
+
position.page_index = pos_data.get('pageIndex')
|
|
635
|
+
position.text_starts_with = pos_data.get('textStartsWith')
|
|
636
|
+
|
|
637
|
+
if 'shape' in pos_data:
|
|
638
|
+
position.shape = ShapeType(pos_data['shape'])
|
|
639
|
+
if 'mode' in pos_data:
|
|
640
|
+
position.mode = PositionMode(pos_data['mode'])
|
|
641
|
+
|
|
642
|
+
if 'boundingRect' in pos_data:
|
|
643
|
+
rect_data = pos_data['boundingRect']
|
|
644
|
+
from .models import BoundingRect
|
|
645
|
+
position.bounding_rect = BoundingRect(
|
|
646
|
+
x=rect_data['x'],
|
|
647
|
+
y=rect_data['y'],
|
|
648
|
+
width=rect_data['width'],
|
|
649
|
+
height=rect_data['height']
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
return position
|
|
653
|
+
|
|
654
|
+
# Builder Pattern Support
|
|
655
|
+
|
|
656
|
+
def paragraph_builder(self) -> 'ParagraphBuilder':
|
|
657
|
+
"""
|
|
658
|
+
Creates a new ParagraphBuilder for fluent paragraph construction.
|
|
659
|
+
Equivalent to paragraphBuilder() in Java client.
|
|
660
|
+
|
|
661
|
+
Returns:
|
|
662
|
+
A new ParagraphBuilder instance
|
|
663
|
+
"""
|
|
664
|
+
from .paragraph_builder import ParagraphBuilder
|
|
665
|
+
return ParagraphBuilder(self)
|
|
666
|
+
|
|
667
|
+
# Context Manager Support (Python enhancement)
|
|
668
|
+
def __enter__(self):
|
|
669
|
+
"""Context manager entry."""
|
|
670
|
+
return self
|
|
671
|
+
|
|
672
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
673
|
+
"""Context manager exit - cleanup if needed."""
|
|
674
|
+
# Could add session cleanup here if API supports it
|
|
675
|
+
pass
|