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.
@@ -0,0 +1,267 @@
1
+ """
2
+ ParagraphBuilder for the PDFDancer Python client.
3
+ Closely mirrors the Java ParagraphBuilder class with Python conventions.
4
+ """
5
+
6
+ from pathlib import Path
7
+ from typing import Optional, Union, TYPE_CHECKING
8
+
9
+ from .exceptions import ValidationException
10
+ from .models import Paragraph, Font, Color, Position
11
+
12
+ if TYPE_CHECKING:
13
+ from .client_v1 import ClientV1
14
+
15
+
16
+ class ParagraphBuilder:
17
+ """
18
+ Builder class for constructing Paragraph objects with fluent interface.
19
+ Mirrors the Java ParagraphBuilder class exactly.
20
+ """
21
+
22
+ def __init__(self, client: 'ClientV1'):
23
+ """
24
+ Initialize the paragraph builder with a client reference.
25
+
26
+ Args:
27
+ client: The ClientV1 instance for font registration
28
+ """
29
+ if client is None:
30
+ raise ValidationException("Client cannot be null")
31
+
32
+ self._client = client
33
+ self._paragraph = Paragraph()
34
+ self._line_spacing = 1.2
35
+ self._text_color = Color(0, 0, 0) # Black by default
36
+ self._text: Optional[str] = None
37
+ self._ttf_file: Optional[Path] = None
38
+ self._font: Optional[Font] = None
39
+
40
+ def from_string(self, text: str, color: Optional[Color] = None) -> 'ParagraphBuilder':
41
+ """
42
+ Set the text content for the paragraph.
43
+ Equivalent to fromString() methods in Java ParagraphBuilder.
44
+
45
+ Args:
46
+ text: The text content for the paragraph
47
+ color: Optional text color (uses default if not provided)
48
+
49
+ Returns:
50
+ Self for method chaining
51
+
52
+ Raises:
53
+ ValidationException: If text is None or empty
54
+ """
55
+ if text is None:
56
+ raise ValidationException("Text cannot be null")
57
+ if not text.strip():
58
+ raise ValidationException("Text cannot be empty")
59
+
60
+ self._text = text
61
+ if color is not None:
62
+ self._text_color = color
63
+
64
+ return self
65
+
66
+ def with_font(self, font: Font) -> 'ParagraphBuilder':
67
+ """
68
+ Set the font for the paragraph using an existing Font object.
69
+ Equivalent to withFont(Font) in Java ParagraphBuilder.
70
+
71
+ Args:
72
+ font: The Font object to use
73
+
74
+ Returns:
75
+ Self for method chaining
76
+
77
+ Raises:
78
+ ValidationException: If font is None
79
+ """
80
+ if font is None:
81
+ raise ValidationException("Font cannot be null")
82
+
83
+ self._font = font
84
+ self._ttf_file = None # Clear TTF file when using existing font
85
+ return self
86
+
87
+ def with_font_file(self, ttf_file: Union[Path, str], font_size: float) -> 'ParagraphBuilder':
88
+ """
89
+ Set the font for the paragraph using a TTF file.
90
+ Equivalent to withFont(File, double) in Java ParagraphBuilder.
91
+
92
+ Args:
93
+ ttf_file: Path to the TTF font file
94
+ font_size: Size of the font
95
+
96
+ Returns:
97
+ Self for method chaining
98
+
99
+ Raises:
100
+ ValidationException: If TTF file is invalid or font size is not positive
101
+ """
102
+ if ttf_file is None:
103
+ raise ValidationException("TTF file cannot be null")
104
+ if font_size <= 0:
105
+ raise ValidationException(f"Font size must be positive, got {font_size}")
106
+
107
+ ttf_path = Path(ttf_file)
108
+
109
+ # Strict validation like Java client
110
+ if not ttf_path.exists():
111
+ raise ValidationException(f"TTF file does not exist: {ttf_path}")
112
+ if not ttf_path.is_file():
113
+ raise ValidationException(f"TTF file is not a file: {ttf_path}")
114
+ if not ttf_path.stat().st_size > 0:
115
+ raise ValidationException(f"TTF file is empty: {ttf_path}")
116
+
117
+ # Check file permissions
118
+ try:
119
+ with open(ttf_path, 'rb') as f:
120
+ f.read(1) # Try to read one byte to check readability
121
+ except (IOError, OSError):
122
+ raise ValidationException(f"TTF file is not readable: {ttf_path}")
123
+
124
+ self._ttf_file = ttf_path
125
+ self._font = self._register_ttf(ttf_path, font_size)
126
+ return self
127
+
128
+ def with_line_spacing(self, spacing: float) -> 'ParagraphBuilder':
129
+ """
130
+ Set the line spacing for the paragraph.
131
+ Equivalent to withLineSpacing() in Java ParagraphBuilder.
132
+
133
+ Args:
134
+ spacing: Line spacing value (typically 1.0 to 2.0)
135
+
136
+ Returns:
137
+ Self for method chaining
138
+
139
+ Raises:
140
+ ValidationException: If spacing is not positive
141
+ """
142
+ if spacing <= 0:
143
+ raise ValidationException(f"Line spacing must be positive, got {spacing}")
144
+
145
+ self._line_spacing = spacing
146
+ return self
147
+
148
+ def with_color(self, color: Color) -> 'ParagraphBuilder':
149
+ """
150
+ Set the text color for the paragraph.
151
+ Equivalent to withColor() in Java ParagraphBuilder.
152
+
153
+ Args:
154
+ color: The Color object for the text
155
+
156
+ Returns:
157
+ Self for method chaining
158
+
159
+ Raises:
160
+ ValidationException: If color is None
161
+ """
162
+ if color is None:
163
+ raise ValidationException("Color cannot be null")
164
+
165
+ self._text_color = color
166
+ return self
167
+
168
+ def with_position(self, position: Position) -> 'ParagraphBuilder':
169
+ """
170
+ Set the position for the paragraph.
171
+ Equivalent to withPosition() in Java ParagraphBuilder.
172
+
173
+ Args:
174
+ position: The Position object for the paragraph
175
+
176
+ Returns:
177
+ Self for method chaining
178
+
179
+ Raises:
180
+ ValidationException: If position is None
181
+ """
182
+ if position is None:
183
+ raise ValidationException("Position cannot be null")
184
+
185
+ self._paragraph.set_position(position)
186
+ return self
187
+
188
+ def build(self) -> Paragraph:
189
+ """
190
+ Build and return the final Paragraph object.
191
+ Equivalent to build() in Java ParagraphBuilder.
192
+
193
+ This method validates all required fields and constructs the final paragraph
194
+ with text processing similar to ParagraphUtil.finalizeText() in Java.
195
+
196
+ Returns:
197
+ The constructed Paragraph object
198
+
199
+ Raises:
200
+ ValidationException: If required fields are missing or invalid
201
+ """
202
+ # Validate required fields
203
+ if self._text is None:
204
+ raise ValidationException("Text must be set before building paragraph")
205
+ if self._font is None:
206
+ raise ValidationException("Font must be set before building paragraph")
207
+ if self._paragraph.get_position() is None:
208
+ raise ValidationException("Position must be set before building paragraph")
209
+
210
+ # Set paragraph properties
211
+ self._paragraph.font = self._font
212
+ self._paragraph.color = self._text_color
213
+ self._paragraph.line_spacing = self._line_spacing
214
+
215
+ # Process text into lines (simplified version of ParagraphUtil.finalizeText)
216
+ # In the full implementation, this would handle text wrapping, line breaks, etc.
217
+ self._paragraph.text_lines = self._process_text_lines(self._text)
218
+
219
+ return self._paragraph
220
+
221
+ def _register_ttf(self, ttf_file: Path, font_size: float) -> Font:
222
+ """
223
+ Register a TTF font with the client and return a Font object.
224
+ Equivalent to registerTTF() private method in Java ParagraphBuilder.
225
+
226
+ Args:
227
+ ttf_file: Path to the TTF font file
228
+ font_size: Size of the font
229
+
230
+ Returns:
231
+ Font object with the registered font name and size
232
+ """
233
+ try:
234
+ font_name = self._client.register_font(ttf_file)
235
+ return Font(font_name, font_size)
236
+ except Exception as e:
237
+ raise ValidationException(f"Failed to register font file {ttf_file}: {e}")
238
+
239
+ def _process_text_lines(self, text: str) -> list[str]:
240
+ """
241
+ Process text into lines for the paragraph.
242
+ This is a simplified version - the full implementation would handle
243
+ word wrapping, line breaks, and other text formatting based on the font
244
+ and paragraph width.
245
+
246
+ Args:
247
+ text: The input text to process
248
+
249
+ Returns:
250
+ List of text lines for the paragraph
251
+ """
252
+ # Handle escaped newlines (\\n) as actual newlines
253
+ processed_text = text.replace('\\n', '\n')
254
+
255
+ # Simple implementation - split on newlines
256
+ # In the full version, this would implement proper text layout
257
+ lines = processed_text.split('\n')
258
+
259
+ # Remove empty lines at the end but preserve intentional line breaks
260
+ while lines and not lines[-1].strip():
261
+ lines.pop()
262
+
263
+ # Ensure at least one line
264
+ if not lines:
265
+ lines = ['']
266
+
267
+ return lines
@@ -0,0 +1,308 @@
1
+ Metadata-Version: 2.4
2
+ Name: pdfdancer-client-python
3
+ Version: 0.1.1
4
+ Summary: Python client for PDFDancer API
5
+ Author-email: TFC <info@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/tfc/pdfdancer-python
8
+ Project-URL: Repository, https://github.com/tfc/pdfdancer-python.git
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: requests>=2.25.0
19
+ Requires-Dist: pydantic>=1.8.0
20
+ Requires-Dist: typing-extensions>=4.0.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=7.0; extra == "dev"
23
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
24
+ Requires-Dist: black>=22.0; extra == "dev"
25
+ Requires-Dist: flake8>=5.0; extra == "dev"
26
+ Requires-Dist: mypy>=1.0; extra == "dev"
27
+
28
+ # PDFDancer Python Client
29
+
30
+ A Python client library for the PDFDancer PDF manipulation API that closely mirrors the Java client structure and functionality.
31
+
32
+ ## Features
33
+
34
+ - **100% Manual Implementation** - Pure Python, no code generation
35
+ - **Java Client Compatibility** - Same methods, validation, and patterns
36
+ - **Session-based Operations** - Automatic session management
37
+ - **Builder Pattern** - Fluent ParagraphBuilder interface
38
+ - **Strict Validation** - Matches Java client validation exactly
39
+ - **Python Enhancements** - Type hints, context managers, Pathlib support
40
+ - **Comprehensive Testing** - 77 tests covering all functionality
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install pdfdancer-client-python
46
+ # Or for development:
47
+ pip install -e .
48
+ ```
49
+
50
+ ## Quick Start
51
+
52
+ ```python
53
+ from pdfdancer import ClientV1, Position, Font, Color
54
+
55
+ # Create client (mirrors Java: new Client(token, pdfFile))
56
+ client = ClientV1(token="your-jwt-token", pdf_data="document.pdf")
57
+
58
+ # Find operations (mirrors Java client methods)
59
+ paragraphs = client.find_paragraphs(None)
60
+ images = client.find_images(Position.from_page_index(0))
61
+
62
+ # Manipulation operations (mirrors Java client methods)
63
+ client.delete(paragraphs[0])
64
+ client.move(images[0], Position.on_page_coordinates(0, 100, 200))
65
+
66
+ # Builder pattern (mirrors Java ParagraphBuilder)
67
+ paragraph = (client.paragraph_builder()
68
+ .from_string("Hello World")
69
+ .with_font(Font("Arial", 12))
70
+ .with_color(Color(255, 0, 0))
71
+ .with_position(Position.from_page_index(0))
72
+ .build())
73
+
74
+ client.add_paragraph(paragraph)
75
+
76
+ # Save result (mirrors Java savePDF)
77
+ client.save_pdf("output.pdf")
78
+ ```
79
+
80
+ ## Context Manager (Python Enhancement)
81
+
82
+ ```python
83
+ from pdfdancer import ClientV1
84
+
85
+ # Automatic resource management
86
+ with ClientV1(token="jwt-token", pdf_data="input.pdf") as client:
87
+ paragraphs = client.find_paragraphs(None)
88
+ client.delete(paragraphs[0])
89
+ client.save_pdf("output.pdf")
90
+ # Session automatically cleaned up
91
+ ```
92
+
93
+ ## API Methods
94
+
95
+ ### Constructor Patterns
96
+ ```python
97
+ # File path (Java: new Client(token, new File("pdf")))
98
+ client = ClientV1(token="jwt-token", pdf_data="document.pdf")
99
+
100
+ # Bytes (Java: new Client(token, pdfBytes, httpClient))
101
+ client = ClientV1(token="jwt-token", pdf_data=pdf_bytes)
102
+
103
+ # Custom server
104
+ client = ClientV1(token="jwt-token", pdf_data=pdf_file, base_url="https://api.server")
105
+ ```
106
+
107
+ ### Find Operations
108
+ ```python
109
+ # Generic find (Java: client.find())
110
+ objects = client.find(ObjectType.PARAGRAPH, position)
111
+
112
+ # Specific finders (Java: client.findParagraphs(), etc.)
113
+ paragraphs = client.find_paragraphs(position)
114
+ images = client.find_images(position)
115
+ forms = client.find_forms(position)
116
+ paths = client.find_paths(position)
117
+ text_lines = client.find_text_lines(position)
118
+
119
+ # Page operations (Java: client.getPages(), client.getPage())
120
+ pages = client.get_pages()
121
+ page = client.get_page(1) # 1-based indexing
122
+ ```
123
+
124
+ ### Manipulation Operations
125
+ ```python
126
+ # Delete (Java: client.delete(), client.deletePage())
127
+ result = client.delete(object_ref)
128
+ result = client.delete_page(page_ref)
129
+
130
+ # Move (Java: client.move())
131
+ result = client.move(object_ref, new_position)
132
+
133
+ # Add (Java: client.addImage(), client.addParagraph())
134
+ result = client.add_image(image, position)
135
+ result = client.add_paragraph(paragraph)
136
+
137
+ # Modify (Java: client.modifyParagraph(), client.modifyTextLine())
138
+ result = client.modify_paragraph(ref, new_paragraph)
139
+ result = client.modify_text_line(ref, "new text")
140
+ ```
141
+
142
+ ### Builder Pattern
143
+ ```python
144
+ # Java: client.paragraphBuilder()
145
+ builder = client.paragraph_builder()
146
+
147
+ # Fluent interface (mirrors Java ParagraphBuilder)
148
+ paragraph = (builder
149
+ .from_string("Text content") # Java: fromString()
150
+ .with_font(Font("Arial", 12)) # Java: withFont()
151
+ .with_color(Color(255, 0, 0)) # Java: withColor()
152
+ .with_line_spacing(1.5) # Java: withLineSpacing()
153
+ .with_position(position) # Java: withPosition()
154
+ .build()) # Java: build()
155
+
156
+ # Font file registration (Java: withFont(File, double))
157
+ paragraph = (builder
158
+ .with_font_file("custom.ttf", 14.0) # Java: withFont(File, double)
159
+ .from_string("Custom font text")
160
+ .with_position(position)
161
+ .build())
162
+ ```
163
+
164
+ ### Position API
165
+
166
+ ```python
167
+ from pdfdancer import Position
168
+
169
+ # Factory methods (Java: Position.fromPageNumber(), Position.onPageCoordinates())
170
+ position = Position.from_page_index(0)
171
+ position = Position.on_page_coordinates(0, 100, 200)
172
+
173
+ # Coordinate access (Java: position.getX(), position.getY())
174
+ x = position.get_x()
175
+ y = position.get_y()
176
+
177
+ # Movement (Java: position.moveX(), position.moveY())
178
+ position.move_x(50.0)
179
+ position.move_y(-25.0)
180
+
181
+ # Copy (Java: position.copy())
182
+ position_copy = position.copy()
183
+ ```
184
+
185
+ ### Font Operations
186
+ ```python
187
+ # Find fonts (Java: client.findFonts())
188
+ fonts = client.find_fonts("Arial", 12)
189
+
190
+ # Register custom font (Java: client.registerFont())
191
+ font_name = client.register_font("custom.ttf")
192
+ font_name = client.register_font(Path("font.ttf"))
193
+ font_name = client.register_font(font_bytes)
194
+ ```
195
+
196
+ ### Document Operations
197
+ ```python
198
+ # Get PDF content (Java: client.getPDFFile())
199
+ pdf_bytes = client.get_pdf_file()
200
+
201
+ # Save PDF (Java: client.savePDF())
202
+ client.save_pdf("output.pdf")
203
+ client.save_pdf(Path("output.pdf"))
204
+ ```
205
+
206
+ ## Exception Handling
207
+
208
+ ```python
209
+ from pdfdancer import (
210
+ PdfDancerException, ValidationException,
211
+ FontNotFoundException, HttpClientException
212
+ )
213
+
214
+ try:
215
+ client = ClientV1(token="", pdf_data=b"pdf")
216
+ except ValidationException as e: # Java: IllegalArgumentException
217
+ print(f"Validation error: {e}")
218
+
219
+ try:
220
+ fonts = client.find_fonts("NonExistentFont", 12)
221
+ except FontNotFoundException as e: # Java: FontNotFoundException
222
+ print(f"Font not found: {e}")
223
+ ```
224
+
225
+ ## Data Models
226
+
227
+ ```python
228
+ from pdfdancer import ObjectRef, Position, Font, Color, ObjectType
229
+
230
+ # Object reference (Java: ObjectRef)
231
+ obj_ref = ObjectRef(internal_id="obj-123", position=position, type=ObjectType.PARAGRAPH)
232
+
233
+ # Font (Java: Font)
234
+ font = Font(name="Arial", size=12.0)
235
+
236
+ # Color (Java: Color) - RGB values 0-255
237
+ color = Color(r=255, g=128, b=0)
238
+
239
+ # Position with bounding rectangle (Java: Position, BoundingRect)
240
+ position = Position.on_page_coordinates(page=0, x=100.0, y=200.0)
241
+ ```
242
+
243
+ ## Development
244
+
245
+ ### Setup
246
+ ```bash
247
+ python -m venv venv
248
+ source venv/bin/activate # On Windows: venv\Scripts\activate
249
+ pip install -e .
250
+ pip install -r requirements-dev.txt
251
+ ```
252
+
253
+ ### Testing
254
+ ```bash
255
+ # Run all tests (77 tests)
256
+ python -m pytest tests/ -v
257
+
258
+ # Run specific test files
259
+ python -m pytest tests/test_client_v1.py -v
260
+ python -m pytest tests/test_paragraph_builder.py -v
261
+ python -m pytest tests/test_models.py -v
262
+ ```
263
+
264
+ ### Run Demo
265
+ ```bash
266
+ python demo.py
267
+ ```
268
+
269
+ ### Build Package
270
+ ```bash
271
+ python -m build
272
+ python -m twine check dist/*
273
+ ```
274
+
275
+ ## Java Client Mapping
276
+
277
+ | Java Method | Python Method | Description |
278
+ |-------------|---------------|-------------|
279
+ | `new Client(token, file)` | `ClientV1(token="", pdf_data="")` | Constructor |
280
+ | `findParagraphs(position)` | `find_paragraphs(position)` | Find paragraphs |
281
+ | `findImages(position)` | `find_images(position)` | Find images |
282
+ | `delete(objectRef)` | `delete(object_ref)` | Delete object |
283
+ | `move(objectRef, position)` | `move(object_ref, position)` | Move object |
284
+ | `addParagraph(paragraph)` | `add_paragraph(paragraph)` | Add paragraph |
285
+ | `getPDFFile()` | `get_pdf_file()` | Get PDF bytes |
286
+ | `savePDF(path)` | `save_pdf(path)` | Save to file |
287
+ | `paragraphBuilder()` | `paragraph_builder()` | Create builder |
288
+ | `findFonts(name, size)` | `find_fonts(name, size)` | Find fonts |
289
+ | `registerFont(ttfFile)` | `register_font(ttf_file)` | Register font |
290
+
291
+ ## Architecture
292
+
293
+ - **Pure Manual Implementation** - No code generation, uses `requests` for HTTP
294
+ - **Session-based** - Constructor creates session, all operations use session ID
295
+ - **Strict Validation** - Matches Java client validation exactly
296
+ - **Type Safety** - Full type hints throughout
297
+ - **Error Handling** - Complete exception hierarchy
298
+ - **Python Conventions** - snake_case methods, context managers, Pathlib support
299
+
300
+ ## Requirements
301
+
302
+ - Python 3.8+
303
+ - `requests` library for HTTP communication
304
+ - `pathlib` for file handling (built-in)
305
+
306
+ ## License
307
+
308
+ [Add your license information here]
@@ -0,0 +1,9 @@
1
+ pdfdancer/__init__.py,sha256=uSIbj9rI81_o_asKd6Er15rI_Fa-TcMl1N7BEq7P_Gc,964
2
+ pdfdancer/client_v1.py,sha256=ZjJkNprzenwUmRJDRwbnXrl93lZBnLok26ytwAxFDWQ,25103
3
+ pdfdancer/exceptions.py,sha256=Y5zwNVZprsv2hvKX304cXWobJt11nrEhCzLklu2wiO8,1567
4
+ pdfdancer/models.py,sha256=-uuN3CY9HWvWom3sQC7vDD_KGrntB59W92vnacn3duc,13906
5
+ pdfdancer/paragraph_builder.py,sha256=uBMSNhL3b5DgbCJWf5VFWxgm3RpsQyQukk67FDd86Bs,8727
6
+ pdfdancer_client_python-0.1.1.dist-info/METADATA,sha256=Qm5zqznfrZ8tcctp--w7cXrpGWElmD9H-8K5Oa3kK_s,9313
7
+ pdfdancer_client_python-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ pdfdancer_client_python-0.1.1.dist-info/top_level.txt,sha256=ICwSVRpcCKrdBF9QlaX9Y0e_N3Nk1p7QVxadGOnbxeY,10
9
+ pdfdancer_client_python-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ pdfdancer