Skip to content

Commit f072f80

Browse files
sign_pdf: Allow zero logo opacity
- Relax logo_opacity validation from gt=0 to ge=0 so callers can pass 0.0 through the public signature_configuration path. - Update the public signature-configuration type docs to reflect the supported [0, 1] opacity range. - Add sync and async unit coverage for request serialization and update payload bounds tests and live zero-opacity cases to exercise the public argument path instead of extra_body. - Keep zero-opacity support aligned across validation, documentation, unit tests, and live tests. Assisted-by: Codex
1 parent bd80725 commit f072f80

4 files changed

Lines changed: 104 additions & 25 deletions

File tree

src/pdfrest/models/_internal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1021,7 +1021,7 @@ class _PdfSignatureDisplayModel(BaseModel):
10211021
class _PdfSignatureConfigurationModel(BaseModel):
10221022
type: Literal["new", "existing"]
10231023
name: str | None = None
1024-
logo_opacity: Annotated[float | None, Field(gt=0, le=1, default=None)] = None
1024+
logo_opacity: Annotated[float | None, Field(ge=0, le=1, default=None)] = None
10251025
location: _PdfSignatureLocationModel | None = None
10261026
display: _PdfSignatureDisplayModel | None = None
10271027

src/pdfrest/types/public.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ class PdfNewSignatureConfiguration(TypedDict, total=False):
257257
type: Must be ``"new"``.
258258
location: Placement rectangle and page as [PdfSignatureLocation][pdfrest.types.PdfSignatureLocation].
259259
name: Optional name for the signature field.
260-
logo_opacity: Optional logo opacity in the range ``(0, 1]``.
260+
logo_opacity: Optional logo opacity in the range ``[0, 1]``.
261261
display: Optional visible-signature settings as [PdfSignatureDisplay][pdfrest.types.PdfSignatureDisplay].
262262
"""
263263

@@ -275,7 +275,7 @@ class PdfExistingSignatureConfiguration(TypedDict, total=False):
275275
type: Must be ``"existing"``.
276276
location: Optional placement override as [PdfSignatureLocation][pdfrest.types.PdfSignatureLocation].
277277
name: Optional existing signature field name.
278-
logo_opacity: Optional logo opacity in the range ``(0, 1]``.
278+
logo_opacity: Optional logo opacity in the range ``[0, 1]``.
279279
display: Optional visible-signature settings as [PdfSignatureDisplay][pdfrest.types.PdfSignatureDisplay].
280280
"""
281281

tests/live/test_live_sign_pdf.py

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -527,21 +527,12 @@ def test_live_sign_pdf_logo_opacity_zero_is_allowed(
527527
"type": "new",
528528
"name": "live-logo-opacity-zero",
529529
"location": make_signature_location(),
530+
"logo_opacity": 0.0,
530531
},
531532
credentials={
532533
"pfx": uploaded_pfx_credential,
533534
"passphrase": uploaded_passphrase,
534535
},
535-
extra_body={
536-
"signature_configuration": _to_json_string(
537-
{
538-
"type": "new",
539-
"name": "live-logo-opacity-zero",
540-
"location": make_signature_location(),
541-
"logo_opacity": 0.0,
542-
}
543-
)
544-
},
545536
output="live-logo-opacity-zero",
546537
)
547538

@@ -669,21 +660,12 @@ async def test_live_async_sign_pdf_logo_opacity_zero_is_allowed(
669660
"type": "new",
670661
"name": "live-async-logo-opacity-zero",
671662
"location": make_signature_location(),
663+
"logo_opacity": 0.0,
672664
},
673665
credentials={
674666
"pfx": uploaded_pfx_credential,
675667
"passphrase": uploaded_passphrase,
676668
},
677-
extra_body={
678-
"signature_configuration": _to_json_string(
679-
{
680-
"type": "new",
681-
"name": "live-async-logo-opacity-zero",
682-
"location": make_signature_location(),
683-
"logo_opacity": 0.0,
684-
}
685-
)
686-
},
687669
output="live-async-logo-opacity-zero",
688670
)
689671

tests/test_sign_pdf.py

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,54 @@ def handler(request: httpx.Request) -> httpx.Response:
773773
assert seen == {"post": 1, "get": 1}
774774

775775

