pdfdancer-client-python 0.2.14__tar.gz → 0.2.15__tar.gz

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.
Files changed (48) hide show
  1. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/PKG-INFO +1 -1
  2. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/pyproject.toml +1 -1
  3. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/src/pdfdancer/__init__.py +5 -1
  4. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/src/pdfdancer/models.py +87 -14
  5. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/src/pdfdancer/pdfdancer_v1.py +33 -9
  6. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/src/pdfdancer/types.py +18 -5
  7. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/src/pdfdancer_client_python.egg-info/PKG-INFO +1 -1
  8. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/e2e/pdf_assertions.py +0 -2
  9. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/e2e/test_form_x_objects.py +0 -2
  10. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/e2e/test_line.py +56 -6
  11. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/e2e/test_new_pdf.py +25 -3
  12. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/e2e/test_paragraph.py +98 -12
  13. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/.claude/commands/discuss.md +0 -0
  14. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/.github/workflows/ci.yml +0 -0
  15. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/.gitignore +0 -0
  16. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/CLAUDE.md +0 -0
  17. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/README.md +0 -0
  18. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/docs/openapi.yml +0 -0
  19. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/release.py +0 -0
  20. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/setup.cfg +0 -0
  21. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/src/pdfdancer/exceptions.py +0 -0
  22. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/src/pdfdancer/image_builder.py +0 -0
  23. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/src/pdfdancer/paragraph_builder.py +0 -0
  24. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/src/pdfdancer_client_python.egg-info/SOURCES.txt +0 -0
  25. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/src/pdfdancer_client_python.egg-info/dependency_links.txt +0 -0
  26. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/src/pdfdancer_client_python.egg-info/requires.txt +0 -0
  27. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/src/pdfdancer_client_python.egg-info/top_level.txt +0 -0
  28. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/__init__.py +0 -0
  29. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/conftest.py +0 -0
  30. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/e2e/__init__.py +0 -0
  31. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/e2e/test_acroform.py +0 -0
  32. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/e2e/test_image.py +0 -0
  33. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/e2e/test_page.py +0 -0
  34. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/e2e/test_path.py +0 -0
  35. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/e2e/test_pdfdancer.py +0 -0
  36. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/e2e/test_positioning.py +0 -0
  37. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/fixtures/DancingScript-Regular.ttf +0 -0
  38. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/fixtures/Empty.pdf +0 -0
  39. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/fixtures/JetBrainsMono-Regular.ttf +0 -0
  40. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/fixtures/ObviouslyAwesome.pdf +0 -0
  41. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/fixtures/basic-paths.pdf +0 -0
  42. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/fixtures/form-xobject-example.pdf +0 -0
  43. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/fixtures/logo-80.png +0 -0
  44. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/fixtures/mixed-form-types.pdf +0 -0
  45. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/test_models.py +0 -0
  46. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/test_openapi_compliance.py +0 -0
  47. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/test_pdf_object_equality.py +0 -0
  48. {pdfdancer_client_python-0.2.14 → pdfdancer_client_python-0.2.15}/tests/test_standard_fonts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdfdancer-client-python
3
- Version: 0.2.14
3
+ Version: 0.2.15
4
4
  Summary: Python client for PDFDancer API
5
5
  Author-email: "The Famous Cat Ltd." <hi@thefamouscat.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pdfdancer-client-python"
7
- version = "0.2.14"
7
+ version = "0.2.15"
8
8
  description = "Python client for PDFDancer API"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -12,7 +12,8 @@ from .exceptions import (
12
12
  )
13
13
  from .models import (
14
14
  ObjectRef, Position, ObjectType, Font, Color, Image, BoundingRect, Paragraph, FormFieldRef, TextObjectRef,
15
- PageRef, PositionMode, ShapeType, Point, StandardFonts, PageSize, Orientation
15
+ PageRef, PositionMode, ShapeType, Point, StandardFonts, PageSize, Orientation, TextStatus, FontRecommendation,
16
+ FontType
16
17
  )
17
18
  from .paragraph_builder import ParagraphBuilder
18
19
 
