pdfdancer-client-python 0.3.6__tar.gz → 0.3.8__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 (83) hide show
  1. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/PKG-INFO +12 -4
  2. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/README.md +11 -3
  3. pdfdancer_client_python-0.3.8/media/logo-silver-512h.webp +0 -0
  4. pdfdancer_client_python-0.3.8/media/logo-silver-60h.webp +0 -0
  5. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/pyproject.toml +1 -1
  6. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/src/pdfdancer/__init__.py +0 -2
  7. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/src/pdfdancer/models.py +15 -9
  8. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/src/pdfdancer/pdfdancer_v1.py +40 -16
  9. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/src/pdfdancer_client_python.egg-info/PKG-INFO +12 -4
  10. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/src/pdfdancer_client_python.egg-info/SOURCES.txt +2 -0
  11. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/test_line.py +3 -1
  12. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/test_template_replace.py +75 -33
  13. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/.claude/commands/discuss.md +0 -0
  14. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/.claude/commands/implement-new-api-features.md +0 -0
  15. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/.flake8 +0 -0
  16. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/.github/workflows/ci.yml +0 -0
  17. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/.github/workflows/daily-tests.yml +0 -0
  18. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/.gitignore +0 -0
  19. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/CLAUDE.md +0 -0
  20. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/LICENSE +0 -0
  21. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/NOTICE +0 -0
  22. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/TODO.md +0 -0
  23. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/check.py +0 -0
  24. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/docs/api-schemas/v0.yml +0 -0
  25. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/docs/api-schemas/v1.yml +0 -0
  26. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/media/logo-orange-512h.webp +0 -0
  27. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/media/logo-orange-60h.webp +0 -0
  28. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/release.py +0 -0
  29. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/setup.cfg +0 -0
  30. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/src/pdfdancer/exceptions.py +0 -0
  31. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/src/pdfdancer/fingerprint.py +0 -0
  32. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/src/pdfdancer/image_builder.py +0 -0
  33. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/src/pdfdancer/page_builder.py +0 -0
  34. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/src/pdfdancer/paragraph_builder.py +0 -0
  35. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/src/pdfdancer/path_builder.py +0 -0
  36. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/src/pdfdancer/text_line_builder.py +0 -0
  37. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/src/pdfdancer/types.py +0 -0
  38. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/src/pdfdancer_client_python.egg-info/dependency_links.txt +0 -0
  39. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/src/pdfdancer_client_python.egg-info/requires.txt +0 -0
  40. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/src/pdfdancer_client_python.egg-info/top_level.txt +0 -0
  41. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/test.sh +0 -0
  42. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/__init__.py +0 -0
  43. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/conftest.py +0 -0
  44. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/__init__.py +0 -0
  45. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/pdf_assertions.py +0 -0
  46. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/test_acroform.py +0 -0
  47. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/test_bezier_builder.py +0 -0
  48. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/test_context_manager.py +0 -0
  49. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/test_form_x_objects.py +0 -0
  50. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/test_image.py +0 -0
  51. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/test_image_transform.py +0 -0
  52. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/test_line_builder.py +0 -0
  53. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/test_new_pdf.py +0 -0
  54. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/test_page.py +0 -0
  55. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/test_paragraph.py +0 -0
  56. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/test_path.py +0 -0
  57. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/test_path_builder.py +0 -0
  58. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/test_path_builder_rectangle.py +0 -0
  59. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/test_path_comprehensive.py +0 -0
  60. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/test_pdfdancer.py +0 -0
  61. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/test_positioning.py +0 -0
  62. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/test_rectangle_builder.py +0 -0
  63. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/test_redact.py +0 -0
  64. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/test_singular_selection.py +0 -0
  65. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/test_snapshot.py +0 -0
  66. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/e2e/test_text_line_edit.py +0 -0
  67. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/fixtures/DancingScript-Regular.ttf +0 -0
  68. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/fixtures/Empty.pdf +0 -0
  69. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/fixtures/JetBrainsMono-Regular.ttf +0 -0
  70. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/fixtures/Showcase.pdf +0 -0
  71. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/fixtures/basic-paths.pdf +0 -0
  72. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/fixtures/form-xobject-example.pdf +0 -0
  73. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/fixtures/logo-80.png +0 -0
  74. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/fixtures/mixed-form-types.pdf +0 -0
  75. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/test_anonymous_token.py +0 -0
  76. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/test_fingerprint.py +0 -0
  77. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/test_models.py +0 -0
  78. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/test_openapi_compliance.py +0 -0
  79. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/test_path_models.py +0 -0
  80. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/test_pdf_object_equality.py +0 -0
  81. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/test_rate_limit.py +0 -0
  82. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/tests/test_standard_fonts.py +0 -0
  83. {pdfdancer_client_python-0.3.6 → pdfdancer_client_python-0.3.8}/update-api-spec.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdfdancer-client-python
