Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/pdfrest/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@
JpegColorModel,
OcrLanguage,
PdfAddTextObject,
PdfAType,
PdfAOutputType,
PdfConversionCompression,
PdfConversionDownsample,
PdfConversionLocale,
Expand Down Expand Up @@ -5289,7 +5289,7 @@ def convert_to_pdfa(
self,
file: PdfRestFile | Sequence[PdfRestFile],
*,
output_type: PdfAType,
output_type: PdfAOutputType,
output: str | None = None,
rasterize_if_errors_encountered: bool = False,
extra_query: Query | None = None,
Expand Down Expand Up @@ -8355,7 +8355,7 @@ async def convert_to_pdfa(
self,
file: PdfRestFile | Sequence[PdfRestFile],
*,
output_type: PdfAType,
output_type: PdfAOutputType,
output: str | None = None,
rasterize_if_errors_encountered: bool = False,
extra_query: Query | None = None,
Expand Down
22 changes: 21 additions & 1 deletion src/pdfrest/models/_internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@
from .public import PdfRestFile, PdfRestFileID

PdfConvertColorProfile = PdfPresetColorProfile | Literal["custom"]
PDFA_OUTPUT_TYPES: tuple[PdfAType, ...] = (
"PDF/A-1b",
"PDF/A-2b",
"PDF/A-2u",
"PDF/A-3b",
"PDF/A-3u",
)
PDFA_OUTPUT_TYPE_MAP: dict[str, PdfAType] = {
output_type.casefold(): output_type for output_type in PDFA_OUTPUT_TYPES
}


def _ensure_list(value: Any) -> Any:
Expand Down Expand Up @@ -188,6 +198,12 @@ def _bool_to_true_false(value: Any) -> Any:
return value


def _normalize_pdfa_output_type(value: Any) -> Any:
if not isinstance(value, str):
return value
return PDFA_OUTPUT_TYPE_MAP.get(value.casefold(), value)


def _serialize_page_ranges(value: list[str | int | tuple[str | int, ...]]) -> str:
def join_tuple(value: str | int | tuple[str | int, ...]) -> str:
if isinstance(value, tuple):
Expand Down Expand Up @@ -1344,7 +1360,11 @@ class PdfToPdfaPayload(BaseModel):
),
PlainSerializer(_serialize_as_first_file_id),
]
output_type: Annotated[PdfAType, Field(serialization_alias="output_type")]
output_type: Annotated[
PdfAType,
Field(serialization_alias="output_type"),
BeforeValidator(_normalize_pdfa_output_type),
]
output: Annotated[
str | None,
Field(serialization_alias="output", min_length=1, default=None),
Expand Down
2 changes: 2 additions & 0 deletions src/pdfrest/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
JpegColorModel,
OcrLanguage,
PdfAddTextObject,
PdfAOutputType,
PdfAType,
PdfCMYKColor,
PdfColorProfile,
Expand Down Expand Up @@ -69,6 +70,7 @@
"HtmlWebLayout",
"JpegColorModel",
"OcrLanguage",
"PdfAOutputType",
"PdfAType",
"PdfAddTextObject",
"PdfCMYKColor",
Expand Down
6 changes: 5 additions & 1 deletion src/pdfrest/types/public.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"HtmlWebLayout",
"JpegColorModel",
"OcrLanguage",
"PdfAOutputType",
"PdfAType",
"PdfAddTextObject",
"PdfCMYKColor",
Expand Down Expand Up @@ -323,8 +324,11 @@ class PdfPemCredentials(TypedDict):
#: [AsyncPdfRestClient.sign_pdf][pdfrest.AsyncPdfRestClient.sign_pdf].
PdfSignatureCredentials = PdfPfxCredentials | PdfPemCredentials

#: PDF/A conformance targets accepted by ``convert_to_pdfa``.
#: Canonical PDF/A conformance targets accepted by ``convert_to_pdfa``.
PdfAType = Literal["PDF/A-1b", "PDF/A-2b", "PDF/A-2u", "PDF/A-3b", "PDF/A-3u"]
#: Caller-facing ``convert_to_pdfa`` input type. Values are matched
#: case-insensitively and normalized to [PdfAType][pdfrest.types.PdfAType].
PdfAOutputType = PdfAType | str
#: PDF/X conformance targets accepted by ``convert_to_pdfx``.
PdfXType = Literal["PDF/X-1a", "PDF/X-3", "PDF/X-4", "PDF/X-6"]
#: Granularity modes for extracted full text payloads.
Expand Down
47 changes: 45 additions & 2 deletions tests/live/test_live_convert_to_pdfa.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,28 @@ def test_live_convert_to_pdfa_with_rasterize_option(
assert str(response.input_id) == str(uploaded_pdf_for_pdfa.id)


def test_live_convert_to_pdfa_accepts_lowercase_output_type(
pdfrest_api_key: str,
pdfrest_live_base_url: str,
uploaded_pdf_for_pdfa: PdfRestFile,
) -> None:
with PdfRestClient(
api_key=pdfrest_api_key,
base_url=pdfrest_live_base_url,
) as client:
response = client.convert_to_pdfa(
uploaded_pdf_for_pdfa,
output_type="pdf/a-2b",
output="pdfa-lowercase",
)

assert response.output_files
output_file = response.output_file
assert output_file.name.startswith("pdfa-lowercase")
assert output_file.type == "application/pdf"
assert str(response.input_id) == str(uploaded_pdf_for_pdfa.id)


@pytest.mark.asyncio
async def test_live_async_convert_to_pdfa_with_rasterize_option(
pdfrest_api_key: str,
Expand All @@ -125,12 +147,34 @@ async def test_live_async_convert_to_pdfa_with_rasterize_option(
assert str(response.input_id) == str(uploaded_pdf_for_pdfa.id)


@pytest.mark.asyncio
async def test_live_async_convert_to_pdfa_accepts_lowercase_output_type(
pdfrest_api_key: str,
pdfrest_live_base_url: str,
uploaded_pdf_for_pdfa: PdfRestFile,
) -> None:
async with AsyncPdfRestClient(
api_key=pdfrest_api_key,
base_url=pdfrest_live_base_url,
) as client:
response = await client.convert_to_pdfa(
uploaded_pdf_for_pdfa,
output_type="pdf/a-2b",
output="async-pdfa-lowercase",
)

assert response.output_files
output_file = response.output_file
assert output_file.name.startswith("async-pdfa-lowercase")
assert output_file.type == "application/pdf"
assert str(response.input_id) == str(uploaded_pdf_for_pdfa.id)


@pytest.mark.parametrize(
"invalid_output_type",
[
pytest.param("PDF/A-0", id="pdfa-0"),
pytest.param("PDF/A-99", id="pdfa-99"),
pytest.param("pdf/a-2b", id="lowercase"),
],
)
def test_live_convert_to_pdfa_invalid_output_type(
Expand Down Expand Up @@ -159,7 +203,6 @@ def test_live_convert_to_pdfa_invalid_output_type(
[
pytest.param("PDF/A-0", id="pdfa-0"),
pytest.param("PDF/A-99", id="pdfa-99"),
pytest.param("pdf/a-2b", id="lowercase"),
],
)
async def test_live_async_convert_to_pdfa_invalid_output_type(
Expand Down
85 changes: 84 additions & 1 deletion tests/live/test_live_sign_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
)

INVALID_LOGO_OPACITY_VALUES = (
pytest.param(0.0, id="zero"),
pytest.param(-0.1, id="below-min"),
pytest.param(1.1, id="above-max"),
)

Expand Down Expand Up @@ -501,6 +501,7 @@ def test_live_sign_pdf_invalid_logo_opacity(
"signature_configuration": _to_json_string(
{
"type": "new",
"name": "live-invalid-logo-opacity",
"location": make_signature_location(),
"logo_opacity": invalid_logo_opacity,
}
Expand All @@ -509,6 +510,46 @@ def test_live_sign_pdf_invalid_logo_opacity(
)


def test_live_sign_pdf_logo_opacity_zero_is_allowed(
pdfrest_api_key: str,
pdfrest_live_base_url: str,
uploaded_pdf_for_signing: PdfRestFile,
uploaded_pfx_credential: PdfRestFile,
uploaded_passphrase: PdfRestFile,
) -> None:
with PdfRestClient(
api_key=pdfrest_api_key,
base_url=pdfrest_live_base_url,
) as client:
response = client.sign_pdf(
uploaded_pdf_for_signing,
signature_configuration={
"type": "new",
"name": "live-logo-opacity-zero",
"location": make_signature_location(),
},
credentials={
"pfx": uploaded_pfx_credential,
"passphrase": uploaded_passphrase,
},
extra_body={
"signature_configuration": _to_json_string(
{
"type": "new",
"name": "live-logo-opacity-zero",
"location": make_signature_location(),
"logo_opacity": 0.0,
}
)
},
output="live-logo-opacity-zero",
)

assert response.output_file.type == "application/pdf"
assert response.output_file.name == "live-logo-opacity-zero.pdf"
assert str(uploaded_pdf_for_signing.id) in response.input_ids


@pytest.mark.asyncio
async def test_live_async_sign_pdf_invalid_signature_configuration(
pdfrest_api_key: str,
Expand Down Expand Up @@ -601,9 +642,51 @@ async def test_live_async_sign_pdf_invalid_logo_opacity(
"signature_configuration": _to_json_string(
{
"type": "new",
"name": "live-async-invalid-logo-opacity",
"location": make_signature_location(),
"logo_opacity": invalid_logo_opacity,
}
)
},
)


@pytest.mark.asyncio
async def test_live_async_sign_pdf_logo_opacity_zero_is_allowed(
pdfrest_api_key: str,
pdfrest_live_base_url: str,
uploaded_pdf_for_signing: PdfRestFile,
uploaded_pfx_credential: PdfRestFile,
uploaded_passphrase: PdfRestFile,
) -> None:
async with AsyncPdfRestClient(
api_key=pdfrest_api_key,
base_url=pdfrest_live_base_url,
) as client:
response = await client.sign_pdf(
uploaded_pdf_for_signing,
signature_configuration={
"type": "new",
"name": "live-async-logo-opacity-zero",
"location": make_signature_location(),
},
credentials={
"pfx": uploaded_pfx_credential,
"passphrase": uploaded_passphrase,
},
extra_body={
"signature_configuration": _to_json_string(
{
"type": "new",
"name": "live-async-logo-opacity-zero",
"location": make_signature_location(),
"logo_opacity": 0.0,
}
)
},
output="live-async-logo-opacity-zero",
)

assert response.output_file.type == "application/pdf"
assert response.output_file.name == "live-async-logo-opacity-zero.pdf"
assert str(uploaded_pdf_for_signing.id) in response.input_ids
4 changes: 2 additions & 2 deletions tests/test_convert_to_pdfa.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ def handler(request: httpx.Request) -> httpx.Response:
with PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client:
response = client.convert_to_pdfa(
input_file,
output_type="PDF/A-3b",
output_type="pdf/a-3b",
output="custom",
rasterize_if_errors_encountered="on",
extra_query={"trace": "true"},
Expand Down Expand Up @@ -251,7 +251,7 @@ def handler(request: httpx.Request) -> httpx.Response:
async with AsyncPdfRestClient(api_key=ASYNC_API_KEY, transport=transport) as client:
response = await client.convert_to_pdfa(
input_file,
output_type="PDF/A-2u",
output_type="pdf/a-2u",
output="async-custom",
rasterize_if_errors_encountered="off",
extra_query={"trace": "async"},
Expand Down
Loading