@@ -37,6 +38,9 @@ __all__ = [
37
38
  "StandardFonts",
38
39
  "PageSize",
39
40
  "Orientation",
41
+ "TextStatus",
42
+ "FontRecommendation",
43
+ "FontType",
40
44
  "PdfDancerException",
41
45
  "FontNotFoundException",
42
46
  "ValidationException",
@@ -154,7 +154,6 @@ class StandardFonts(Enum):
154
154
 
155
155
 
156
156
  class ObjectType(Enum):
157
- """Object type enumeration matching the Java ObjectType."""
158
157
  FORM_FIELD = "FORM_FIELD"
159
158
  IMAGE = "IMAGE"
160
159
  FORM_X_OBJECT = "FORM_X_OBJECT"
@@ -192,7 +191,6 @@ class Point:
192
191
  class BoundingRect:
193
192
  """
194
193
  Represents a bounding rectangle with position and dimensions.
195
- Matches the Java BoundingRect class.
196
194
  """
197
195
  x: float
198
196
  y: float
@@ -216,7 +214,6 @@ class BoundingRect:
216
214
  class Position:
217
215
  """
218
216
  Represents spatial positioning and location information for PDF objects.
219
- Closely mirrors the Java Position class with Python conventions.
220
217
  """
221
218
  page_index: Optional[int] = None
222
219
  shape: Optional[ShapeType] = None
@@ -230,7 +227,6 @@ class Position:
230
227
  def at_page(page_index: int) -> 'Position':
231
228
  """
232
229
  Creates a position specification for an entire page.
233
- Equivalent to Position.fromPageIndex() in Java.
234
230
  """
235
231
  return Position(page_index=page_index, mode=PositionMode.CONTAINS)
236
232
 
@@ -238,7 +234,6 @@ class Position:
238
234
  def at_page_coordinates(page_index: int, x: float, y: float) -> 'Position':
239
235
  """
240
236
  Creates a position specification for specific coordinates on a page.
241
- Equivalent to Position.onPageCoordinates() in Java.
242
237
  """
243
238
  position = Position.at_page(page_index)
244
239
  position.at_coordinates(Point(x, y))
@@ -248,7 +243,6 @@ class Position:
248
243
  def by_name(name: str) -> 'Position':
249
244
  """
250
245
  Creates a position specification for finding objects by name.
251
- Equivalent to Position.byName() in Java.
252
246
  """
253
247
  position = Position()
254
248
  position.name = name
@@ -257,7 +251,6 @@ class Position:
257
251
  def at_coordinates(self, point: Point) -> 'Position':
258
252
  """
259
253
  Sets the position to a specific point location.
260
- Equivalent to Position.set() in Java.
261
254
  """
262
255
  self.mode = PositionMode.CONTAINS
263
256
  self.shape = ShapeType.POINT
@@ -293,7 +286,6 @@ class Position:
293
286
  class ObjectRef:
294
287
  """
295
288
  Lightweight reference to a PDF object providing identity and type information.
296
- Mirrors the Java ObjectRef class exactly.
297
289
  """
298
290
  internal_id: str
299
291
  position: Position
@@ -333,7 +325,6 @@ class Color:
333
325
  a: int = 255 # Alpha channel, default fully opaque
334
326
 
335
327
  def __post_init__(self):
336
- # Validation similar to Java client
337
328
  for component in [self.r, self.g, self.b, self.a]:
338
329
  if not 0 <= component <= 255:
339
330
  raise ValueError(f"Color component must be between 0 and 255, got {component}")
@@ -354,7 +345,6 @@ class Font:
354
345
  class Image:
355
346
  """
356
347
  Represents an image object in a PDF document.
357
- Matches the Java Image class structure.
358
348
  """
359
349
  position: Optional[Position] = None
360
350
  format: Optional[str] = None
@@ -375,7 +365,6 @@ class Image:
375
365
  class Paragraph:
376
366
  """
377
367
  Represents a paragraph of text in a PDF document.
378
- Structure mirrors the Java Paragraph class.
379
368
  """
380
369
  position: Optional[Position] = None
381
370
  text_lines: Optional[List[str]] = None
@@ -456,7 +445,7 @@ class MoveRequest:
456
445
 
457
446
  def to_dict(self) -> dict:
458
447
  """Convert to dictionary for JSON serialization."""
459
- # Server API expects the new coordinates under 'newPosition' (see Java MoveRequest)
448
+ # Server API expects the new coordinates under 'newPosition'
460
449
  return {
461
450
  "objectRef": {
462
451
  "internalId": self.object_ref.internal_id,
@@ -488,7 +477,7 @@ class AddRequest:
488
477
  def to_dict(self) -> dict:
489
478
  """Convert to dictionary for JSON serialization matching server API.
490
479
  Server expects an AddRequest with a nested 'object' containing the PDFObject
491
- (with a 'type' discriminator), mirroring Java AddRequest(PDFObject object).
480
+ (with a 'type' discriminator).
492
481
  """
493
482
  obj = self.pdf_object
494
483
  return {
@@ -621,6 +610,58 @@ class FormFieldRef(ObjectRef):
621
610
  return self.value
622
611
 
623
612
 
613
+ class FontType(Enum):
614
+ """Font type classification from the PDF."""
615
+ SYSTEM = "SYSTEM"
616
+ STANDARD = "STANDARD"
617
+ EMBEDDED = "EMBEDDED"
618
+
619
+
620
+ @dataclass
621
+ class FontRecommendation:
622
+ """Represents a font recommendation with similarity score."""
623
+ font_name: str
624
+ font_type: 'FontType'
625
+ similarity_score: float
626
+
627
+ def get_font_name(self) -> str:
628
+ """Get the recommended font name."""
629
+ return self.font_name
630
+
631
+ def get_font_type(self) -> 'FontType':
632
+ """Get the recommended font type."""
633
+ return self.font_type
634
+
635
+ def get_similarity_score(self) -> float:
636
+ """Get the similarity score."""
637
+ return self.similarity_score
638
+
639
+
640
+ @dataclass
641
+ class TextStatus:
642
+ """Status information for text objects."""
643
+ modified: bool
644
+ encodable: bool
645
+ font_type: FontType
646
+ font_recommendation: FontRecommendation
647
+
648
+ def is_modified(self) -> bool:
649
+ """Check if the text has been modified."""
650
+ return self.modified
651
+
652
+ def is_encodable(self) -> bool:
653
+ """Check if the text is encodable."""
654
+ return self.encodable
655
+
656
+ def get_font_type(self) -> FontType:
657
+ """Get the font type."""
658
+ return self.font_type
659
+
660
+ def get_font_recommendation(self) -> FontRecommendation:
661
+ """Get the font recommendation."""
662
+ return self.font_recommendation
663
+
664
+
624
665
  class TextObjectRef(ObjectRef):
625
666
  """
626
667
  Represents a text object reference with additional text-specific properties.
@@ -630,13 +671,14 @@ class TextObjectRef(ObjectRef):
630
671
  def __init__(self, internal_id: str, position: Position, object_type: ObjectType,
631
672
  text: Optional[str] = None, font_name: Optional[str] = None,
632
673
  font_size: Optional[float] = None, line_spacings: Optional[List[float]] = None,
633
- color: Optional[Color] = None):
674
+ color: Optional[Color] = None, status: Optional[TextStatus] = None):
634
675
  super().__init__(internal_id, position, object_type)
635
676
  self.text = text
636
677
  self.font_name = font_name
637
678
  self.font_size = font_size
638
679
  self.line_spacings = line_spacings
639
680
  self.color = color
681
+ self.status = status
640
682
  self.children: List['TextObjectRef'] = []
641
683
 
642
684
  def get_text(self) -> Optional[str]:
@@ -663,6 +705,10 @@ class TextObjectRef(ObjectRef):
663
705
  """Get the child text objects."""
664
706
  return self.children
665
707
 
708
+ def get_status(self) -> Optional[TextStatus]:
709
+ """Get the status information."""
710
+ return self.status
711
+
666
712
 
667
713
  @dataclass
668
714
  class PageRef(ObjectRef):
@@ -680,3 +726,30 @@ class PageRef(ObjectRef):
680
726
  def get_orientation(self) -> Optional[Orientation]:
681
727
  """Get the page orientation."""
682
728
  return self.orientation
729
+
730
+
731
+ @dataclass
732
+ class CommandResult:
733
+ """
734
+ Result object returned by certain API endpoints indicating the outcome of an operation.
735
+ """
736
+ command_name: str
737
+ element_id: str | None
738
+ message: str | None
739
+ success: bool
740
+ warning: str | None
741
+
742
+ @classmethod
743
+ def from_dict(cls, data: dict) -> 'CommandResult':
744
+ """Create a CommandResult from a dictionary response."""
745
+ return cls(
746
+ command_name=data.get('commandName', ''),
747
+ element_id=data.get('elementId', ''),
748
+ message=data.get('message', ''),
749
+ success=data.get('success', False),
750
+ warning=data.get('warning', '')
751
+ )
752
+
753
+ @classmethod
754
+ def empty(cls, command_name: str, element_id: str | None) -> 'CommandResult':
755
+ return CommandResult(command_name=command_name, element_id=element_id, message=None, success=True, warning=None)
@@ -27,7 +27,7 @@ from .image_builder import ImageBuilder
27
27
  from .models import (
28
28
  ObjectRef, Position, ObjectType, Font, Image, Paragraph, FormFieldRef, TextObjectRef, PageRef,
29
29
  FindRequest, DeleteRequest, MoveRequest, PageMoveRequest, AddRequest, ModifyRequest, ModifyTextRequest,
30
- ChangeFormFieldRequest,
30
+ ChangeFormFieldRequest, CommandResult,
31
31
  ShapeType, PositionMode, PageSize, Orientation
32
32
  )
33
33
  from .paragraph_builder import ParagraphPageBuilder
@@ -915,7 +915,7 @@ class PDFDancer:
915
915
  return ImageBuilder(self)
916
916
 
917
917
  # Modify Operations
918
- def _modify_paragraph(self, object_ref: ObjectRef, new_paragraph: Union[Paragraph, str]) -> bool:
918
+ def _modify_paragraph(self, object_ref: ObjectRef, new_paragraph: Union[Paragraph, str]) -> CommandResult:
919
919
  """
920
920
  Modifies a paragraph object or its text content.
921
921
 
@@ -929,20 +929,20 @@ class PDFDancer:
929
929
  if object_ref is None:
930
930
  raise ValidationException("Object reference cannot be null")
931
931
  if new_paragraph is None:
932
- raise ValidationException("New paragraph cannot be null")
932
+ return CommandResult.empty("ModifyParagraph", object_ref.internal_id)
933
933
 
934
934
  if isinstance(new_paragraph, str):
935
- # Text modification
935
+ # Text modification - returns CommandResult
936
936
  request_data = ModifyTextRequest(object_ref, new_paragraph).to_dict()
937
937
  response = self._make_request('PUT', '/pdf/text/paragraph', data=request_data)
938
+ return CommandResult.from_dict(response.json())
938
939
  else:
939
940
  # Object modification
940
941
  request_data = ModifyRequest(object_ref, new_paragraph).to_dict()
941
942
  response = self._make_request('PUT', '/pdf/modify', data=request_data)
943
+ return CommandResult.from_dict(response.json())
942
944
 
943
- return response.json()
944
-
945
- def _modify_text_line(self, object_ref: ObjectRef, new_text: str) -> bool:
945
+ def _modify_text_line(self, object_ref: ObjectRef, new_text: str) -> CommandResult:
946
946
  """
947
947
  Modifies a text line object.
948
948
 
@@ -960,7 +960,7 @@ class PDFDancer:
960
960
 
961
961
  request_data = ModifyTextRequest(object_ref, new_text).to_dict()
962
962
  response = self._make_request('PUT', '/pdf/text/line', data=request_data)
963
- return response.json()
963
+ return CommandResult.from_dict(response.json())
964
964
 
965
965
  # Font Operations
966
966
 
@@ -1171,6 +1171,29 @@ class PDFDancer:
1171
1171
  if all(isinstance(v, int) for v in [red, green, blue]):
1172
1172
  color = Color(red, green, blue, alpha)
1173
1173
 
1174
+ # Parse status if present
1175
+ status = None
1176
+ status_data = obj_data.get('status')
1177
+ if isinstance(status_data, dict):
1178
+ from .models import TextStatus, FontRecommendation, FontType
1179
+
1180
+ # Parse font recommendation
1181
+ font_rec_data = status_data.get('fontRecommendation')
1182
+ font_rec = None
1183
+ if isinstance(font_rec_data, dict):
1184
+ font_rec = FontRecommendation(
1185
+ font_name=font_rec_data.get('fontName', ''),
1186
+ font_type=FontType(font_rec_data.get('fontType', 'SYSTEM')),
1187
+ similarity_score=font_rec_data.get('similarityScore', 0.0)
1188
+ )
1189
+
1190
+ status = TextStatus(
1191
+ modified=status_data.get('modified', False),
1192
+ encodable=status_data.get('encodable', True),
1193
+ font_type=FontType(status_data.get('fontType', 'UNKNOWN')),
1194
+ font_recommendation=font_rec
1195
+ )
1196
+
1174
1197
  text_object = TextObjectRef(
1175
1198
  internal_id=internal_id,
1176
1199
  position=position,
@@ -1179,7 +1202,8 @@ class PDFDancer:
1179
1202
  font_name=obj_data.get('fontName') if isinstance(obj_data.get('fontName'), str) else None,
1180
1203
  font_size=obj_data.get('fontSize') if isinstance(obj_data.get('fontSize'), (int, float)) else None,
1181
1204
  line_spacings=line_spacings,
1182
- color=color
1205
+ color=color,
1206
+ status=status
1183
1207
  )
1184
1208
 
1185
1209
  if isinstance(obj_data.get('children'), list) and len(obj_data['children']) > 0:
@@ -1,10 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import statistics
4
+ import sys
4
5
  from dataclasses import dataclass
5
6
  from typing import Optional, List
6
7
 
7
8
  from . import ObjectType, Position, ObjectRef, Point, Paragraph, Font, Color, FormFieldRef, TextObjectRef
9
+ from .models import CommandResult
8
10
 
9
11
 
10
12
  @dataclass
@@ -177,7 +179,7 @@ class BaseTextEdit:
177
179
 
178
180
 
179
181
  class ParagraphEdit(BaseTextEdit):
180
- def apply(self) -> bool:
182
+ def apply(self) -> CommandResult:
181
183
  if (
182
184
  self._position is None
183
185
  and self._line_spacing is None
@@ -186,7 +188,10 @@ class ParagraphEdit(BaseTextEdit):
186
188
  and self._color is None
187
189
  ):
188
190
  # noinspection PyProtectedMember
189
- return self._target_obj._client._modify_paragraph(self._object_ref, self._new_text)
191
+ result = self._target_obj._client._modify_paragraph(self._object_ref, self._new_text)
192
+ if result.warning is not None:
193
+ print(f"WARNING: {result.warning}", file=sys.stderr)
194
+ return result
190
195
  else:
191
196
  new_paragraph = Paragraph(
192
197
  position=self._position if self._position is not None else self._object_ref.position,
@@ -196,7 +201,10 @@ class ParagraphEdit(BaseTextEdit):
196
201
  color=self._get_color(),
197
202
  )
198
203
  # noinspection PyProtectedMember
199
- return self._target_obj._client._modify_paragraph(self._object_ref, new_paragraph)
204
+ result = self._target_obj._client._modify_paragraph(self._object_ref, new_paragraph)
205
+ if result.warning is not None:
206
+ print(f"WARNING: {result.warning}", file=sys.stderr)
207
+ return result
200
208
 
201
209
  def _get_line_spacing(self) -> float:
202
210
  if self._line_spacing is not None:
@@ -241,9 +249,14 @@ class TextLineEdit(BaseTextEdit):
241
249
  and self._color is None
242
250
  ):
243
251
  # noinspection PyProtectedMember
244
- return self._target_obj._client._modify_text_line(self._object_ref, self._new_text)
252
+ result = self._target_obj._client._modify_text_line(self._object_ref, self._new_text)
253
+ if result.warning is not None:
254
+ print(f"WARNING: {result.warning}", file=sys.stderr)
255
+ return result
245
256
  else:
246
- raise UnsupportedOperation("TextLineEdit cannot be applied to text lines")
257
+ # noinspection PyProtectedMember
258
+ # return self._target_obj._client._modify_text_line(self._object_ref, new_textline)
259
+ raise UnsupportedOperation("Full TextLineEdit not implemented - TODO")
247
260
 
248
261
 
249
262
  class ParagraphObject(PDFObjectBase):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdfdancer-client-python
3
- Version: 0.2.14
3
+ Version: 0.2.15
4
4
  Summary: Python client for PDFDancer API
5
5
  Author-email: "The Famous Cat Ltd." <hi@thefamouscat.com>
6
6
  License: MIT
@@ -169,8 +169,6 @@ class PDFAssertions(object):
169
169
  if page_index is None:
170
170
  for page in self.pdf.pages():
171
171
  total = total + len(page.select_elements())
172
- for e in page.select_elements():
173
- print(e)
174
172
  else:
175
173
  total = len(self.pdf.page(page_index).select_elements())
176
174
  assert total == nr_of_elements, f"Total number of elements differ, actual {total} != expected {nr_of_elements}"
@@ -21,8 +21,6 @@ def test_delete_form(tmp_path: Path):
21
21
 
22
22
  assert pdf.select_forms() == []
23
23
  assert len(pdf.select_elements()) == len(all_elements) - 17
24
- for e in pdf.select_elements():
25
- print(e)
26
24
  pdf.save("/tmp/delete-form1.pdf")
27
25
 
28
26
  (
@@ -1,10 +1,22 @@
1
1
  import pytest
2
2
 
3
+ from pdfdancer import FontType
3
4
  from pdfdancer.pdfdancer_v1 import PDFDancer
4
5
  from tests.e2e import _require_env_and_fixture
5
6
  from tests.e2e.pdf_assertions import PDFAssertions
6
7
 
7
8
 
9
+ def test_find_lines_by_position_multi():
10
+ base_url, token, pdf_path = _require_env_and_fixture("ObviouslyAwesome.pdf")
11
+
12
+ with PDFDancer.open(pdf_path, token=token, base_url=base_url, timeout=30.0) as pdf:
13
+ for i in range(0, 10):
14
+ for line in pdf.select_text_lines():
15
+ assert line.object_ref().status is not None
16
+ assert not line.object_ref().status.is_modified()
17
+ assert line.object_ref().status.is_encodable()
18
+
19
+
8
20
  def test_find_lines_by_position():
9
21
  base_url, token, pdf_path = _require_env_and_fixture("ObviouslyAwesome.pdf")
10
22
 
@@ -13,16 +25,22 @@ def test_find_lines_by_position():
13
25
  assert len(lines) == 340
14
26
 
15
27
  first = lines[0]
16
- assert first.internal_id == "LINE_000001"
28
+ assert first.internal_id == "TEXTLINE_000001"
17
29
  assert first.position is not None
18
30
  assert pytest.approx(first.position.x(), rel=0, abs=1) == 326
19
31
  assert pytest.approx(first.position.y(), rel=0, abs=1) == 706
32
+ assert first.object_ref().status is not None
33
+ assert not first.object_ref().status.is_modified()
34
+ assert first.object_ref().status.is_encodable()
20
35
 
21
36
  last = lines[-1]
22
- assert last.internal_id == "LINE_000340"
37
+ assert last.internal_id == "TEXTLINE_000340"
23
38
  assert last.position is not None
24
39
  assert pytest.approx(last.position.x(), rel=0, abs=2) == 548
25
40
  assert pytest.approx(last.position.y(), rel=0, abs=2) == 35
41
+ assert last.object_ref().status is not None
42
+ assert not last.object_ref().status.is_modified()
43
+ assert last.object_ref().status.is_encodable()
26
44
 
27
45
 
28
46
  def test_find_lines_by_text():
@@ -33,7 +51,7 @@ def test_find_lines_by_text():
33
51
  assert len(lines) == 1
34
52
 
35
53
  line = lines[0]
36
- assert line.internal_id == "LINE_000002"
54
+ assert line.internal_id == "TEXTLINE_000002"
37
55
  assert pytest.approx(line.position.x(), rel=0, abs=1) == 54
38
56
  assert pytest.approx(line.position.y(), rel=0, abs=2) == 606
39
57
 
@@ -66,6 +84,10 @@ def test_move_line():
66
84
 
67
85
  moved_para = pdf.page(0).select_paragraphs_at(new_x, new_y)[0]
68
86
  assert moved_para is not None
87
+ assert moved_para.object_ref().status is not None
88
+ assert moved_para.object_ref().status.is_encodable()
89
+ assert moved_para.object_ref().status.font_type == FontType.EMBEDDED
90
+ assert not moved_para.object_ref().status.is_modified()
69
91
 
70
92
  (
71
93
  PDFAssertions(pdf)
@@ -78,16 +100,44 @@ def test_modify_line():
78
100
 
79
101
  with PDFDancer.open(pdf_path, token=token, base_url=base_url, timeout=30.0) as pdf:
80
102
  line = pdf.page(0).select_text_lines_starting_with("The Complete")[0]
81
- line.edit().replace(" replaced ").apply()
103
+ result = line.edit().replace(" replaced ").apply()
104
+
105
+ # this should issue a warning about an modified text with an embedded font
106
+ # the information is right now only available when selecting the paragraph again, that's bad
107
+ assert result.warning is not None
108
+ assert "You are using an embedded font and modified the text." in result.warning
82
109
 
83
110
  # Validate replacements
84
111
  assert pdf.page(0).select_text_lines_starting_with("The Complete") == []
85
- assert pdf.page(0).select_text_lines_starting_with(" replaced ") != []
112
+ lines = pdf.page(0).select_text_lines_starting_with(" replaced ")
113
+ assert lines != []
86
114
  assert pdf.page(0).select_paragraphs_starting_with(" replaced ") != []
87
-
115
+ assert lines[0] is not None
116
+ assert lines[0].object_ref().status is not None
117
+ assert lines[0].object_ref().status.is_encodable
118
+ assert lines[0].object_ref().status.font_type == FontType.EMBEDDED
119
+ assert lines[0].object_ref().status.is_modified
88
120
  (
89
121
  PDFAssertions(pdf)
90
122
  .assert_textline_does_not_exist("The Complete")
91
123
  .assert_textline_exists(" replaced ")
92
124
  .assert_paragraph_exists(" replaced ")
93
125
  )
126
+
127
+
128
+ def test_modify_line_multi():
129
+ base_url, token, pdf_path = _require_env_and_fixture("ObviouslyAwesome.pdf")
130
+
131
+ with PDFDancer.open(pdf_path, token=token, base_url=base_url, timeout=30.0) as pdf:
132
+ line_text = "The Complete"
133
+ for i in range(0, 10):
134
+ line = pdf.page(0).select_text_lines_starting_with(line_text)[0]
135
+ line_text = f"{i} The Complete C"
136
+ # line.edit().replace(line_text).color(Color(255, 0, 0)).apply()
137
+ assert line.edit().replace(line_text).apply()
138
+ pdf.save("/tmp/test_modify_line_multi.pdf")
139
+
140
+ (
141
+ PDFAssertions(pdf)
142
+ .assert_textline_exists("9 The Complete C")
143
+ )
@@ -1,6 +1,6 @@
1
1
  import pytest
2
2
 
3
- from pdfdancer import PDFDancer, PageSize, Orientation, StandardFonts, Color
3
+ from pdfdancer import PDFDancer, PageSize, Orientation, StandardFonts, Color, ValidationException
4
4
  from tests.e2e import _require_env
5
5
  from tests.e2e.pdf_assertions import PDFAssertions
6
6
 
@@ -89,6 +89,30 @@ def test_create_blank_pdf_add_content():
89
89
  )
90
90
 
91
91
 
92
+ def test_create_blank_pdf_add_and_modify_content():
93
+ """Test creating a blank PDF and adding content"""
94
+ base_url, token = _require_env()
95
+
96
+ with PDFDancer.new(token=token, base_url=base_url) as pdf:
97
+ (
98
+ pdf.new_paragraph()
99
+ .text("Hello from blank PDF")
100
+ .font(StandardFonts.COURIER_BOLD_OBLIQUE, 9)
101
+ .color(Color(128, 56, 127))
102
+ .at(0, 100, 201.5)
103
+ .add()
104
+ )
105
+ assert pdf.page(0).select_text_lines()[0].internal_id
106
+ pdf.save("/tmp/test_create_blank_pdf_add_and_modify_content.pdf")
107
+
108
+ with PDFDancer.open("/tmp/test_create_blank_pdf_add_and_modify_content.pdf", token=token,
109
+ base_url=base_url) as pdf2:
110
+ for i in range(0, 10):
111
+ line = pdf2.page(0).select_text_lines()[0]
112
+ assert line.edit().replace(f"hello {i}").apply()
113
+ pdf2.save("/tmp/test_create_blank_pdf_add_and_modify_content2.pdf")
114
+
115
+
92
116
  def test_create_blank_pdf_add_page():
93
117
  base_url, token = _require_env()
94
118
 
@@ -109,8 +133,6 @@ def test_create_blank_pdf_invalid_page_count():
109
133
  """Test that invalid page count raises validation error"""
110
134
  base_url, token = _require_env()
111
135
 
112
- from pdfdancer import ValidationException
113
-
114
136
  with pytest.raises(ValidationException) as exc_info:
115
137
  PDFDancer.new(
116
138
  token=token,
@@ -1,6 +1,6 @@
1
1
  import pytest
2
2
 
3
- from pdfdancer import Color, StandardFonts
3
+ from pdfdancer import Color, StandardFonts, FontType
4
4
  from pdfdancer.pdfdancer_v1 import PDFDancer
5
5
  from tests.e2e import _require_env_and_fixture
6
6
  from tests.e2e.pdf_assertions import PDFAssertions
@@ -28,6 +28,11 @@ def test_find_paragraphs_by_position():
28
28
  assert pytest.approx(last.position.x(), rel=0, abs=1) == 54
29
29
  assert pytest.approx(last.position.y(), rel=0, abs=2) == 496
30
30
 
31
+ assert last.object_ref().status is not None
32
+ assert last.object_ref().status.is_encodable()
33
+ assert last.object_ref().status.font_type == FontType.EMBEDDED
34
+ assert not last.object_ref().status.is_modified()
35
+
31
36
 
32
37
  def test_find_paragraphs_by_text():
33
38
  base_url, token, pdf_path = _require_env_and_fixture("ObviouslyAwesome.pdf")
@@ -60,6 +65,11 @@ def test_move_paragraph():
60
65
  moved = pdf.page(0).select_paragraphs_at(0.1, 300)[0]
61
66
  assert moved is not None
62
67
 
68
+ assert moved.object_ref().status is not None
69
+ assert moved.object_ref().status.is_encodable()
70
+ assert moved.object_ref().status.font_type == FontType.EMBEDDED
71
+ assert not moved.object_ref().status.is_modified()
72
+
63
73
 
64
74
  def test_modify_paragraph():
65
75
  base_url, token, pdf_path = _require_env_and_fixture("ObviouslyAwesome.pdf")
@@ -76,6 +86,12 @@ def test_modify_paragraph():
76
86
  .apply()
77
87
  )
78
88
 
89
+ moved = pdf.page(0).select_paragraphs_at(300.1, 500)[0]
90
+ assert moved.object_ref().status is not None
91
+ assert moved.object_ref().status.is_encodable()
92
+ assert moved.object_ref().status.font_type == FontType.STANDARD
93
+ assert moved.object_ref().status.is_modified()
94
+
79
95
  (
80
96
  PDFAssertions(pdf)
81
97
  .assert_textline_has_font("Awesomely", "Helvetica", 12)
@@ -136,17 +152,75 @@ def test_modify_paragraph_without_position_and_spacing():
136
152
  )
137
153
 
138
154
 
139
- def test_modify_paragraph_only_font():
155
+ def test_modify_paragraph_noop():
140
156
  base_url, token, pdf_path = _require_env_and_fixture("ObviouslyAwesome.pdf")
141
157
 
142
158
  with PDFDancer.open(pdf_path, token=token, base_url=base_url, timeout=30.0) as pdf:
143
159
  paragraph = pdf.page(0).select_paragraphs_starting_with("The Complete")[0]
160
+ (
161
+ paragraph.edit()
162
+ .apply()
163
+ )
164
+ paragraph = pdf.page(0).select_paragraphs_starting_with("The Complete")[0]
165
+ assert paragraph.object_ref().status is not None
166
+ assert paragraph.object_ref().status.is_encodable()
167
+ assert paragraph.object_ref().status.font_type == FontType.EMBEDDED
168
+ assert not paragraph.object_ref().status.is_modified()
169
+
144
170
  (
145
- paragraph.edit()
146
- .font("Helvetica", 28)
147
- .apply()
171
+ PDFAssertions(pdf)
172
+ .assert_textline_has_font("The Complete", "IXKSWR+Poppins-Bold", 1)
173
+ .assert_textline_has_color("The Complete", Color(255, 255, 255))
148
174
  )
149
175
 
176
+
177
+ def test_modify_paragraph_only_text():
178
+ base_url, token, pdf_path = _require_env_and_fixture("ObviouslyAwesome.pdf")
179
+
180
+ with PDFDancer.open(pdf_path, token=token, base_url=base_url, timeout=30.0) as pdf:
181
+ paragraph = pdf.page(0).select_paragraphs_starting_with("The Complete")[0]
182
+ result = (
183
+ paragraph.edit()
184
+ .replace("lorem\nipsum\nCaesar")
185
+ .apply()
186
+ )
187
+
188
+ # this should issue a warning about an modified text with an embedded font
189
+ # the information is right now only available when selecting the paragraph again, that's bad
190
+ assert result.warning is not None
191
+ assert "You are using an embedded font and modified the text." in result.warning
192
+
193
+ paragraph = pdf.page(0).select_paragraphs_starting_with("lorem")[0]
194
+ assert paragraph.object_ref().status is not None
195
+ assert paragraph.object_ref().status.is_encodable()
196
+ assert paragraph.object_ref().status.font_type == FontType.EMBEDDED
197
+ assert paragraph.object_ref().status.is_modified()
198
+
199
+ (
200
+ PDFAssertions(pdf)
201
+ .assert_textline_does_not_exist("The Complete")
202
+ .assert_textline_has_color("lorem", Color(255, 255, 255))
203
+ .assert_textline_has_color("ipsum", Color(255, 255, 255))
204
+ .assert_textline_has_color("Caesar", Color(255, 255, 255))
205
+ )
206
+
207
+
208
+ def test_modify_paragraph_only_font():
209
+ base_url, token, pdf_path = _require_env_and_fixture("ObviouslyAwesome.pdf")
210
+
211
+ with PDFDancer.open(pdf_path, token=token, base_url=base_url, timeout=30.0) as pdf:
212
+ paragraph = pdf.page(0).select_paragraphs_starting_with("The Complete")[0]
213
+ (
214
+ paragraph.edit()
215
+ .font("Helvetica", 28)
216
+ .apply()
217
+ )
218
+ paragraph = pdf.page(0).select_paragraphs_starting_with("The Complete")[0]
219
+ assert paragraph.object_ref().status is not None
220
+ assert paragraph.object_ref().status.is_encodable()
221
+ assert paragraph.object_ref().status.font_type == FontType.STANDARD
222
+ assert paragraph.object_ref().status.is_modified()
223
+
150
224
  # TODO does not preserve color and fucks up line spacings
151
225
  (
152
226
  PDFAssertions(pdf)
@@ -167,6 +241,12 @@ def test_modify_paragraph_only_move():
167
241
  .apply()
168
242
  )
169
243
 
244
+ paragraph = pdf.page(0).select_paragraphs_starting_with("The Complete")[0]
245
+ assert paragraph.object_ref().status is not None
246
+ assert paragraph.object_ref().status.is_encodable()
247
+ assert paragraph.object_ref().status.font_type == FontType.EMBEDDED
248
+ assert paragraph.object_ref().status.is_modified() # This should actually not be marked as 'modified' but since we are using a ModifyObject operation we are not (yet) able to detect this
249
+
170
250
  (
171
251
  PDFAssertions(pdf)
172
252
  .assert_textline_has_font("The Complete", "IXKSWR+Poppins-Bold", 1)
@@ -182,13 +262,19 @@ def test_modify_paragraph_simple():
182
262
  paragraph = pdf.page(0).select_paragraphs_starting_with("The Complete")[0]
183
263
  paragraph.edit().replace("Awesomely\nObvious!").apply()
184
264
 
185
- (
186
- PDFAssertions(pdf)
187
- .assert_textline_has_font("Awesomely", "IXKSWR+Poppins-Bold", 1)
188
- .assert_textline_has_font("Obvious!", "IXKSWR+Poppins-Bold", 1)
189
- .assert_textline_has_color("Awesomely", Color(255, 255, 255))
190
- .assert_textline_has_color("Obvious!", Color(255, 255, 255))
191
- )
265
+ paragraph = pdf.page(0).select_paragraphs_starting_with("Awesomely")[0]
266
+ assert paragraph.object_ref().status is not None
267
+ assert paragraph.object_ref().status.is_encodable()
268
+ assert paragraph.object_ref().status.font_type == FontType.EMBEDDED
269
+ assert paragraph.object_ref().status.is_modified()
270
+
271
+ (
272
+ PDFAssertions(pdf)
273
+ .assert_textline_has_font("Awesomely", "IXKSWR+Poppins-Bold", 1)
274
+ .assert_textline_has_font("Obvious!", "IXKSWR+Poppins-Bold", 1)
275
+ .assert_textline_has_color("Awesomely", Color(255, 255, 255))
276
+ .assert_textline_has_color("Obvious!", Color(255, 255, 255))
277
+ )
192
278
 
193
279
 
194
280
  def test_add_paragraph_with_custom_font1_expect_not_found():