pdfdancer-client-python 0.2.12__py3-none-any.whl → 0.2.14__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.
Potentially problematic release.
This version of pdfdancer-client-python might be problematic. Click here for more details.
- pdfdancer/__init__.py +4 -1
- pdfdancer/models.py +135 -2
- pdfdancer/paragraph_builder.py +8 -3
- pdfdancer/pdfdancer_v1.py +340 -20
- {pdfdancer_client_python-0.2.12.dist-info → pdfdancer_client_python-0.2.14.dist-info}/METADATA +40 -29
- pdfdancer_client_python-0.2.14.dist-info/RECORD +11 -0
- pdfdancer_client_python-0.2.12.dist-info/RECORD +0 -11
- {pdfdancer_client_python-0.2.12.dist-info → pdfdancer_client_python-0.2.14.dist-info}/WHEEL +0 -0
- {pdfdancer_client_python-0.2.12.dist-info → pdfdancer_client_python-0.2.14.dist-info}/top_level.txt +0 -0
pdfdancer/__init__.py
CHANGED
|
@@ -12,7 +12,7 @@ from .exceptions import (
|
|
|
12
12
|
)
|
|
13
13
|
from .models import (
|
|
14
14
|
ObjectRef, Position, ObjectType, Font, Color, Image, BoundingRect, Paragraph, FormFieldRef, TextObjectRef,
|
|
15
|
-
PositionMode, ShapeType, Point, StandardFonts
|
|
15
|
+
PageRef, PositionMode, ShapeType, Point, StandardFonts, PageSize, Orientation
|
|
16
16
|
)
|
|
17
17
|
from .paragraph_builder import ParagraphBuilder
|
|
18
18
|
|
|
@@ -30,10 +30,13 @@ __all__ = [
|
|
|
30
30
|
"Paragraph",
|
|
31
31
|
"FormFieldRef",
|
|
32
32
|
"TextObjectRef",
|
|
33
|
+
"PageRef",
|
|
33
34
|
"PositionMode",
|
|
34
35
|
"ShapeType",
|
|
35
36
|
"Point",
|
|
36
37
|
"StandardFonts",
|
|
38
|
+
"PageSize",
|
|
39
|
+
"Orientation",
|
|
37
40
|
"PdfDancerException",
|
|
38
41
|
"FontNotFoundException",
|
|
39
42
|
"ValidationException",
|
pdfdancer/models.py
CHANGED
|
@@ -1,11 +1,113 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Model classes for the PDFDancer Python client.
|
|
3
|
-
Closely mirrors the Java model classes with Python conventions.
|
|
4
3
|
"""
|
|
5
4
|
|
|
6
5
|
from dataclasses import dataclass
|
|
7
6
|
from enum import Enum
|
|
8
|
-
from typing import Optional, List, Any
|
|
7
|
+
from typing import Optional, List, Any, Dict, Mapping, Tuple, ClassVar, Union
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class PageSize:
|
|
12
|
+
"""Represents a page size specification, covering both standard and custom dimensions."""
|
|
13
|
+
|
|
14
|
+
name: Optional[str]
|
|
15
|
+
width: float
|
|
16
|
+
height: float
|
|
17
|
+
|
|
18
|
+
_STANDARD_SIZES: ClassVar[Dict[str, Tuple[float, float]]] = {
|
|
19
|
+
"A4": (595.0, 842.0),
|
|
20
|
+
"LETTER": (612.0, 792.0),
|
|
21
|
+
"LEGAL": (612.0, 1008.0),
|
|
22
|
+
"TABLOID": (792.0, 1224.0),
|
|
23
|
+
"A3": (842.0, 1191.0),
|
|
24
|
+
"A5": (420.0, 595.0),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Convenience aliases populated after class definition; annotated for type checkers.
|
|
28
|
+
A4: ClassVar['PageSize']
|
|
29
|
+
LETTER: ClassVar['PageSize']
|
|
30
|
+
LEGAL: ClassVar['PageSize']
|
|
31
|
+
TABLOID: ClassVar['PageSize']
|
|
32
|
+
A3: ClassVar['PageSize']
|
|
33
|
+
A5: ClassVar['PageSize']
|
|
34
|
+
|
|
35
|
+
def __post_init__(self) -> None:
|
|
36
|
+
if not isinstance(self.width, (int, float)) or not isinstance(self.height, (int, float)):
|
|
37
|
+
raise TypeError("Page width and height must be numeric")
|
|
38
|
+
if self.width <= 0 or self.height <= 0:
|
|
39
|
+
raise ValueError("Page width and height must be positive values")
|
|
40
|
+
|
|
41
|
+
width = float(self.width)
|
|
42
|
+
height = float(self.height)
|
|
43
|
+
object.__setattr__(self, 'width', width)
|
|
44
|
+
object.__setattr__(self, 'height', height)
|
|
45
|
+
|
|
46
|
+
if self.name is not None:
|
|
47
|
+
if not isinstance(self.name, str):
|
|
48
|
+
raise TypeError("Page size name must be a string when provided")
|
|
49
|
+
normalized_name = self.name.strip().upper()
|
|
50
|
+
object.__setattr__(self, 'name', normalized_name if normalized_name else None)
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> dict:
|
|
53
|
+
"""Convert to dictionary for JSON serialization."""
|
|
54
|
+
return {
|
|
55
|
+
"name": self.name,
|
|
56
|
+
"width": self.width,
|
|
57
|
+
"height": self.height,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_name(cls, name: str) -> 'PageSize':
|
|
62
|
+
"""Create a page size from a known standard name."""
|
|
63
|
+
if not name or not isinstance(name, str):
|
|
64
|
+
raise ValueError("Page size name must be a non-empty string")
|
|
65
|
+
normalized = name.strip().upper()
|
|
66
|
+
if normalized not in cls._STANDARD_SIZES:
|
|
67
|
+
raise ValueError(f"Unknown page size name: {name}")
|
|
68
|
+
width, height = cls._STANDARD_SIZES[normalized]
|
|
69
|
+
return cls(name=normalized, width=width, height=height)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_dict(cls, data: Mapping[str, Any]) -> 'PageSize':
|
|
73
|
+
"""Create a page size from a dictionary-like object."""
|
|
74
|
+
width = data.get('width') if isinstance(data, Mapping) else None
|
|
75
|
+
height = data.get('height') if isinstance(data, Mapping) else None
|
|
76
|
+
if width is None or height is None:
|
|
77
|
+
raise ValueError("Page size dictionary must contain 'width' and 'height'")
|
|
78
|
+
name = data.get('name') if isinstance(data, Mapping) else None
|
|
79
|
+
return cls(name=name, width=width, height=height)
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def coerce(cls, value: Union['PageSize', str, Mapping[str, Any]]) -> 'PageSize':
|
|
83
|
+
"""Normalize various page size inputs into a PageSize instance."""
|
|
84
|
+
if isinstance(value, cls):
|
|
85
|
+
return value
|
|
86
|
+
if isinstance(value, str):
|
|
87
|
+
return cls.from_name(value)
|
|
88
|
+
if isinstance(value, Mapping):
|
|
89
|
+
return cls.from_dict(value)
|
|
90
|
+
raise TypeError(f"Cannot convert type {type(value)} to PageSize")
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def standard_names(cls) -> List[str]:
|
|
94
|
+
"""Return a list of supported standard page size names."""
|
|
95
|
+
return sorted(cls._STANDARD_SIZES.keys())
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# Populate convenience constants for standard sizes.
|
|
99
|
+
PageSize.A4 = PageSize.from_name("A4")
|
|
100
|
+
PageSize.LETTER = PageSize.from_name("LETTER")
|
|
101
|
+
PageSize.LEGAL = PageSize.from_name("LEGAL")
|
|
102
|
+
PageSize.TABLOID = PageSize.from_name("TABLOID")
|
|
103
|
+
PageSize.A3 = PageSize.from_name("A3")
|
|
104
|
+
PageSize.A5 = PageSize.from_name("A5")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Orientation(Enum):
|
|
108
|
+
"""Page orientation options."""
|
|
109
|
+
PORTRAIT = "PORTRAIT"
|
|
110
|
+
LANDSCAPE = "LANDSCAPE"
|
|
9
111
|
|
|
10
112
|
|
|
11
113
|
class StandardFonts(Enum):
|
|
@@ -365,6 +467,19 @@ class MoveRequest:
|
|
|
365
467
|
}
|
|
366
468
|
|
|
367
469
|
|
|
470
|
+
@dataclass
|
|
471
|
+
class PageMoveRequest:
|
|
472
|
+
"""Request object for moving pages within the document."""
|
|
473
|
+
from_page_index: int
|
|
474
|
+
to_page_index: int
|
|
475
|
+
|
|
476
|
+
def to_dict(self) -> dict:
|
|
477
|
+
return {
|
|
478
|
+
"fromPageIndex": self.from_page_index,
|
|
479
|
+
"toPageIndex": self.to_page_index
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
|
|
368
483
|
@dataclass
|
|
369
484
|
class AddRequest:
|
|
370
485
|
"""Request object for add operations."""
|
|
@@ -547,3 +662,21 @@ class TextObjectRef(ObjectRef):
|
|
|
547
662
|
def get_children(self) -> List['TextObjectRef']:
|
|
548
663
|
"""Get the child text objects."""
|
|
549
664
|
return self.children
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
@dataclass
|
|
668
|
+
class PageRef(ObjectRef):
|
|
669
|
+
"""
|
|
670
|
+
Represents a page reference with additional page-specific properties.
|
|
671
|
+
Extends ObjectRef to include page size and orientation.
|
|
672
|
+
"""
|
|
673
|
+
page_size: Optional[PageSize]
|
|
674
|
+
orientation: Optional[Orientation]
|
|
675
|
+
|
|
676
|
+
def get_page_size(self) -> Optional[PageSize]:
|
|
677
|
+
"""Get the page size."""
|
|
678
|
+
return self.page_size
|
|
679
|
+
|
|
680
|
+
def get_orientation(self) -> Optional[Orientation]:
|
|
681
|
+
"""Get the page orientation."""
|
|
682
|
+
return self.orientation
|
pdfdancer/paragraph_builder.py
CHANGED
|
@@ -6,6 +6,7 @@ Closely mirrors the Java ParagraphBuilder class with Python conventions.
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Optional, Union
|
|
8
8
|
|
|
9
|
+
from . import StandardFonts
|
|
9
10
|
from .exceptions import ValidationException
|
|
10
11
|
from .models import Paragraph, Font, Color, Position
|
|
11
12
|
|
|
@@ -60,7 +61,7 @@ class ParagraphBuilder:
|
|
|
60
61
|
|
|
61
62
|
return self
|
|
62
63
|
|
|
63
|
-
def font(self, font_name: str, font_size: float) -> 'ParagraphBuilder':
|
|
64
|
+
def font(self, font_name: str | StandardFonts, font_size: float) -> 'ParagraphBuilder':
|
|
64
65
|
"""
|
|
65
66
|
Set the font for the paragraph using an existing Font object.
|
|
66
67
|
Equivalent to withFont(Font) in Java ParagraphBuilder.
|
|
@@ -75,6 +76,10 @@ class ParagraphBuilder:
|
|
|
75
76
|
Raises:
|
|
76
77
|
ValidationException: If font is None
|
|
77
78
|
"""
|
|
79
|
+
# If font_name is an enum member, use its value
|
|
80
|
+
if isinstance(font_name, StandardFonts):
|
|
81
|
+
font_name = font_name.value
|
|
82
|
+
|
|
78
83
|
font = Font(font_name, font_size)
|
|
79
84
|
if font is None:
|
|
80
85
|
raise ValidationException("Font cannot be null")
|
|
@@ -185,7 +190,7 @@ class ParagraphBuilder:
|
|
|
185
190
|
self._paragraph.set_position(position)
|
|
186
191
|
return self
|
|
187
192
|
|
|
188
|
-
def
|
|
193
|
+
def _build(self) -> Paragraph:
|
|
189
194
|
"""
|
|
190
195
|
Build and return the final Paragraph object.
|
|
191
196
|
Equivalent to build() in Java ParagraphBuilder.
|
|
@@ -267,7 +272,7 @@ class ParagraphBuilder:
|
|
|
267
272
|
return lines
|
|
268
273
|
|
|
269
274
|
def add(self):
|
|
270
|
-
self._client._add_paragraph(self.
|
|
275
|
+
self._client._add_paragraph(self._build())
|
|
271
276
|
|
|
272
277
|
|
|
273
278
|
class ParagraphPageBuilder(ParagraphBuilder):
|
pdfdancer/pdfdancer_v1.py
CHANGED
|
@@ -8,9 +8,12 @@ Provides session-based PDF manipulation operations with strict validation.
|
|
|
8
8
|
import json
|
|
9
9
|
import os
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import List, Optional, Union, BinaryIO
|
|
11
|
+
from typing import List, Optional, Union, BinaryIO, Mapping, Any
|
|
12
12
|
|
|
13
13
|
import requests
|
|
14
|
+
from dotenv import load_dotenv
|
|
15
|
+
|
|
16
|
+
load_dotenv()
|
|
14
17
|
|
|
15
18
|
from . import ParagraphBuilder
|
|
16
19
|
from .exceptions import (
|
|
@@ -22,21 +25,32 @@ from .exceptions import (
|
|
|
22
25
|
)
|
|
23
26
|
from .image_builder import ImageBuilder
|
|
24
27
|
from .models import (
|
|
25
|
-
ObjectRef, Position, ObjectType, Font, Image, Paragraph, FormFieldRef, TextObjectRef,
|
|
26
|
-
FindRequest, DeleteRequest, MoveRequest, AddRequest, ModifyRequest, ModifyTextRequest,
|
|
27
|
-
|
|
28
|
+
ObjectRef, Position, ObjectType, Font, Image, Paragraph, FormFieldRef, TextObjectRef, PageRef,
|
|
29
|
+
FindRequest, DeleteRequest, MoveRequest, PageMoveRequest, AddRequest, ModifyRequest, ModifyTextRequest,
|
|
30
|
+
ChangeFormFieldRequest,
|
|
31
|
+
ShapeType, PositionMode, PageSize, Orientation
|
|
28
32
|
)
|
|
29
33
|
from .paragraph_builder import ParagraphPageBuilder
|
|
30
34
|
from .types import PathObject, ParagraphObject, TextLineObject, ImageObject, FormObject, FormFieldObject
|
|
31
35
|
|
|
32
36
|
|
|
33
37
|
class PageClient:
|
|
34
|
-
def __init__(self, page_index: int, root: "PDFDancer"
|
|
38
|
+
def __init__(self, page_index: int, root: "PDFDancer", page_size: Optional[PageSize] = None,
|
|
39
|
+
orientation: Optional[Union[Orientation, str]] = Orientation.PORTRAIT):
|
|
35
40
|
self.page_index = page_index
|
|
36
41
|
self.root = root
|
|
37
42
|
self.object_type = ObjectType.PAGE
|
|
38
43
|
self.position = Position.at_page(page_index)
|
|
39
44
|
self.internal_id = f"PAGE-{page_index}"
|
|
45
|
+
self.page_size = page_size
|
|
46
|
+
if isinstance(orientation, str):
|
|
47
|
+
normalized = orientation.strip().upper()
|
|
48
|
+
try:
|
|
49
|
+
self.orientation = Orientation(normalized)
|
|
50
|
+
except ValueError:
|
|
51
|
+
self.orientation = normalized
|
|
52
|
+
else:
|
|
53
|
+
self.orientation = orientation
|
|
40
54
|
|
|
41
55
|
def select_paths_at(self, x: float, y: float) -> List[PathObject]:
|
|
42
56
|
# noinspection PyProtectedMember
|
|
@@ -121,20 +135,71 @@ class PageClient:
|
|
|
121
135
|
return self.root._to_form_field_objects(self.root._find_form_fields(position))
|
|
122
136
|
|
|
123
137
|
@classmethod
|
|
124
|
-
def from_ref(cls, root: 'PDFDancer',
|
|
125
|
-
page_client = PageClient(
|
|
138
|
+
def from_ref(cls, root: 'PDFDancer', page_ref: PageRef) -> 'PageClient':
|
|
139
|
+
page_client = PageClient(
|
|
140
|
+
page_index=page_ref.position.page_index,
|
|
141
|
+
root=root,
|
|
142
|
+
page_size=page_ref.page_size,
|
|
143
|
+
orientation=page_ref.orientation
|
|
144
|
+
)
|
|
145
|
+
page_client.internal_id = page_ref.internal_id
|
|
146
|
+
if page_ref.position is not None:
|
|
147
|
+
page_client.position = page_ref.position
|
|
148
|
+
page_client.page_index = page_ref.position.page_index
|
|
126
149
|
return page_client
|
|
127
150
|
|
|
128
151
|
def delete(self) -> bool:
|
|
129
152
|
# noinspection PyProtectedMember
|
|
130
153
|
return self.root._delete_page(self._ref())
|
|
131
154
|
|
|
155
|
+
def move_to(self, target_page_index: int) -> bool:
|
|
156
|
+
"""Move this page to a different index within the document."""
|
|
157
|
+
if target_page_index is None or target_page_index < 0:
|
|
158
|
+
raise ValidationException(f"Target page index must be >= 0, got {target_page_index}")
|
|
159
|
+
|
|
160
|
+
# noinspection PyProtectedMember
|
|
161
|
+
moved = self.root._move_page(self.page_index, target_page_index)
|
|
162
|
+
if moved:
|
|
163
|
+
self.page_index = target_page_index
|
|
164
|
+
self.position = Position.at_page(target_page_index)
|
|
165
|
+
return moved
|
|
166
|
+
|
|
132
167
|
def _ref(self):
|
|
133
168
|
return ObjectRef(internal_id=self.internal_id, position=self.position, type=self.object_type)
|
|
134
169
|
|
|
135
170
|
def new_paragraph(self):
|
|
136
171
|
return ParagraphPageBuilder(self.root, self.page_index)
|
|
137
172
|
|
|
173
|
+
def select_paths(self):
|
|
174
|
+
# noinspection PyProtectedMember
|
|
175
|
+
return self.root._to_path_objects(self.root._find_paths(Position.at_page(self.page_index)))
|
|
176
|
+
|
|
177
|
+
def select_elements(self):
|
|
178
|
+
"""
|
|
179
|
+
Select all elements (paragraphs, images, paths, forms) on this page.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
List of all PDF objects on this page
|
|
183
|
+
"""
|
|
184
|
+
result = []
|
|
185
|
+
result.extend(self.select_paragraphs())
|
|
186
|
+
result.extend(self.select_text_lines())
|
|
187
|
+
result.extend(self.select_images())
|
|
188
|
+
result.extend(self.select_paths())
|
|
189
|
+
result.extend(self.select_forms())
|
|
190
|
+
result.extend(self.select_form_fields())
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def size(self):
|
|
195
|
+
"""Property alias for page size."""
|
|
196
|
+
return self.page_size
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def page_orientation(self):
|
|
200
|
+
"""Property alias for orientation."""
|
|
201
|
+
return self.orientation
|
|
202
|
+
|
|
138
203
|
|
|
139
204
|
class PDFDancer:
|
|
140
205
|
"""
|
|
@@ -197,12 +262,57 @@ class PDFDancer:
|
|
|
197
262
|
def new(cls,
|
|
198
263
|
token: Optional[str] = None,
|
|
199
264
|
base_url: Optional[str] = None,
|
|
200
|
-
timeout: float = 30.0
|
|
265
|
+
timeout: float = 30.0,
|
|
266
|
+
page_size: Optional[Union[PageSize, str, Mapping[str, Any]]] = None,
|
|
267
|
+
orientation: Optional[Union[Orientation, str]] = None,
|
|
268
|
+
initial_page_count: int = 1) -> "PDFDancer":
|
|
269
|
+
"""
|
|
270
|
+
Create a new blank PDF document with optional configuration.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
token: Override for the API token; falls back to `PDFDANCER_TOKEN` environment variable.
|
|
274
|
+
base_url: Override for the API base URL; falls back to `PDFDANCER_BASE_URL`
|
|
275
|
+
or defaults to `https://api.pdfdancer.com`.
|
|
276
|
+
timeout: HTTP read timeout in seconds.
|
|
277
|
+
page_size: Page size for the PDF (default: A4). Accepts `PageSize`, a standard name string, or a
|
|
278
|
+
mapping with `width`/`height` values.
|
|
279
|
+
orientation: Page orientation (default: PORTRAIT). Can be Orientation enum or string.
|
|
280
|
+
initial_page_count: Number of initial blank pages (default: 1).
|
|
201
281
|
|
|
282
|
+
Returns:
|
|
283
|
+
A ready-to-use `PDFDancer` client instance with a blank PDF.
|
|
284
|
+
"""
|
|
202
285
|
resolved_token = cls._resolve_token(token)
|
|
203
286
|
resolved_base_url = cls._resolve_base_url(base_url)
|
|
204
287
|
|
|
205
|
-
|
|
288
|
+
# Create a new instance that will call _create_blank_pdf_session
|
|
289
|
+
instance = object.__new__(cls)
|
|
290
|
+
|
|
291
|
+
# Initialize instance variables
|
|
292
|
+
if not resolved_token or not resolved_token.strip():
|
|
293
|
+
raise ValidationException("Authentication token cannot be null or empty")
|
|
294
|
+
|
|
295
|
+
instance._token = resolved_token.strip()
|
|
296
|
+
instance._base_url = resolved_base_url.rstrip('/')
|
|
297
|
+
instance._read_timeout = timeout
|
|
298
|
+
|
|
299
|
+
# Create HTTP session for connection reuse
|
|
300
|
+
instance._session = requests.Session()
|
|
301
|
+
instance._session.headers.update({
|
|
302
|
+
'Authorization': f'Bearer {instance._token}'
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
# Create blank PDF session
|
|
306
|
+
instance._session_id = instance._create_blank_pdf_session(
|
|
307
|
+
page_size=page_size,
|
|
308
|
+
orientation=orientation,
|
|
309
|
+
initial_page_count=initial_page_count
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Set pdf_bytes to None since we don't have the PDF bytes yet
|
|
313
|
+
instance._pdf_bytes = None
|
|
314
|
+
|
|
315
|
+
return instance
|
|
206
316
|
|
|
207
317
|
def __init__(self, token: str, pdf_data: Union[bytes, Path, str, BinaryIO],
|
|
208
318
|
base_url: str, read_timeout: float = 0):
|
|
@@ -335,6 +445,22 @@ class PDFDancer:
|
|
|
335
445
|
f"Server response: {details}"
|
|
336
446
|
)
|
|
337
447
|
|
|
448
|
+
@staticmethod
|
|
449
|
+
def _cleanup_url_path(base_url: str, path: str) -> str:
|
|
450
|
+
"""
|
|
451
|
+
Combine base_url and path, ensuring no double slashes.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
base_url: Base URL (may or may not have trailing slash)
|
|
455
|
+
path: Path segment (may or may not have leading slash)
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
Combined URL with no double slashes
|
|
459
|
+
"""
|
|
460
|
+
base = base_url.rstrip('/')
|
|
461
|
+
path = path.lstrip('/')
|
|
462
|
+
return f"{base}/{path}"
|
|
463
|
+
|
|
338
464
|
def _create_session(self) -> str:
|
|
339
465
|
"""
|
|
340
466
|
Creates a new PDF processing session by uploading the PDF data.
|
|
@@ -345,7 +471,7 @@ class PDFDancer:
|
|
|
345
471
|
}
|
|
346
472
|
|
|
347
473
|
response = self._session.post(
|
|
348
|
-
|
|
474
|
+
self._cleanup_url_path(self._base_url, "/session/create"),
|
|
349
475
|
files=files,
|
|
350
476
|
timeout=self._read_timeout if self._read_timeout > 0 else None
|
|
351
477
|
)
|
|
@@ -365,6 +491,76 @@ class PDFDancer:
|
|
|
365
491
|
raise HttpClientException(f"Failed to create session: {error_message}",
|
|
366
492
|
response=getattr(e, 'response', None), cause=e) from None
|
|
367
493
|
|
|
494
|
+
def _create_blank_pdf_session(self,
|
|
495
|
+
page_size: Optional[Union[PageSize, str, Mapping[str, Any]]] = None,
|
|
496
|
+
orientation: Optional[Union[Orientation, str]] = None,
|
|
497
|
+
initial_page_count: int = 1) -> str:
|
|
498
|
+
"""
|
|
499
|
+
Creates a new PDF processing session with a blank PDF document.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
page_size: Page size (default: A4). Accepts `PageSize`, a standard name string, or a
|
|
503
|
+
mapping with `width`/`height` values.
|
|
504
|
+
orientation: Page orientation (default: PORTRAIT). Can be Orientation enum or string.
|
|
505
|
+
initial_page_count: Number of initial pages (default: 1)
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
Session ID for the newly created blank PDF
|
|
509
|
+
|
|
510
|
+
Raises:
|
|
511
|
+
SessionException: If session creation fails
|
|
512
|
+
HttpClientException: If HTTP communication fails
|
|
513
|
+
"""
|
|
514
|
+
try:
|
|
515
|
+
# Build request payload
|
|
516
|
+
request_data = {}
|
|
517
|
+
|
|
518
|
+
# Handle page_size - convert to type-safe object with dimensions
|
|
519
|
+
if page_size is not None:
|
|
520
|
+
try:
|
|
521
|
+
request_data['pageSize'] = PageSize.coerce(page_size).to_dict()
|
|
522
|
+
except ValueError as exc:
|
|
523
|
+
raise ValidationException(str(exc)) from exc
|
|
524
|
+
except TypeError:
|
|
525
|
+
raise ValidationException(f"Invalid page_size type: {type(page_size)}")
|
|
526
|
+
|
|
527
|
+
# Handle orientation
|
|
528
|
+
if orientation is not None:
|
|
529
|
+
if isinstance(orientation, Orientation):
|
|
530
|
+
request_data['orientation'] = orientation.value
|
|
531
|
+
elif isinstance(orientation, str):
|
|
532
|
+
request_data['orientation'] = orientation
|
|
533
|
+
else:
|
|
534
|
+
raise ValidationException(f"Invalid orientation type: {type(orientation)}")
|
|
535
|
+
|
|
536
|
+
# Handle initial_page_count with validation
|
|
537
|
+
if initial_page_count < 1:
|
|
538
|
+
raise ValidationException(f"Initial page count must be at least 1, got {initial_page_count}")
|
|
539
|
+
request_data['initialPageCount'] = initial_page_count
|
|
540
|
+
|
|
541
|
+
headers = {'Content-Type': 'application/json'}
|
|
542
|
+
response = self._session.post(
|
|
543
|
+
self._cleanup_url_path(self._base_url, "/session/new"),
|
|
544
|
+
json=request_data,
|
|
545
|
+
headers=headers,
|
|
546
|
+
timeout=self._read_timeout if self._read_timeout > 0 else None
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
self._handle_authentication_error(response)
|
|
550
|
+
response.raise_for_status()
|
|
551
|
+
session_id = response.text.strip()
|
|
552
|
+
|
|
553
|
+
if not session_id:
|
|
554
|
+
raise SessionException("Server returned empty session ID")
|
|
555
|
+
|
|
556
|
+
return session_id
|
|
557
|
+
|
|
558
|
+
except requests.exceptions.RequestException as e:
|
|
559
|
+
self._handle_authentication_error(getattr(e, 'response', None))
|
|
560
|
+
error_message = self._extract_error_message(getattr(e, 'response', None))
|
|
561
|
+
raise HttpClientException(f"Failed to create blank PDF session: {error_message}",
|
|
562
|
+
response=getattr(e, 'response', None), cause=e) from None
|
|
563
|
+
|
|
368
564
|
def _make_request(self, method: str, path: str, data: Optional[dict] = None,
|
|
369
565
|
params: Optional[dict] = None) -> requests.Response:
|
|
370
566
|
"""
|
|
@@ -378,7 +574,7 @@ class PDFDancer:
|
|
|
378
574
|
try:
|
|
379
575
|
response = self._session.request(
|
|
380
576
|
method=method,
|
|
381
|
-
url=
|
|
577
|
+
url=self._cleanup_url_path(self._base_url, path),
|
|
382
578
|
json=data,
|
|
383
579
|
params=params,
|
|
384
580
|
headers=headers,
|
|
@@ -528,22 +724,36 @@ class PDFDancer:
|
|
|
528
724
|
return self._to_textline_objects(self._find_text_lines(None))
|
|
529
725
|
|
|
530
726
|
def page(self, page_index: int) -> PageClient:
|
|
531
|
-
|
|
727
|
+
"""
|
|
728
|
+
Get a specific page by index, fetching page properties from the server.
|
|
729
|
+
|
|
730
|
+
Args:
|
|
731
|
+
page_index: The 0-based page index
|
|
732
|
+
|
|
733
|
+
Returns:
|
|
734
|
+
PageClient with page properties populated
|
|
735
|
+
"""
|
|
736
|
+
page_ref = self._get_page(page_index)
|
|
737
|
+
if page_ref:
|
|
738
|
+
return PageClient.from_ref(self, page_ref)
|
|
739
|
+
else:
|
|
740
|
+
# Fallback to basic PageClient if page not found
|
|
741
|
+
return PageClient(page_index, self)
|
|
532
742
|
|
|
533
743
|
# Page Operations
|
|
534
744
|
|
|
535
745
|
def pages(self) -> List[PageClient]:
|
|
536
746
|
return self._to_page_objects(self._get_pages())
|
|
537
747
|
|
|
538
|
-
def _get_pages(self) -> List[
|
|
748
|
+
def _get_pages(self) -> List[PageRef]:
|
|
539
749
|
"""
|
|
540
750
|
Retrieves references to all pages in the PDF document.
|
|
541
751
|
"""
|
|
542
752
|
response = self._make_request('POST', '/pdf/page/find')
|
|
543
753
|
pages_data = response.json()
|
|
544
|
-
return [self.
|
|
754
|
+
return [self._parse_page_ref(page_data) for page_data in pages_data]
|
|
545
755
|
|
|
546
|
-
def _get_page(self, page_index: int) -> Optional[
|
|
756
|
+
def _get_page(self, page_index: int) -> Optional[PageRef]:
|
|
547
757
|
"""
|
|
548
758
|
Retrieves a reference to a specific page by its page index.
|
|
549
759
|
|
|
@@ -551,7 +761,7 @@ class PDFDancer:
|
|
|
551
761
|
page_index: The page index to retrieve (1-based indexing)
|
|
552
762
|
|
|
553
763
|
Returns:
|
|
554
|
-
|
|
764
|
+
Page reference for the specified page, or None if not found
|
|
555
765
|
"""
|
|
556
766
|
if page_index < 0:
|
|
557
767
|
raise ValidationException(f"Page index must be >= 0, got {page_index}")
|
|
@@ -563,7 +773,7 @@ class PDFDancer:
|
|
|
563
773
|
if not pages_data:
|
|
564
774
|
return None
|
|
565
775
|
|
|
566
|
-
return self.
|
|
776
|
+
return self._parse_page_ref(pages_data[0])
|
|
567
777
|
|
|
568
778
|
def _delete_page(self, page_ref: ObjectRef) -> bool:
|
|
569
779
|
"""
|
|
@@ -583,6 +793,25 @@ class PDFDancer:
|
|
|
583
793
|
response = self._make_request('DELETE', '/pdf/page/delete', data=request_data)
|
|
584
794
|
return response.json()
|
|
585
795
|
|
|
796
|
+
def move_page(self, from_page_index: int, to_page_index: int) -> bool:
|
|
797
|
+
"""Move a page to a different index within the document."""
|
|
798
|
+
return self._move_page(from_page_index, to_page_index)
|
|
799
|
+
|
|
800
|
+
def _move_page(self, from_page_index: int, to_page_index: int) -> bool:
|
|
801
|
+
"""Internal helper to perform the page move operation."""
|
|
802
|
+
for value, label in ((from_page_index, "from_page_index"), (to_page_index, "to_page_index")):
|
|
803
|
+
if value is None:
|
|
804
|
+
raise ValidationException(f"{label} cannot be null")
|
|
805
|
+
if not isinstance(value, int):
|
|
806
|
+
raise ValidationException(f"{label} must be an integer, got {type(value)}")
|
|
807
|
+
if value < 0:
|
|
808
|
+
raise ValidationException(f"{label} must be >= 0, got {value}")
|
|
809
|
+
|
|
810
|
+
request_data = PageMoveRequest(from_page_index, to_page_index).to_dict()
|
|
811
|
+
response = self._make_request('PUT', '/pdf/page/move', data=request_data)
|
|
812
|
+
result = response.json()
|
|
813
|
+
return bool(result)
|
|
814
|
+
|
|
586
815
|
# Manipulation Operations
|
|
587
816
|
|
|
588
817
|
def _delete(self, object_ref: ObjectRef) -> bool:
|
|
@@ -678,6 +907,10 @@ class PDFDancer:
|
|
|
678
907
|
def new_paragraph(self) -> ParagraphBuilder:
|
|
679
908
|
return ParagraphBuilder(self)
|
|
680
909
|
|
|
910
|
+
def new_page(self):
|
|
911
|
+
response = self._make_request('POST', '/pdf/page/add', data=None)
|
|
912
|
+
return self._parse_page_ref(response.json())
|
|
913
|
+
|
|
681
914
|
def new_image(self) -> ImageBuilder:
|
|
682
915
|
return ImageBuilder(self)
|
|
683
916
|
|
|
@@ -809,7 +1042,7 @@ class PDFDancer:
|
|
|
809
1042
|
|
|
810
1043
|
headers = {'X-Session-Id': self._session_id}
|
|
811
1044
|
response = self._session.post(
|
|
812
|
-
|
|
1045
|
+
self._cleanup_url_path(self._base_url, "/font/register"),
|
|
813
1046
|
files=files,
|
|
814
1047
|
headers=headers,
|
|
815
1048
|
timeout=30
|
|
@@ -957,6 +1190,42 @@ class PDFDancer:
|
|
|
957
1190
|
|
|
958
1191
|
return text_object
|
|
959
1192
|
|
|
1193
|
+
def _parse_page_ref(self, obj_data: dict) -> PageRef:
|
|
1194
|
+
"""Parse JSON object data into PageRef instance with page-specific properties."""
|
|
1195
|
+
position_data = obj_data.get('position', {})
|
|
1196
|
+
position = self._parse_position(position_data) if position_data else None
|
|
1197
|
+
|
|
1198
|
+
object_type = ObjectType(obj_data['type'])
|
|
1199
|
+
|
|
1200
|
+
# Parse page size if present
|
|
1201
|
+
page_size = None
|
|
1202
|
+
if 'pageSize' in obj_data and isinstance(obj_data['pageSize'], dict):
|
|
1203
|
+
page_size_data = obj_data['pageSize']
|
|
1204
|
+
try:
|
|
1205
|
+
page_size = PageSize.from_dict(page_size_data)
|
|
1206
|
+
except ValueError:
|
|
1207
|
+
page_size = None
|
|
1208
|
+
|
|
1209
|
+
# Parse orientation if present
|
|
1210
|
+
orientation_value = obj_data.get('orientation')
|
|
1211
|
+
orientation = None
|
|
1212
|
+
if isinstance(orientation_value, str):
|
|
1213
|
+
normalized = orientation_value.strip().upper()
|
|
1214
|
+
try:
|
|
1215
|
+
orientation = Orientation(normalized)
|
|
1216
|
+
except ValueError:
|
|
1217
|
+
orientation = None
|
|
1218
|
+
elif isinstance(orientation_value, Orientation):
|
|
1219
|
+
orientation = orientation_value
|
|
1220
|
+
|
|
1221
|
+
return PageRef(
|
|
1222
|
+
internal_id=obj_data.get('internalId'),
|
|
1223
|
+
position=position,
|
|
1224
|
+
type=object_type,
|
|
1225
|
+
page_size=page_size,
|
|
1226
|
+
orientation=orientation
|
|
1227
|
+
)
|
|
1228
|
+
|
|
960
1229
|
# Builder Pattern Support
|
|
961
1230
|
|
|
962
1231
|
def _paragraph_builder(self) -> 'ParagraphBuilder':
|
|
@@ -997,8 +1266,59 @@ class PDFDancer:
|
|
|
997
1266
|
return [FormFieldObject(self, ref.internal_id, ref.type, ref.position, ref.name, ref.value) for ref in
|
|
998
1267
|
refs]
|
|
999
1268
|
|
|
1000
|
-
def _to_page_objects(self, refs: List[
|
|
1269
|
+
def _to_page_objects(self, refs: List[PageRef]) -> List[PageClient]:
|
|
1001
1270
|
return [PageClient.from_ref(self, ref) for ref in refs]
|
|
1002
1271
|
|
|
1003
|
-
def _to_page_object(self, ref:
|
|
1272
|
+
def _to_page_object(self, ref: PageRef) -> PageClient:
|
|
1004
1273
|
return PageClient.from_ref(self, ref)
|
|
1274
|
+
|
|
1275
|
+
def _to_mixed_objects(self, refs: List[ObjectRef]) -> List:
|
|
1276
|
+
"""
|
|
1277
|
+
Convert a list of ObjectRefs to their appropriate object types.
|
|
1278
|
+
Handles mixed object types by checking the type of each ref.
|
|
1279
|
+
"""
|
|
1280
|
+
result = []
|
|
1281
|
+
for ref in refs:
|
|
1282
|
+
if ref.type == ObjectType.PARAGRAPH:
|
|
1283
|
+
# Need to convert to TextObjectRef first
|
|
1284
|
+
if isinstance(ref, TextObjectRef):
|
|
1285
|
+
result.append(ParagraphObject(self, ref))
|
|
1286
|
+
else:
|
|
1287
|
+
# Re-fetch with proper type
|
|
1288
|
+
text_refs = self._find_paragraphs(ref.position)
|
|
1289
|
+
result.extend(self._to_paragraph_objects(text_refs))
|
|
1290
|
+
elif ref.type == ObjectType.TEXT_LINE:
|
|
1291
|
+
if isinstance(ref, TextObjectRef):
|
|
1292
|
+
result.append(TextLineObject(self, ref))
|
|
1293
|
+
else:
|
|
1294
|
+
text_refs = self._find_text_lines(ref.position)
|
|
1295
|
+
result.extend(self._to_textline_objects(text_refs))
|
|
1296
|
+
elif ref.type == ObjectType.IMAGE:
|
|
1297
|
+
result.append(ImageObject(self, ref.internal_id, ref.type, ref.position))
|
|
1298
|
+
elif ref.type == ObjectType.PATH:
|
|
1299
|
+
result.append(PathObject(self, ref.internal_id, ref.type, ref.position))
|
|
1300
|
+
elif ref.type == ObjectType.FORM_X_OBJECT:
|
|
1301
|
+
result.append(FormObject(self, ref.internal_id, ref.type, ref.position))
|
|
1302
|
+
elif ref.type == ObjectType.FORM_FIELD:
|
|
1303
|
+
if isinstance(ref, FormFieldRef):
|
|
1304
|
+
result.append(FormFieldObject(self, ref.internal_id, ref.type, ref.position, ref.name, ref.value))
|
|
1305
|
+
else:
|
|
1306
|
+
form_refs = self._find_form_fields(ref.position)
|
|
1307
|
+
result.extend(self._to_form_field_objects(form_refs))
|
|
1308
|
+
return result
|
|
1309
|
+
|
|
1310
|
+
def select_elements(self):
|
|
1311
|
+
"""
|
|
1312
|
+
Select all elements (paragraphs, images, paths, forms) in the document.
|
|
1313
|
+
|
|
1314
|
+
Returns:
|
|
1315
|
+
List of all PDF objects in the document
|
|
1316
|
+
"""
|
|
1317
|
+
result = []
|
|
1318
|
+
result.extend(self.select_paragraphs())
|
|
1319
|
+
result.extend(self.select_text_lines())
|
|
1320
|
+
result.extend(self.select_images())
|
|
1321
|
+
result.extend(self.select_paths())
|
|
1322
|
+
result.extend(self.select_forms())
|
|
1323
|
+
result.extend(self.select_form_fields())
|
|
1324
|
+
return result
|
{pdfdancer_client_python-0.2.12.dist-info → pdfdancer_client_python-0.2.14.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pdfdancer-client-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.14
|
|
4
4
|
Summary: Python client for PDFDancer API
|
|
5
5
|
Author-email: "The Famous Cat Ltd." <hi@thefamouscat.com>
|
|
6
6
|
License: MIT
|
|
@@ -9,28 +9,39 @@ Project-URL: Repository, https://github.com/MenschMachine/pdfdancer-client-pytho
|
|
|
9
9
|
Classifier: Development Status :: 4 - Beta
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Python: >=3.10
|
|
16
17
|
Description-Content-Type: text/markdown
|
|
17
18
|
Requires-Dist: requests>=2.25.0
|
|
18
19
|
Requires-Dist: pydantic>=1.8.0
|
|
19
20
|
Requires-Dist: typing-extensions>=4.0.0
|
|
21
|
+
Requires-Dist: python-dotenv>=0.19.0
|
|
20
22
|
Provides-Extra: dev
|
|
21
23
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
22
24
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-mock>=3.10.0; extra == "dev"
|
|
23
26
|
Requires-Dist: black>=22.0; extra == "dev"
|
|
24
27
|
Requires-Dist: flake8>=5.0; extra == "dev"
|
|
25
28
|
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
29
|
+
Requires-Dist: isort>=5.10.0; extra == "dev"
|
|
30
|
+
Requires-Dist: build>=0.8.0; extra == "dev"
|
|
31
|
+
Requires-Dist: twine>=4.0.0; extra == "dev"
|
|
26
32
|
|
|
27
33
|
# PDFDancer Python Client
|
|
28
34
|
|
|
29
|
-
Automate PDF clean-up, redaction, form filling, and content injection against the PDFDancer API from Python. The client
|
|
35
|
+
Automate PDF clean-up, redaction, form filling, and content injection against the PDFDancer API from Python. The client
|
|
36
|
+
gives you page-scoped selectors, fluent editors, and builders so you can read, modify, and export PDFs programmatically
|
|
37
|
+
in just a few lines.
|
|
38
|
+
|
|
39
|
+
Latest schema version available at https://bucket.pdfdancer.com/api-doc/development-0.0.yml.
|
|
30
40
|
|
|
31
41
|
## Highlights
|
|
32
42
|
|
|
33
|
-
- Locate anything inside a PDF—paragraphs, text lines, images, vector paths, pages, AcroForm fields—by page,
|
|
43
|
+
- Locate anything inside a PDF—paragraphs, text lines, images, vector paths, pages, AcroForm fields—by page,
|
|
44
|
+
coordinates, or text prefixes
|
|
34
45
|
- Edit or delete existing content with fluent paragraph/text editors and safe apply-on-exit context managers
|
|
35
46
|
- Fill or update form fields and propagate the changes back to the document instantly
|
|
36
47
|
- Add brand-new content with paragraph/image builders, custom fonts, and precise page positioning
|
|
@@ -47,7 +58,7 @@ Automate PDF clean-up, redaction, form filling, and content injection against th
|
|
|
47
58
|
|
|
48
59
|
## Requirements
|
|
49
60
|
|
|
50
|
-
- Python 3.
|
|
61
|
+
- Python 3.10 or newer
|
|
51
62
|
- A PDFDancer API token (set `PDFDANCER_TOKEN` or pass `token=...`)
|
|
52
63
|
- Network access to a PDFDancer service (defaults to `https://api.pdfdancer.com`; override with `PDFDANCER_BASE_URL`)
|
|
53
64
|
|
|
@@ -67,21 +78,21 @@ from pathlib import Path
|
|
|
67
78
|
from pdfdancer import Color, PDFDancer
|
|
68
79
|
|
|
69
80
|
with PDFDancer.open(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
81
|
+
pdf_data=Path("input.pdf"),
|
|
82
|
+
token="your-api-token", # optional when PDFDANCER_TOKEN is set
|
|
83
|
+
base_url="https://api.pdfdancer.com",
|
|
73
84
|
) as pdf:
|
|
74
85
|
# Locate existing content
|
|
75
86
|
heading = pdf.page(0).select_paragraphs_starting_with("Executive Summary")[0]
|
|
76
87
|
heading.edit().replace("Overview").apply()
|
|
77
88
|
|
|
78
89
|
# Add a new paragraph using the fluent builder
|
|
79
|
-
pdf.new_paragraph()
|
|
80
|
-
.text("Generated with PDFDancer")
|
|
81
|
-
.font("Helvetica", 12)
|
|
82
|
-
.color(Color(70, 70, 70))
|
|
83
|
-
.line_spacing(1.4)
|
|
84
|
-
.at(page_index=0, x=72, y=520)
|
|
90
|
+
pdf.new_paragraph()
|
|
91
|
+
.text("Generated with PDFDancer")
|
|
92
|
+
.font("Helvetica", 12)
|
|
93
|
+
.color(Color(70, 70, 70))
|
|
94
|
+
.line_spacing(1.4)
|
|
95
|
+
.at(page_index=0, x=72, y=520)
|
|
85
96
|
.add()
|
|
86
97
|
|
|
87
98
|
# Persist the modified document
|
|
@@ -107,7 +118,8 @@ with PDFDancer.open("report.pdf") as pdf: # environment variables provide token
|
|
|
107
118
|
print(page.internal_id, page.position.bounding_rect)
|
|
108
119
|
```
|
|
109
120
|
|
|
110
|
-
Selectors return rich objects (`ParagraphObject`, `TextLineObject`, `ImageObject`, `FormFieldObject`, etc.) with helpers
|
|
121
|
+
Selectors return rich objects (`ParagraphObject`, `TextLineObject`, `ImageObject`, `FormFieldObject`, etc.) with helpers
|
|
122
|
+
such as `delete()`, `move_to(x, y)`, or `edit()` depending on the object type.
|
|
111
123
|
|
|
112
124
|
## Editing Text and Forms
|
|
113
125
|
|
|
@@ -116,11 +128,11 @@ with PDFDancer.open("report.pdf") as pdf:
|
|
|
116
128
|
paragraph = pdf.page(0).select_paragraphs_starting_with("Disclaimer")[0]
|
|
117
129
|
|
|
118
130
|
# Chain updates explicitly…
|
|
119
|
-
paragraph.edit()
|
|
120
|
-
.replace("Updated disclaimer text")
|
|
121
|
-
.font("Roboto-Regular", 11)
|
|
122
|
-
.line_spacing(1.1)
|
|
123
|
-
.move_to(72, 140)
|
|
131
|
+
paragraph.edit()
|
|
132
|
+
.replace("Updated disclaimer text")
|
|
133
|
+
.font("Roboto-Regular", 11)
|
|
134
|
+
.line_spacing(1.1)
|
|
135
|
+
.move_to(72, 140)
|
|
124
136
|
.apply()
|
|
125
137
|
|
|
126
138
|
# …or use the context manager to auto-apply on success
|
|
@@ -141,16 +153,16 @@ with PDFDancer.open("report.pdf") as pdf:
|
|
|
141
153
|
pdf.register_font("/path/to/custom.ttf")
|
|
142
154
|
|
|
143
155
|
# Paragraphs
|
|
144
|
-
pdf.new_paragraph()
|
|
145
|
-
.text("Greetings from PDFDancer!")
|
|
146
|
-
.font(fonts[0].name, fonts[0].size)
|
|
147
|
-
.at(page_index=0, x=220, y=480)
|
|
156
|
+
pdf.new_paragraph()
|
|
157
|
+
.text("Greetings from PDFDancer!")
|
|
158
|
+
.font(fonts[0].name, fonts[0].size)
|
|
159
|
+
.at(page_index=0, x=220, y=480)
|
|
148
160
|
.add()
|
|
149
161
|
|
|
150
162
|
# Raster images
|
|
151
|
-
pdf.new_image()
|
|
152
|
-
.from_file(Path("logo.png"))
|
|
153
|
-
.at(page=0, x=48, y=700)
|
|
163
|
+
pdf.new_image()
|
|
164
|
+
.from_file(Path("logo.png"))
|
|
165
|
+
.at(page=0, x=48, y=700)
|
|
154
166
|
.add()
|
|
155
167
|
```
|
|
156
168
|
|
|
@@ -175,8 +187,7 @@ Wrap complex workflows in `try/except` blocks to surface actionable errors to yo
|
|
|
175
187
|
```bash
|
|
176
188
|
python -m venv venv
|
|
177
189
|
source venv/bin/activate # Windows: venv\Scripts\activate
|
|
178
|
-
pip install -e .
|
|
179
|
-
pip install -r requirements-dev.txt
|
|
190
|
+
pip install -e ".[dev]"
|
|
180
191
|
|
|
181
192
|
pytest -q # run the fast unit suite
|
|
182
193
|
pytest tests/e2e # integration tests (requires live API + fixtures)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
pdfdancer/__init__.py,sha256=STOBUkVrBG7SbgoT6wM6tfwBVbjUiQ9JTpmznJwBF94,1158
|
|
2
|
+
pdfdancer/exceptions.py,sha256=Y5zwNVZprsv2hvKX304cXWobJt11nrEhCzLklu2wiO8,1567
|
|
3
|
+
pdfdancer/image_builder.py,sha256=Omxc2LcieJ1MbvWBXR5_sfia--eAucTUe0KWgr22HYo,842
|
|
4
|
+
pdfdancer/models.py,sha256=yhatfgMWxYareL7J20Wz_6-V7oCzrqX35oZdNJ8UFJM,22984
|
|
5
|
+
pdfdancer/paragraph_builder.py,sha256=pgFTkyhYrx4VQDKy4Vhp-042OMlJOD8D0MW9flkvC7Y,9410
|
|
6
|
+
pdfdancer/pdfdancer_v1.py,sha256=uHcn-Thh5dQA8FnC-YojwkfzdGokYeQEUyiz41lut2c,53296
|
|
7
|
+
pdfdancer/types.py,sha256=SOmYP49XPVy6DZ4JXSJrfy0Aww-Tv7QjZCDnOB8VTT4,11860
|
|
8
|
+
pdfdancer_client_python-0.2.14.dist-info/METADATA,sha256=Pqhnjgfz_SoLD0zOJNAGg-otbszgdc4HG39v1iRGgf4,7060
|
|
9
|
+
pdfdancer_client_python-0.2.14.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
10
|
+
pdfdancer_client_python-0.2.14.dist-info/top_level.txt,sha256=ICwSVRpcCKrdBF9QlaX9Y0e_N3Nk1p7QVxadGOnbxeY,10
|
|
11
|
+
pdfdancer_client_python-0.2.14.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
pdfdancer/__init__.py,sha256=71HwLjHHPsCQMTUtLHYAwzslhF3PqN5g1QwMr4HbKSQ,1076
|
|
2
|
-
pdfdancer/exceptions.py,sha256=Y5zwNVZprsv2hvKX304cXWobJt11nrEhCzLklu2wiO8,1567
|
|
3
|
-
pdfdancer/image_builder.py,sha256=Omxc2LcieJ1MbvWBXR5_sfia--eAucTUe0KWgr22HYo,842
|
|
4
|
-
pdfdancer/models.py,sha256=ZoB5ZP1jaZsubqzhMr9W9nsIUirVUty_FkRiPZWq8vY,18276
|
|
5
|
-
pdfdancer/paragraph_builder.py,sha256=mjV36-XOqcYATfIjSOy7_SBO0EKXjsAtMqYL8IaowGU,9218
|
|
6
|
-
pdfdancer/pdfdancer_v1.py,sha256=XgcyKHPOBMI5vNM86ZRhRvJFA3wB7DTcWwDt6tHQxpI,39851
|
|
7
|
-
pdfdancer/types.py,sha256=SOmYP49XPVy6DZ4JXSJrfy0Aww-Tv7QjZCDnOB8VTT4,11860
|
|
8
|
-
pdfdancer_client_python-0.2.12.dist-info/METADATA,sha256=XHEG0LuL-bi7MQyYVUX2RWrq6mutyVlLcYSlqiUzMAg,6770
|
|
9
|
-
pdfdancer_client_python-0.2.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
10
|
-
pdfdancer_client_python-0.2.12.dist-info/top_level.txt,sha256=ICwSVRpcCKrdBF9QlaX9Y0e_N3Nk1p7QVxadGOnbxeY,10
|
|
11
|
-
pdfdancer_client_python-0.2.12.dist-info/RECORD,,
|
|
File without changes
|
{pdfdancer_client_python-0.2.12.dist-info → pdfdancer_client_python-0.2.14.dist-info}/top_level.txt
RENAMED
|
File without changes
|