Skip to content
Open
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
3 changes: 3 additions & 0 deletions docs/api-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ Use this group for password protection, permissions, signatures, and standards.
- Archival/print conformance:
[convert_to_pdfa][pdfrest.PdfRestClient.convert_to_pdfa],
[convert_to_pdfx][pdfrest.PdfRestClient.convert_to_pdfx]
- Electronic invoices:
[create_zugferd_pdf][pdfrest.PdfRestClient.create_zugferd_pdf]

## PDF cleanup and rendering normalization

Expand All @@ -130,6 +132,7 @@ Use this group when the user starts with non-PDF content or needs downstream
formats.

- Into PDF:
[create_zugferd_pdf][pdfrest.PdfRestClient.create_zugferd_pdf],
[convert_office_to_pdf][pdfrest.PdfRestClient.convert_office_to_pdf],
[convert_postscript_to_pdf][pdfrest.PdfRestClient.convert_postscript_to_pdf],
[convert_email_to_pdf][pdfrest.PdfRestClient.convert_email_to_pdf],
Expand Down
2 changes: 2 additions & 0 deletions docs/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ to the API guide and API reference for exact method names and options.
- Apply an uploaded `.joboptions` Distiller profile when converting PostScript
or EPS with `convert_postscript_to_pdf`
- Convert to standards formats (`PDF/A`, `PDF/X`)
- Create validated ZUGFeRD or Factur-X PDF/A-3 invoices from uploaded CII XML
with optional source-PDF, logo, and render controls

### Extraction and Analysis

Expand Down
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ supported interpreter matrix.
synchronously convert selected PDF pages and verify the PostScript output.
- `examples/convert_pdf_to_postscript/async_convert_pdf_to_postscript_example.py`
– asynchronously convert a PDF page and verify the PostScript output.
- `examples/create_zugferd_pdf/create_zugferd_pdf_example.py` – create a
formatted ZUGFeRD PDF/A-3 invoice from uploaded CII XML.
- `examples/delete/delete_example.py` – demonstrate file deletion (sync + async
variants).
- `examples/extract_text/extract_pdf_text_example.py` – run `extract_pdf_text`
Expand Down
72 changes: 72 additions & 0 deletions examples/create_zugferd_pdf/create_zugferd_pdf_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# /// script
# requires-python = ">=3.10"
# dependencies = ["pdfrest", "python-dotenv"]
# ///
"""Create a branded ZUGFeRD PDF/A-3 invoice from CII XML.

This sample demonstrates how to:

1. Upload the checked-in invoice XML resource.
2. Generate a ZUGFeRD PDF with typed appearance and locale options.
3. Download and verify the generated PDF.
4. Print the result metadata and input resource IDs.

Set ``PDFREST_API_KEY`` and optionally ``PDFREST_BASE_URL`` for a self-hosted
deployment, then run from the repository root with
``uv run examples/create_zugferd_pdf/create_zugferd_pdf_example.py``. The
required XML input is included in the repository.
"""

from __future__ import annotations

import os
from pathlib import Path

from dotenv import load_dotenv

from pdfrest import PdfRestClient
from pdfrest.types import PdfZugferdRenderOptions

RESOURCE_DIRECTORY = Path(__file__).resolve().parents[1] / "resources"
INVOICE_PATH = RESOURCE_DIRECTORY / "zugferd-minimum-invoice.xml"


def create_zugferd_pdf() -> None:
"""Upload invoice resources, create the PDF/A-3 invoice, and verify it."""
load_dotenv()
base_url = os.getenv("PDFREST_BASE_URL")
render_options = PdfZugferdRenderOptions(
font="courier",
bold_font="courierbold",
accent_color=(20, 64, 115),
locale="de-DE",
label_language="en",
currency_display="symbol",
date_format="dd.MM.yyyy",
decimal_precision=2,
footer_message="Thank you for your business.",
)

with PdfRestClient(base_url=base_url) as client:
invoice = client.files.create_from_paths(INVOICE_PATH)[0]
response = client.create_zugferd_pdf(
invoice,
render_options=render_options,
output="zugferd-invoice",
)
pdf_bytes = client.files.read_bytes(response.output_file)

if not pdf_bytes.startswith(b"%PDF-"):
msg = "The generated resource is not a PDF."
raise RuntimeError(msg)

output = response.output_file
print(f"Created: {output.name}")
print(f"MIME type: {output.type}")
print(f"Size: {output.size} bytes")
print(f"Input IDs: {', '.join(response.input_ids)}")
print(f"Download URL: {output.url}")


if __name__ == "__main__": # pragma: no cover - manual example
create_zugferd_pdf()
51 changes: 51 additions & 0 deletions examples/resources/zugferd-minimum-invoice.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Reduced from the official ZUGFeRD 2.4 MINIMUM_Rechnung.xml example.
This deterministic fixture exercises the smallest supported invoice profile.
-->
<rsm:CrossIndustryInvoice
xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:factur-x.eu:1p0:minimum</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>471102</ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">20200305</udt:DateTimeString>
</ram:IssueDateTime>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty>
<ram:Name>Lieferant GmbH</ram:Name>
<ram:PostalTradeAddress>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="FC">201/113/40209</ram:ID>
</ram:SpecifiedTaxRegistration>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="VA">DE123456789</ram:ID>
</ram:SpecifiedTaxRegistration>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:Name>Kunden AG Frankreich</ram:Name>
</ram:BuyerTradeParty>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeDelivery/>
<ram:ApplicableHeaderTradeSettlement>
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<ram:TaxBasisTotalAmount>198.00</ram:TaxBasisTotalAmount>
<ram:TaxTotalAmount currencyID="EUR">37.62</ram:TaxTotalAmount>
<ram:GrandTotalAmount>235.62</ram:GrandTotalAmount>
<ram:DuePayableAmount>235.62</ram:DuePayableAmount>
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>
131 changes: 131 additions & 0 deletions src/pdfrest/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
PdfToWordPayload,
PdfUnrestrictPayload,
PdfXfaToAcroformsPayload,
PdfZugferdPayload,
PngPdfRestPayload,
SummarizePdfTextPayload,
TiffPdfRestPayload,
Expand Down Expand Up @@ -178,6 +179,7 @@
PdfStructuredTextTableStyle,
PdfTextColor,
PdfXType,
PdfZugferdRenderOptions,
PngColorModel,
PostScriptLevel,
SummaryFormat,
Expand Down Expand Up @@ -4615,6 +4617,69 @@ def sign_pdf(
timeout=timeout,
)