776+
def test_sign_pdf_allows_zero_logo_opacity_via_public_argument(
777+
monkeypatch: pytest.MonkeyPatch,
778+
) -> None:
779+
monkeypatch.delenv("PDFREST_API_KEY", raising=False)
780+
input_file = make_pdf_file(PdfRestFileID.generate())
781+
pfx_file = make_pfx_file(str(PdfRestFileID.generate()))
782+
passphrase_file = make_passphrase_file(str(PdfRestFileID.generate()))
783+
output_id = str(PdfRestFileID.generate())
784+
785+
def handler(request: httpx.Request) -> httpx.Response:
786+
if request.method == "POST" and request.url.path == "/signed-pdf":
787+
payload = json.loads(request.content.decode("utf-8"))
788+
signature_payload = json.loads(payload["signature_configuration"])
789+
assert signature_payload["logo_opacity"] == pytest.approx(0.0)
790+
assert signature_payload["type"] == "new"
791+
return httpx.Response(
792+
200,
793+
json={"inputId": [input_file.id], "outputId": [output_id]},
794+
)
795+
if request.method == "GET" and request.url.path == f"/resource/{output_id}":
796+
return httpx.Response(
797+
200,
798+
json=build_file_info_payload(
799+
output_id,
800+
"logo-opacity-zero.pdf",
801+
"application/pdf",
802+
),
803+
)
804+
msg = f"Unexpected request {request.method} {request.url}"
805+
raise AssertionError(msg)
806+
807+
transport = httpx.MockTransport(handler)
808+
with PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client:
809+
response = client.sign_pdf(
810+
input_file,
811+
signature_configuration={
812+
"type": "new",
813+
"name": "visible-zero",
814+
"location": make_signature_location(),
815+
"logo_opacity": 0.0,
816+
},
817+
credentials={"pfx": pfx_file, "passphrase": passphrase_file},
818+
)
819+
820+
assert isinstance(response, PdfRestFileBasedResponse)
821+
assert response.output_file.name == "logo-opacity-zero.pdf"
822+
823+
776824
@pytest.mark.asyncio
777825
async def test_async_sign_pdf_request_customization(
778826
monkeypatch: pytest.MonkeyPatch,
@@ -849,6 +897,55 @@ def handler(request: httpx.Request) -> httpx.Response:
849897
assert seen == {"post": 1, "get": 1}
850898

851899

900+
@pytest.mark.asyncio
901+
async def test_async_sign_pdf_allows_zero_logo_opacity_via_public_argument(
902+
monkeypatch: pytest.MonkeyPatch,
903+
) -> None:
904+
monkeypatch.delenv("PDFREST_API_KEY", raising=False)
905+
input_file = make_pdf_file(PdfRestFileID.generate())
906+
pfx_file = make_pfx_file(str(PdfRestFileID.generate()))
907+
passphrase_file = make_passphrase_file(str(PdfRestFileID.generate()))
908+
output_id = str(PdfRestFileID.generate())
909+
910+
def handler(request: httpx.Request) -> httpx.Response:
911+
if request.method == "POST" and request.url.path == "/signed-pdf":
912+
payload = json.loads(request.content.decode("utf-8"))
913+
signature_payload = json.loads(payload["signature_configuration"])
914+
assert signature_payload["logo_opacity"] == pytest.approx(0.0)
915+
assert signature_payload["type"] == "new"
916+
return httpx.Response(
917+
200,
918+
json={"inputId": [input_file.id], "outputId": [output_id]},
919+
)
920+
if request.method == "GET" and request.url.path == f"/resource/{output_id}":
921+
return httpx.Response(
922+
200,
923+
json=build_file_info_payload(
924+
output_id,
925+
"async-logo-opacity-zero.pdf",
926+
"application/pdf",
927+
),
928+
)
929+
msg = f"Unexpected request {request.method} {request.url}"
930+
raise AssertionError(msg)
931+
932+
transport = httpx.MockTransport(handler)
933+
async with AsyncPdfRestClient(api_key=ASYNC_API_KEY, transport=transport) as client:
934+
response = await client.sign_pdf(
935+
input_file,
936+
signature_configuration={
937+
"type": "new",
938+
"name": "async-visible-zero",
939+
"location": make_signature_location(),
940+
"logo_opacity": 0.0,
941+
},
942+
credentials={"pfx": pfx_file, "passphrase": passphrase_file},
943+
)
944+
945+
assert isinstance(response, PdfRestFileBasedResponse)
946+
assert response.output_file.name == "async-logo-opacity-zero.pdf"
947+
948+
852949
def test_sign_payload_requires_location_when_type_new() -> None:
853950
input_file = make_pdf_file(PdfRestFileID.generate())
854951
pfx_file = make_pfx_file(str(PdfRestFileID.generate()))
@@ -988,6 +1085,7 @@ def test_sign_payload_accepts_logo_tuple_sequence() -> None:
9881085
@pytest.mark.parametrize(
9891086
"logo_opacity",
9901087
[
1088+
pytest.param(0.0, id="zero"),
9911089
pytest.param(0.01, id="min"),
9921090
pytest.param(1.0, id="max"),
9931091
],
@@ -1016,7 +1114,6 @@ def test_sign_payload_accepts_logo_opacity_bounds(logo_opacity: float) -> None:
10161114
@pytest.mark.parametrize(
10171115
"invalid_logo_opacity",
10181116
[
1019-
pytest.param(0.0, id="zero"),
10201117
pytest.param(-0.01, id="below-min"),
10211118
pytest.param(1.01, id="above-max"),
10221119
],
@@ -1030,7 +1127,7 @@ def test_sign_payload_rejects_logo_opacity_out_of_bounds(
10301127

10311128
with pytest.raises(
10321129
ValidationError,
1033-
match=r"greater than 0|less than or equal to 1",
1130+
match=r"greater than or equal to 0|less than or equal to 1",
10341131
):
10351132
PdfSignPayload.model_validate(
10361133
{

0 commit comments

Comments
 (0)