-
Notifications
You must be signed in to change notification settings - Fork 3
PDFCLOUD-5556 Add client methods for digital signatures in PDF #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -170,6 +170,13 @@ def _serialize_text_objects(value: list[BaseModel]) -> str: | |
| return to_json(payload).decode() | ||
|
|
||
|
|
||
| def _serialize_signature_configuration( | ||
| value: _PdfSignatureConfigurationModel, | ||
| ) -> str: | ||
| payload = value.model_dump(mode="json", exclude_none=True) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use model_dump_json instead of model_dump followed by json.dumps. |
||
| return to_json(payload).decode() | ||
|
|
||
|
|
||
| def _allowed_mime_types( | ||
| allowed_mime_types: str, *more_allowed_mime_types: str, error_msg: str | None | ||
| ) -> Callable[[Any], Any]: | ||
|
|
@@ -603,6 +610,41 @@ class PdfPresetRedactionModel(BaseModel): | |
| value: PdfRedactionPreset | ||
|
|
||
|
|
||
| class _PdfSignaturePointModel(BaseModel): | ||
| x: str | int | float | ||
| y: str | int | float | ||
|
Comment on lines
+614
to
+615
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use float. |
||
|
|
||
|
|
||
| class _PdfSignatureLocationModel(BaseModel): | ||
| bottom_left: _PdfSignaturePointModel | ||
| top_right: _PdfSignaturePointModel | ||
| page: str | int | ||
|
|
||
|
|
||
| class _PdfSignatureDisplayModel(BaseModel): | ||
| include_distinguished_name: bool | None = None | ||
| include_datetime: bool | None = None | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these bools really optional? Optional bool can be confusing...and the TypedDict doesn't have optional values. This is the part that validates, and makes sure that at runtime, the TypedDict got passed the correct values. type checking -> IDE time |
||
| contact: str | None = None | ||
| location: str | None = None | ||
| name: str | None = None | ||
| reason: str | None = None | ||
|
Comment on lines
+627
to
+630
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to the types on
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are meant to be optional. If they are not wanted, then they should be omitted from the request. |
||
|
|
||
|
|
||
| class _PdfSignatureConfigurationModel(BaseModel): | ||
| type: Literal["new"] | ||
| name: str | None = None | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But the public interface says this isn't optional...
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's required. |
||
| logo_opacity: Annotated[float | None, Field(ge=0, le=1, default=None)] = None | ||
| location: _PdfSignatureLocationModel | None = None | ||
| display: _PdfSignatureDisplayModel | None = None | ||
|
|
||
| @model_validator(mode="after") | ||
| def _validate_location_for_new_type(self) -> _PdfSignatureConfigurationModel: | ||
| if self.type == "new" and self.location is None: | ||
| msg = "Missing location information for a new digital signature field." | ||
| raise ValueError(msg) | ||
| return self | ||
|
|
||
|
|
||
| _PdfRedactionVariant = Annotated[ | ||
| PdfLiteralRedactionModel | PdfRegexRedactionModel | PdfPresetRedactionModel, | ||
| Field(discriminator="type"), | ||
|
|
@@ -986,6 +1028,158 @@ class PdfFlattenFormsPayload(BaseModel): | |
| ] = None | ||
|
|
||
|
|
||
| class PdfSignPayload(BaseModel): | ||
| """Adapt caller options into a pdfRest-ready sign request payload.""" | ||
|
|
||
| files: Annotated[ | ||
| list[PdfRestFile], | ||
| Field( | ||
| min_length=1, | ||
| max_length=1, | ||
| validation_alias=AliasChoices("file", "files"), | ||
| serialization_alias="id", | ||
| ), | ||
| BeforeValidator(_ensure_list), | ||
| AfterValidator( | ||
| _allowed_mime_types("application/pdf", error_msg="Must be a PDF file") | ||
| ), | ||
| PlainSerializer(_serialize_as_first_file_id), | ||
| ] | ||
| signature_configuration: Annotated[ | ||
| _PdfSignatureConfigurationModel, | ||
| Field(serialization_alias="signature_configuration"), | ||
| PlainSerializer(_serialize_signature_configuration), | ||
| ] | ||
| pfx_credential: Annotated[ | ||
| list[PdfRestFile] | None, | ||
| Field( | ||
| default=None, | ||
| min_length=1, | ||
| max_length=1, | ||
| validation_alias=AliasChoices("pfx", "pfx_credential"), | ||
| serialization_alias="pfx_credential_id", | ||
| ), | ||
| BeforeValidator(_ensure_list), | ||
| BeforeValidator( | ||
| _allowed_mime_types( | ||
| "application/x-pkcs12", | ||
| "application/pkcs12", | ||
| "application/octet-stream", | ||
| error_msg="PFX credentials must be a .pfx or .p12 file", | ||
| ) | ||
| ), | ||
| PlainSerializer(_serialize_as_first_file_id), | ||
| ] = None | ||
| pfx_passphrase: Annotated[ | ||
| list[PdfRestFile] | None, | ||
| Field( | ||
| default=None, | ||
| min_length=1, | ||
| max_length=1, | ||
| validation_alias=AliasChoices("pfx_passphrase", "passphrase"), | ||
| serialization_alias="pfx_passphrase_id", | ||
| ), | ||
| BeforeValidator(_ensure_list), | ||
| BeforeValidator( | ||
| _allowed_mime_types( | ||
| "text/plain", | ||
| "application/octet-stream", | ||
| error_msg="PFX passphrase must be a text file", | ||
| ) | ||
| ), | ||
| PlainSerializer(_serialize_as_first_file_id), | ||
| ] = None | ||
| certificate: Annotated[ | ||
| list[PdfRestFile] | None, | ||
| Field( | ||
| default=None, | ||
| min_length=1, | ||
| max_length=1, | ||
| validation_alias=AliasChoices("certificate", "cert"), | ||
| serialization_alias="certificate_id", | ||
| ), | ||
| BeforeValidator(_ensure_list), | ||
| BeforeValidator( | ||
| _allowed_mime_types( | ||
| "application/pkix-cert", | ||
| "application/x-x509-ca-cert", | ||
| "application/x-pem-file", | ||
| "application/octet-stream", | ||
| error_msg="Certificate must be a .pem or .der file", | ||
| ) | ||
| ), | ||
| PlainSerializer(_serialize_as_first_file_id), | ||
| ] = None | ||
| private_key: Annotated[ | ||
| list[PdfRestFile] | None, | ||
| Field( | ||
| default=None, | ||
| min_length=1, | ||
| max_length=1, | ||
| validation_alias=AliasChoices("private_key", "key"), | ||
| serialization_alias="private_key_id", | ||
| ), | ||
| BeforeValidator(_ensure_list), | ||
| BeforeValidator( | ||
| _allowed_mime_types( | ||
| "application/pkcs8", | ||
| "application/x-pem-file", | ||
| "application/octet-stream", | ||
| error_msg="Private key must be a .pem or .der file", | ||
| ) | ||
| ), | ||
| PlainSerializer(_serialize_as_first_file_id), | ||
| ] = None | ||
| logo: Annotated[ | ||
| list[PdfRestFile] | None, | ||
| Field( | ||
| default=None, | ||
| min_length=1, | ||
| max_length=1, | ||
| validation_alias=AliasChoices("logo", "logos"), | ||
| serialization_alias="logo_id", | ||
| ), | ||
| BeforeValidator(_ensure_list), | ||
| BeforeValidator( | ||
| _allowed_mime_types( | ||
| "image/jpeg", | ||
| "image/png", | ||
| "image/tiff", | ||
| "image/bmp", | ||
| error_msg="Logo must be an image file", | ||
| ) | ||
| ), | ||
| PlainSerializer(_serialize_as_first_file_id), | ||
| ] = None | ||
| output: Annotated[ | ||
| str | None, | ||
| Field(serialization_alias="output", min_length=1, default=None), | ||
| AfterValidator(_validate_output_prefix), | ||
| ] = None | ||
|
|
||
| @model_validator(mode="after") | ||
| def _validate_credentials(self) -> PdfSignPayload: | ||
| has_pfx = self.pfx_credential is not None or self.pfx_passphrase is not None | ||
| has_pem = self.certificate is not None or self.private_key is not None | ||
|
|
||
| if has_pfx and has_pem: | ||
| msg = "Provide either PFX credentials (pfx + passphrase) or certificate/private_key, not both." | ||
| raise ValueError(msg) | ||
| if has_pfx: | ||
| if not self.pfx_credential or not self.pfx_passphrase: | ||
| msg = "Both pfx and passphrase are required when supplying PFX credentials." | ||
| raise ValueError(msg) | ||
| elif has_pem: | ||
| if not self.certificate or not self.private_key: | ||
| msg = "Both certificate and private_key are required when supplying PEM/DER credentials." | ||
| raise ValueError(msg) | ||
| else: | ||
| msg = "Either PFX credentials (pfx + passphrase) or certificate/private_key credentials are required." | ||
| raise ValueError(msg) | ||
|
|
||
| return self | ||
|
|
||
|
|
||
| class PdfCompressPayload(BaseModel): | ||
| """Adapt caller options into a pdfRest-ready compress request payload.""" | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,10 @@ | |
| "PdfRedactionPreset", | ||
| "PdfRedactionType", | ||
| "PdfRestriction", | ||
| "PdfSignatureConfiguration", | ||
| "PdfSignatureCredentials", | ||
| "PdfSignatureDisplay", | ||
| "PdfSignatureLocation", | ||
| "PdfXType", | ||
| "PngColorModel", | ||
| "SummaryFormat", | ||
|
|
@@ -136,6 +140,47 @@ class PdfMergeSource(TypedDict, total=False): | |
|
|
||
| PdfMergeInput = PdfRestFile | PdfMergeSource | tuple[PdfRestFile, PdfPageSelection] | ||
|
|
||
|
|
||
| class PdfSignaturePoint(TypedDict): | ||
| x: str | int | float | ||
| y: str | int | float | ||
|
Comment on lines
+145
to
+146
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use float as a type here. |
||
|
|
||
|
|
||
| class PdfSignatureLocation(TypedDict): | ||
| bottom_left: Required[PdfSignaturePoint] | ||
| top_right: Required[PdfSignaturePoint] | ||
| page: Required[str | int] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering...can this still be typed...maybe a Pydantic annotation won't help on the int but probably that str is a Literal really. |
||
|
|
||
|
|
||
| class PdfSignatureDisplay(TypedDict, total=False): | ||
| include_distinguished_name: bool | ||
| include_datetime: bool | ||
| contact: str | ||
| location: str | ||
| name: str | ||
| reason: str | ||
|
|
||
|
|
||
| class PdfSignatureConfiguration(TypedDict, total=False): | ||
| type: Required[Literal["new"]] | ||
| location: Required[PdfSignatureLocation] | ||
| name: str | ||
| logo_opacity: float | ||
| display: PdfSignatureDisplay | ||
|
|
||
|
|
||
| class PdfPfxCredentials(TypedDict): | ||
| pfx: Required[PdfRestFile] | ||
| passphrase: Required[PdfRestFile] | ||
|
|
||
|
|
||
| class PdfPemCredentials(TypedDict): | ||
| certificate: Required[PdfRestFile] | ||
| private_key: Required[PdfRestFile] | ||
|
|
||
|
|
||
| PdfSignatureCredentials = PdfPfxCredentials | PdfPemCredentials | ||
|
|
||
| PdfAType = Literal["PDF/A-1b", "PDF/A-2b", "PDF/A-2u", "PDF/A-3b", "PDF/A-3u"] | ||
| PdfXType = Literal["PDF/X-1a", "PDF/X-3", "PDF/X-4", "PDF/X-6"] | ||
| ExtractTextGranularity = Literal["off", "by_page", "document"] | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since I was on the topic of "things we split into separate functions for clarity", pdfAssistant has
...should that idea be surfaced here? Just trying to keep the interface grounded.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is what I have now done with Watermark PDF and what I'm currently working on for
/pdf.