def create_zugferd_pdf(
self,
invoice: PdfRestFile | Sequence[PdfRestFile],
*,
pdf: PdfRestFile | Sequence[PdfRestFile] | None = None,
logo: PdfRestFile | Sequence[PdfRestFile] | None = None,
render_options: PdfZugferdRenderOptions | None = None,
regenerate_pdf: bool = False,
output: str | None = None,
extra_query: Query | None = None,
extra_headers: AnyMapping | None = None,
extra_body: Body | None = None,
timeout: TimeoutTypes | None = None,
) -> PdfRestFileBasedResponse:
"""Create a validated ZUGFeRD or Factur-X PDF/A-3 invoice.

Args:
invoice: Uploaded CII invoice XML resource.
pdf: Optional uploaded PDF to preserve when its content matches the
invoice XML.
logo: Optional uploaded PNG or JPEG logo for generated or
regenerated visual PDFs.
render_options: Optional appearance and regional-formatting options.
regenerate_pdf: Allow replacement of the supplied PDF when its
content cannot be confirmed against the invoice XML. Requires
``pdf`` when true.
output: Output filename prefix used by pdfRest when creating the
ZUGFeRD PDF.
extra_query: Additional query parameters merged into the request.
extra_headers: Additional HTTP headers merged into the request.
extra_body: Additional request body fields merged into the payload.
timeout: Request timeout override for this call.

Returns:
Validated file-based response containing the generated PDF/A-3 file.

Raises:
PdfRestError: If request execution fails at the client or API layer.
ValidationError: If local payload validation fails before sending.
"""
payload: dict[str, Any] = {
"invoice": invoice,
"regenerate_pdf": regenerate_pdf,
}
if pdf is not None:
payload["pdf"] = pdf
if logo is not None:
payload["logo"] = logo
if render_options is not None:
payload["render_options"] = render_options
if output is not None:
payload["output"] = output

return self._post_file_operation(
endpoint="/zugferd-pdf",
payload=payload,
payload_model=PdfZugferdPayload,
extra_query=extra_query,
extra_headers=extra_headers,
extra_body=extra_body,
timeout=timeout,
)

def blank_pdf(
self,
*,
Expand Down Expand Up @@ -8151,6 +8216,72 @@ async def sign_pdf(
timeout=timeout,
)

async def create_zugferd_pdf(
self,
invoice: PdfRestFile | Sequence[PdfRestFile],
*,
pdf: PdfRestFile | Sequence[PdfRestFile] | None = None,
logo: PdfRestFile | Sequence[PdfRestFile] | None = None,
render_options: PdfZugferdRenderOptions | None = None,
regenerate_pdf: bool = False,
output: str | None = None,
extra_query: Query | None = None,
extra_headers: AnyMapping | None = None,
extra_body: Body | None = None,
timeout: TimeoutTypes | None = None,
) -> PdfRestFileBasedResponse:
"""Asynchronously create a ZUGFeRD or Factur-X PDF/A-3 invoice.

This is the asynchronous variant of
[PdfRestClient.create_zugferd_pdf][pdfrest.PdfRestClient.create_zugferd_pdf].

Args:
invoice: Uploaded CII invoice XML resource.
pdf: Optional uploaded PDF to preserve when its content matches the
invoice XML.
logo: Optional uploaded PNG or JPEG logo for generated or
regenerated visual PDFs.
render_options: Optional appearance and regional-formatting options.
regenerate_pdf: Allow replacement of the supplied PDF when its
content cannot be confirmed against the invoice XML. Requires
``pdf`` when true.
output: Output filename prefix used by pdfRest when creating the
ZUGFeRD PDF.
extra_query: Additional query parameters merged into the request.
extra_headers: Additional HTTP headers merged into the request.
extra_body: Additional request body fields merged into the payload.
timeout: Request timeout override for this call.

Returns:
Validated file-based response containing the generated PDF/A-3 file.

Raises:
PdfRestError: If request execution fails at the client or API layer.
ValidationError: If local payload validation fails before sending.
"""
payload: dict[str, Any] = {
"invoice": invoice,
"regenerate_pdf": regenerate_pdf,
}
if pdf is not None:
payload["pdf"] = pdf
if logo is not None:
payload["logo"] = logo
if render_options is not None:
payload["render_options"] = render_options
if output is not None:
payload["output"] = output

return await self._post_file_operation(
endpoint="/zugferd-pdf",
payload=payload,
payload_model=PdfZugferdPayload,
extra_query=extra_query,
extra_headers=extra_headers,
extra_body=extra_body,
timeout=timeout,
)

async def blank_pdf(
self,
*,
Expand Down
Loading
Loading