3
- Version: 0.3.6
3
+ Version: 0.3.8
4
4
  Summary: Python client for PDFDancer API
5
5
  Author-email: "The Famous Cat Ltd." <hi@thefamouscat.com>
6
6
  License:
@@ -239,9 +239,9 @@ Dynamic: license-file
239
239
 
240
240
  # PDFDancer Python Client
241
241
 
242
- ![PDFDancer logo](media/logo-orange-60h.webp)
242
+ ![PDFDancer logo](media/logo-silver-60h.webp)
243
243
 
244
- **Stop fighting PDFs. Start editing them.**
244
+ ## PDF used to be read-only. We fixed that.
245
245
 
246
246
  Edit text in real-world PDFs—even ones you didn't create. Move images, reposition headers, and change fonts with
247
247
  pixel-perfect control from Python. The same API is also available for TypeScript and Java.
@@ -665,9 +665,17 @@ Contributions are welcome via pull request. Please:
665
665
  4. Follow existing code style and patterns
666
666
  5. Update documentation as needed
667
667
 
668
+ ## Helpful links
669
+
670
+ - [API documentation](https://docs.pdfdancer.com?utm_source=github&utm_medium=readme&utm_campaign=pdfdancer-python)
671
+ - [Product overview](https://www.pdfdancer.com?utm_source=github&utm_medium=readme&utm_campaign=pdfdancer-python)
672
+ - [PyPI](https://pypi.org/project/pdfdancer-client-python/)
673
+ - [Changelog](https://www.pdfdancer.com/changelog/?utm_source=github&utm_medium=readme&utm_campaign=pdfdancer-python)
674
+ - [Status](https://status.pdfdancer.com?utm_source=github&utm_medium=readme&utm_campaign=pdfdancer-python)
675
+
668
676
  ## Related SDKs
669
677
 
670
- - TypeScript client: https://github.com/MenschMachine/pdfdancer-client-js
678
+ - TypeScript client: https://github.com/MenschMachine/pdfdancer-client-typescript
671
679
  - Java client: https://github.com/MenschMachine/pdfdancer-client-java
672
680
 
673
681
  ## License
@@ -1,8 +1,8 @@
1
1
  # PDFDancer Python Client
2
2
 
3
- ![PDFDancer logo](media/logo-orange-60h.webp)
3
+ ![PDFDancer logo](media/logo-silver-60h.webp)
4
4
 
5
- **Stop fighting PDFs. Start editing them.**
5
+ ## PDF used to be read-only. We fixed that.
6
6
 
7
7
  Edit text in real-world PDFs—even ones you didn't create. Move images, reposition headers, and change fonts with
8
8
  pixel-perfect control from Python. The same API is also available for TypeScript and Java.
@@ -426,9 +426,17 @@ Contributions are welcome via pull request. Please:
426
426
  4. Follow existing code style and patterns
427
427
  5. Update documentation as needed
428
428
 
429
+ ## Helpful links
430
+
431
+ - [API documentation](https://docs.pdfdancer.com?utm_source=github&utm_medium=readme&utm_campaign=pdfdancer-python)
432
+ - [Product overview](https://www.pdfdancer.com?utm_source=github&utm_medium=readme&utm_campaign=pdfdancer-python)
433
+ - [PyPI](https://pypi.org/project/pdfdancer-client-python/)
434
+ - [Changelog](https://www.pdfdancer.com/changelog/?utm_source=github&utm_medium=readme&utm_campaign=pdfdancer-python)
435
+ - [Status](https://status.pdfdancer.com?utm_source=github&utm_medium=readme&utm_campaign=pdfdancer-python)
436
+
429
437
  ## Related SDKs
430
438
 
431
- - TypeScript client: https://github.com/MenschMachine/pdfdancer-client-js
439
+ - TypeScript client: https://github.com/MenschMachine/pdfdancer-client-typescript
432
440
  - Java client: https://github.com/MenschMachine/pdfdancer-client-java
433
441
 
434
442
  ## License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pdfdancer-client-python"
7
- version = "0.3.6"
7
+ version = "0.3.8"
8
8
  description = "Python client for PDFDancer API"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -44,7 +44,6 @@ from .models import (
44
44
  ShapeType,
45
45
  Size,
46
46
  StandardFonts,
47
- TemplateReplacement,
48
47
  TextObjectRef,
49
48
  TextStatus,
50
49
  )
@@ -93,7 +92,6 @@ __all__ = [
93
92
  "RedactTarget",
94
93
  "RedactResponse",
95
94
  "ReflowPreset",
96
- "TemplateReplacement",
97
95
  "PdfDancerException",
98
96
  "FontNotFoundException",
99
97
  "ValidationException",
@@ -1686,25 +1686,31 @@ class TemplateReplacement:
1686
1686
  Parameters:
1687
1687
  - placeholder: The exact text to find and replace in the PDF.
1688
1688
  - text: The text to replace the placeholder with.
1689
-
1690
- Example:
1691
- ```python
1692
- replacement = TemplateReplacement(
1693
- placeholder="{{NAME}}",
1694
- text="John Doe"
1695
- )
1696
- ```
1689
+ - font: Optional font for the replacement text.
1690
+ - color: Optional color for the replacement text.
1697
1691
  """
1698
1692
 
1699
1693
  placeholder: str
1700
1694
  text: str
1695
+ font: Optional[Font] = None
1696
+ color: Optional[Color] = None
1701
1697
 
1702
1698
  def to_dict(self) -> dict:
1703
1699
  """Convert to dictionary for JSON serialization."""
1704
- return {
1700
+ result: Dict[str, Any] = {
1705
1701
  "placeholder": self.placeholder,
1706
1702
  "text": self.text,
1707
1703
  }
1704
+ if self.font:
1705
+ result["font"] = {"name": self.font.name, "size": self.font.size}
1706
+ if self.color:
1707
+ result["color"] = {
1708
+ "red": self.color.r,
1709
+ "green": self.color.g,
1710
+ "blue": self.color.b,
1711
+ "alpha": self.color.a,
1712
+ }
1713
+ return result
1708
1714
 
1709
1715
 
1710
1716
  @dataclass
@@ -16,7 +16,7 @@ import time
16
16
  from datetime import datetime, timezone
17
17
  from importlib.metadata import version as get_package_version
18
18
  from pathlib import Path
19
- from typing import TYPE_CHECKING, Any, BinaryIO, List, Mapping, Optional, Union
19
+ from typing import TYPE_CHECKING, Any, BinaryIO, Dict, List, Mapping, Optional, Union
20
20
 
21
21
  import httpx
22
22
  from dotenv import load_dotenv
@@ -123,6 +123,26 @@ DEFAULT_RETRY_BACKOFF_FACTOR = float(
123
123
  )
124
124
 
125
125
 
126
+ def _dict_to_replacements(
127
+ replacements: Dict[str, Union[str, dict]]
128
+ ) -> List[TemplateReplacement]:
129
+ """Convert dict-based replacements to TemplateReplacement list."""
130
+ result = []
131
+ for placeholder, value in replacements.items():
132
+ if isinstance(value, str):
133
+ result.append(TemplateReplacement(placeholder=placeholder, text=value))
134
+ else:
135
+ result.append(
136
+ TemplateReplacement(
137
+ placeholder=placeholder,
138
+ text=value["text"],
139
+ font=value.get("font"),
140
+ color=value.get("color"),
141
+ )
142
+ )
143
+ return result
144
+
145
+
126
146
  def _generate_timestamp() -> str:
127
147
  """
128
148
  Generate a timestamp string in the format expected by the API.
@@ -611,7 +631,7 @@ class PageClient:
611
631
 
612
632
  def apply_replacements(
613
633
  self,
614
- replacements: List[TemplateReplacement],
634
+ replacements: Dict[str, Union[str, dict]],
615
635
  reflow_preset: Optional[ReflowPreset] = None,
616
636
  ) -> bool:
617
637
  """
@@ -621,8 +641,9 @@ class PageClient:
621
641
  content. All placeholders must be found or the operation fails atomically.
622
642
 
623
643
  Args:
624
- replacements: List of TemplateReplacement objects specifying
625
- placeholder/replacement pairs
644
+ replacements: Dict mapping placeholder strings to replacement values.
645
+ - Simple: {"{{NAME}}": "John Doe"}
646
+ - With options: {"{{NAME}}": {"text": "John", "font": Font(...), "color": Color(...)}}
626
647
  reflow_preset: Optional ReflowPreset to control text reflow behavior.
627
648
  - BEST_EFFORT: Attempt to reflow, proceed even if imperfect
628
649
  - FIT_OR_FAIL: Reflow must succeed or operation fails
@@ -633,14 +654,15 @@ class PageClient:
633
654
 
634
655
  Example:
635
656
  ```python
636
- page.apply_replacements([
637
- TemplateReplacement("{{NAME}}", "John Doe"),
638
- ])
657
+ page.apply_replacements({
658
+ "{{NAME}}": "John Doe",
659
+ })
639
660
  ```
640
661
  """
662
+ replacement_list = _dict_to_replacements(replacements)
641
663
  # noinspection PyProtectedMember
642
664
  return self.root._apply_replacements(
643
- replacements=replacements,
665
+ replacements=replacement_list,
644
666
  page_number=self.page_number,
645
667
  reflow_preset=reflow_preset,
646
668
  )
@@ -2246,7 +2268,7 @@ class PDFDancer:
2246
2268
 
2247
2269
  def apply_replacements(
2248
2270
  self,
2249
- replacements: List[TemplateReplacement],
2271
+ replacements: Dict[str, Union[str, dict]],
2250
2272
  reflow_preset: Optional[ReflowPreset] = None,
2251
2273
  ) -> bool:
2252
2274
  """
@@ -2256,8 +2278,9 @@ class PDFDancer:
2256
2278
  content. All placeholders must be found or the operation fails atomically.
2257
2279
 
2258
2280
  Args:
2259
- replacements: List of TemplateReplacement objects specifying
2260
- placeholder/replacement pairs
2281
+ replacements: Dict mapping placeholder strings to replacement values.
2282
+ - Simple: {"{{NAME}}": "John Doe"}
2283
+ - With options: {"{{NAME}}": {"text": "John", "font": Font(...), "color": Color(...)}}
2261
2284
  reflow_preset: Optional ReflowPreset to control text reflow behavior.
2262
2285
  - BEST_EFFORT: Attempt to reflow, proceed even if imperfect
2263
2286
  - FIT_OR_FAIL: Reflow must succeed or operation fails
@@ -2268,14 +2291,15 @@ class PDFDancer:
2268
2291
 
2269
2292
  Example:
2270
2293
  ```python
2271
- pdf.apply_replacements([
2272
- TemplateReplacement("{{NAME}}", "John Doe"),
2273
- TemplateReplacement("{{DATE}}", "2025-01-15"),
2274
- ])
2294
+ pdf.apply_replacements({
2295
+ "{{NAME}}": "John Doe",
2296
+ "{{DATE}}": "2025-01-15",
2297
+ })
2275
2298
  ```
2276
2299
  """
2300
+ replacement_list = _dict_to_replacements(replacements)
2277
2301
  return self._apply_replacements(
2278
- replacements=replacements,
2302
+ replacements=replacement_list,
2279
2303
  page_number=None,
2280
2304
  reflow_preset=reflow_preset,
2281
2305
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdfdancer-client-python
3
- Version: 0.3.6
3
+ Version: 0.3.8
4
4
  Summary: Python client for PDFDancer API
5
5
  Author-email: "The Famous Cat Ltd." <hi@thefamouscat.com>
6
6
  License:
@@ -239,9 +239,9 @@ Dynamic: license-file
239
239
 
240
240
  # PDFDancer Python Client
241
241
 
242
- ![PDFDancer logo](media/logo-orange-60h.webp)
242
+ ![PDFDancer logo](media/logo-silver-60h.webp)
243
243
 
244
- **Stop fighting PDFs. Start editing them.**
244
+ ## PDF used to be read-only. We fixed that.
245
245
 
246
246
  Edit text in real-world PDFs—even ones you didn't create. Move images, reposition headers, and change fonts with
247
247
  pixel-perfect control from Python. The same API is also available for TypeScript and Java.
@@ -665,9 +665,17 @@ Contributions are welcome via pull request. Please:
665
665
  4. Follow existing code style and patterns
666
666
  5. Update documentation as needed
667
667
 
668
+ ## Helpful links
669
+
670
+ - [API documentation](https://docs.pdfdancer.com?utm_source=github&utm_medium=readme&utm_campaign=pdfdancer-python)
671
+ - [Product overview](https://www.pdfdancer.com?utm_source=github&utm_medium=readme&utm_campaign=pdfdancer-python)
672
+ - [PyPI](https://pypi.org/project/pdfdancer-client-python/)
673
+ - [Changelog](https://www.pdfdancer.com/changelog/?utm_source=github&utm_medium=readme&utm_campaign=pdfdancer-python)
674
+ - [Status](https://status.pdfdancer.com?utm_source=github&utm_medium=readme&utm_campaign=pdfdancer-python)
675
+
668
676
  ## Related SDKs
669
677
 
670
- - TypeScript client: https://github.com/MenschMachine/pdfdancer-client-js
678
+ - TypeScript client: https://github.com/MenschMachine/pdfdancer-client-typescript
671
679
  - Java client: https://github.com/MenschMachine/pdfdancer-client-java
672
680
 
673
681
  ## License
@@ -18,6 +18,8 @@ docs/api-schemas/v0.yml
18
18
  docs/api-schemas/v1.yml
19
19
  media/logo-orange-512h.webp
20
20
  media/logo-orange-60h.webp
21
+ media/logo-silver-512h.webp
22
+ media/logo-silver-60h.webp
21
23
  src/pdfdancer/__init__.py
22
24
  src/pdfdancer/exceptions.py
23
25
  src/pdfdancer/fingerprint.py
@@ -111,7 +111,9 @@ def test_modify_line():
111
111
  line = pdf.page(1).select_text_lines_starting_with(
112
112
  "This is regular Sans text showing alignment and styles."
113
113
  )[0]
114
- result = line.edit().replace(" replaced ").apply()
114
+
115
+ line.edit().replace(" replaced ").apply()
116
+ # TODO assert line.text == " replaced "
115
117
 
116
118
  # Validate replacements
117
119
  assert (
@@ -2,7 +2,7 @@
2
2
 
3
3
  import pytest
4
4
 
5
- from pdfdancer import ReflowPreset, TemplateReplacement, ValidationException
5
+ from pdfdancer import Color, Font, ReflowPreset, ValidationException
6
6
  from pdfdancer.pdfdancer_v1 import PDFDancer
7
7
  from tests.e2e import _require_env_and_fixture
8
8
  from tests.e2e.pdf_assertions import PDFAssertions
@@ -21,9 +21,7 @@ def test_replace_single_template():
21
21
  )
22
22
 
23
23
  # Replace the existing "Showcase" text
24
- result = pdf.apply_replacements([
25
- TemplateReplacement(placeholder="Showcase", text="Replaced"),
26
- ])
24
+ result = pdf.apply_replacements({"Showcase": "Replaced"})
27
25
 
28
26
  assert result is True
29
27
 
@@ -42,10 +40,10 @@ def test_replace_multiple_templates():
42
40
 
43
41
  with PDFDancer.open(pdf_path, token=token, base_url=base_url, timeout=30.0) as pdf:
44
42
  # Replace multiple placeholders
45
- result = pdf.apply_replacements([
46
- TemplateReplacement(placeholder="PDFDancer", text="TestApp"),
47
- TemplateReplacement(placeholder="Engine", text="System"),
48
- ])
43
+ result = pdf.apply_replacements({
44
+ "PDFDancer": "TestApp",
45
+ "Engine": "System",
46
+ })
49
47
 
50
48
  assert result is True
51
49
 
@@ -71,9 +69,7 @@ def test_replace_template_on_specific_page():
71
69
  )
72
70
 
73
71
  # Replace only on page 1
74
- result = pdf.page(1).apply_replacements([
75
- TemplateReplacement(placeholder="Showcase", text="PageOneOnly"),
76
- ])
72
+ result = pdf.page(1).apply_replacements({"Showcase": "PageOneOnly"})
77
73
 
78
74
  assert result is True
79
75
 
@@ -98,7 +94,7 @@ def test_replace_template_with_reflow_best_effort():
98
94
 
99
95
  # Replace with longer text using BEST_EFFORT reflow
100
96
  result = pdf.apply_replacements(
101
- [TemplateReplacement(placeholder="Showcase", text="ThisIsAMuchLongerReplacementText")],
97
+ {"Showcase": "ThisIsAMuchLongerReplacementText"},
102
98
  reflow_preset=ReflowPreset.BEST_EFFORT,
103
99
  )
104
100
 
@@ -125,7 +121,7 @@ def test_replace_template_with_reflow_none():
125
121
 
126
122
  # Replace without reflow
127
123
  result = pdf.apply_replacements(
128
- [TemplateReplacement(placeholder="Showcase", text="NoReflow")],
124
+ {"Showcase": "NoReflow"},
129
125
  reflow_preset=ReflowPreset.NONE,
130
126
  )
131
127
 
@@ -139,40 +135,86 @@ def test_replace_template_with_reflow_none():
139
135
  )
140
136
 
141
137
 
142
- def test_replace_template_empty_list_raises():
143
- """Test that replacing with an empty list raises ValidationException."""
138
+ def test_replace_template_empty_dict_raises():
139
+ """Test that replacing with an empty dict raises ValidationException."""
144
140
  base_url, token, pdf_path = _require_env_and_fixture("Showcase.pdf")
145
141
 
146
142
  with PDFDancer.open(pdf_path, token=token, base_url=base_url, timeout=30.0) as pdf:
147
143
  with pytest.raises(ValidationException):
148
- pdf.apply_replacements([])
144
+ pdf.apply_replacements({})
149
145
 
150
146
 
151
- def test_replace_template_page_level_empty_list_raises():
152
- """Test that page-level replacement with empty list raises ValidationException."""
147
+ def test_replace_template_page_level_empty_dict_raises():
148
+ """Test that page-level replacement with empty dict raises ValidationException."""
153
149
  base_url, token, pdf_path = _require_env_and_fixture("Showcase.pdf")
154
150
 
155
151
  with PDFDancer.open(pdf_path, token=token, base_url=base_url, timeout=30.0) as pdf:
156
152
  with pytest.raises(ValidationException):
157
- pdf.page(1).apply_replacements([])
153
+ pdf.page(1).apply_replacements({})
158
154
 
159
155
 
160
- def test_template_replacement_dataclass():
161
- """Test TemplateReplacement dataclass structure and serialization."""
162
- replacement = TemplateReplacement(
163
- placeholder="{{NAME}}",
164
- text="John Doe",
165
- )
156
+ def test_replace_template_with_font():
157
+ """Test template replacement with custom font."""
158
+ base_url, token, pdf_path = _require_env_and_fixture("Showcase.pdf")
159
+
160
+ with PDFDancer.open(pdf_path, token=token, base_url=base_url, timeout=30.0) as pdf:
161
+ result = pdf.apply_replacements({
162
+ "Showcase": {
163
+ "text": "FontTest",
164
+ "font": Font("Helvetica-Bold", 14),
165
+ }
166
+ })
167
+
168
+ assert result is True
169
+
170
+ (
171
+ PDFAssertions(pdf)
172
+ .assert_textline_does_not_exist("Showcase")
173
+ .assert_textline_exists("FontTest")
174
+ )
175
+
176
+
177
+ def test_replace_template_with_color():
178
+ """Test template replacement with custom color."""
179
+ base_url, token, pdf_path = _require_env_and_fixture("Showcase.pdf")
180
+
181
+ with PDFDancer.open(pdf_path, token=token, base_url=base_url, timeout=30.0) as pdf:
182
+ result = pdf.apply_replacements({
183
+ "Showcase": {
184
+ "text": "ColorTest",
185
+ "color": Color(255, 0, 0),
186
+ }
187
+ })
188
+
189
+ assert result is True
190
+
191
+ (
192
+ PDFAssertions(pdf)
193
+ .assert_textline_does_not_exist("Showcase")
194
+ .assert_textline_exists("ColorTest")
195
+ )
196
+
197
+
198
+ def test_replace_template_with_font_and_color():
199
+ """Test template replacement with both font and color."""
200
+ base_url, token, pdf_path = _require_env_and_fixture("Showcase.pdf")
201
+
202
+ with PDFDancer.open(pdf_path, token=token, base_url=base_url, timeout=30.0) as pdf:
203
+ result = pdf.apply_replacements({
204
+ "Showcase": {
205
+ "text": "StyledText",
206
+ "font": Font("Helvetica-Bold", 16),
207
+ "color": Color(0, 100, 0),
208
+ }
209
+ })
166
210
 
167
- assert replacement.placeholder == "{{NAME}}"
168
- assert replacement.text == "John Doe"
211
+ assert result is True
169
212
 
170
- # Test to_dict
171
- d = replacement.to_dict()
172
- assert d == {
173
- "placeholder": "{{NAME}}",
174
- "text": "John Doe",
175
- }
213
+ (
214
+ PDFAssertions(pdf)
215
+ .assert_textline_does_not_exist("Showcase")
216
+ .assert_textline_exists("StyledText")
217
+ )
176
218
 
177
219
 
178
220
  def test_reflow_preset_values():