📄 PHP Invoice Payment Stamping Engine Architecture: FPDI, Batch Processing, and Original Document Preservation

title: "📄 PHP Invoice Payment Stamping Engine Architecture: FPDI, Batch Processing, and Original Document Preservation"
published: true
description: "A technical overview of a PHP invoice payment stamping engine using FPDI, FPDF, and JSZip, with individual and batch processing, local archiving, and original document preservation."
tags: php, architecture, pdf, automation

cover_image: https://direct_url_to_image.jpg

Use a ratio of 100:42 for best results.

published_at: 2026-11-15 20:41 +0000

Invoice payment stamping interface supporting individual PDF processing and batch invoice selection.

A PDF invoice generally represents a document that has already been generated, transmitted, and potentially archived.

When a payment is received, it may be necessary to produce a paid version of the invoice, including a payment confirmation stamp and the corresponding payment date.

One approach is to modify the existing document directly. Another is to generate a new PDF from the original and overlay the payment information.

The second approach preserves the original document while producing a separate visual payment confirmation.

This article presents the architecture of a PHP invoice payment stamping engine using FPDI and FPDF, with support for individual PDF files, ZIP batch processing, and optional local archiving without a database.

1. Separating the Original Invoice From Its Payment Confirmation

The first architectural principle is to never modify the original file.

An invoice may already be stored in an accounting system, associated with a reference, or integrated into an electronic invoicing workflow.

Directly modifying that document can introduce several problems:

  • Loss of the original version
  • Modification of an already archived document
  • Potential alteration of signatures or metadata
  • Loss of embedded data

The engine therefore separates the source document from the generated document.

Original invoice │ ▼ PDF import │ ▼ Generate a copy │ ▼ Overlay payment stamp │ ▼ Paid invoice 

The original file remains unchanged.

The generated document contains a visual representation of the invoice, supplemented by a red payment confirmation stamp and a payment date.

This separation is particularly important when a PDF contains structured data or other elements that are not visible in its graphical representation.

2. General Engine Architecture

The engine is designed to be deployed directly within the client's environment.

It relies on a protected web interface, a PHP engine, and specialized PDF processing libraries.

No database is required to store documents.

Browser │ ▼ Protected web interface │ ▼ PHP engine │ ├── Authentication ├── File processing ├── ZIP extraction ├── PDF generation └── Optional archiving │ ▼ Download generated result 

Documents are processed temporarily and made available for immediate download.

Depending on the deployment configuration, a stamped copy can also be retained in a dedicated local archive directory.

The engine was designed as an add-on to a Factur-X EN16931 batch invoicing system, but it can operate independently of the main service after installation.

3. Project Structure

The documented project structure separates the web interface, internal engine, dependencies, and storage directories.

invoice-stamper/ │ ├── temp_runtime/ │ └── .htaccess │ ├── dependencies/ ├── paid_archives/ ├── direct_archives/ ├── logs/ │ ├── access.php ├── stamper-engine.php ├── .htaccess ├── LICENCE.md │ └── docs/ └── README_FR.md 

Each component has a distinct responsibility.

Component Responsibility
access.php Public web interface entry point
stamper-engine.php Authentication and PDF processing
temp_runtime/ Temporary processing files
dependencies/ Composer libraries
paid_archives/ Archived invoices from batch mode
direct_archives/ Archived PDFs from direct mode
logs/ Error logs
.htaccess Protection of internal resources

This organization separates the files required to execute the engine from the documents handled during processing.

It also distinguishes temporary files from documents intended for long-term retention.

The public documentation describes this architecture but does not distribute the production files.

4. FPDI and FPDF: Importing and Reconstructing a Document

PDF processing relies on two complementary libraries.

Library Responsibility
FPDI Import pages from an existing PDF
FPDF Generate a new PDF document

FPDI allows pages from an existing PDF to be imported and used as templates within a new document.

FPDF then provides the functionality required to add text, shapes, and other graphical elements.

The processing principle is:

Original PDF │ ▼ FPDI: import pages │ ▼ FPDF: create new document │ ▼ Add payment confirmation │ ▼ Generated PDF 

The operation is not simply a matter of opening a PDF and modifying a few bytes.

The engine reconstructs a new document from the imported page content.

This distinction explains why certain properties of the original PDF may not be preserved in the generated result.

5. Educational Example: Overlaying a Payment Stamp

A simplified example illustrates how FPDI and FPDF can be used together.

<?php require 'vendor/autoload.php'; use setasign\Fpdi\Fpdi; $pdf = new Fpdi(); $pageCount = $pdf->setSourceFile('invoice.pdf'); for ($page = 1; $page <= $pageCount; $page++) { $template = $pdf->importPage($page); $size = $pdf->getTemplateSize($template); $pdf->AddPage( $size['orientation'], [$size['width'], $size['height']] ); $pdf->useTemplate($template); if ($page === 1) { $pdf->SetFont('Helvetica', 'B', 16); $pdf->SetTextColor(200, 0, 0); $pdf->SetXY(15, 15); $pdf->Cell(0, 8, 'PAID'); $pdf->SetFont('Helvetica', '', 10); $pdf->SetXY(15, 25); $pdf->Cell(0, 6, 'Payment date: 2026-11-15'); } } $pdf->Output('F', 'invoice_paid.pdf'); 

This is an educational example. It does not reproduce the production engine, its internal paths, or the exact positioning of its payment stamp.

It illustrates several important operations:

  • Importing each page of the source document
  • Retrieving page dimensions and orientation
  • Reproducing the page in a new document
  • Adding graphical information
  • Saving the result as a separate file

In this example, the payment stamp is applied only to the first page.

A real implementation must consider the document layout to avoid placing the stamp over important invoice information.

The invoice.pdf file is never rewritten. Only invoice_paid.pdf is generated.

6. Preserving Page Dimensions and Orientation

Not every PDF uses an A4 portrait layout.

An invoice may contain multiple pages, different page dimensions, or landscape-oriented content.

It is therefore preferable to retrieve the characteristics of each imported page.

$template = $pdf->importPage($page); $size = $pdf->getTemplateSize($template); $pdf->AddPage( $size['orientation'], [$size['width'], $size['height']] ); 

This approach avoids imposing an arbitrary page format on the generated document.

Stamp positioning introduces another consideration.

A fixed position may work well for an invoice generated by a known invoicing engine but become problematic when processing a document from another system.

An engine that accepts external PDF files must consider several constraints.

Constraint Potential issue
Page format Incorrect positioning or scaling
Orientation Incorrect stamp placement
Existing content Overlaying important information
Page count Stamping an inappropriate page

These considerations illustrate general PDF processing challenges. They do not imply that the documented engine includes an automatic free-space detection algorithm.

7. Two Processing Modes: Direct and Batch

The engine provides two processing modes.

The first handles an individual PDF document.

The second processes a monthly ZIP archive containing multiple invoices.

Payment stamping engine │ ├── Direct mode │ └── Individual PDF │ └── Batch mode ├── Monthly ZIP ├── Invoice list ├── Individual stamping └── Group stamping 

Both modes use the same PDF generation principle, but they handle documents differently.

Direct Mode

The user uploads a PDF, enters a payment date, and downloads the stamped document.

Upload PDF │ ▼ Enter payment date │ ▼ Validation │ ▼ Generate paid PDF │ ▼ Download 

This mode is not limited to invoices generated by the batch invoicing system.

It accepts PDFs from other systems, provided their characteristics are compatible with the PDF processing libraries.

Batch Mode

Batch mode begins with the upload of a monthly ZIP archive.

The engine detects the invoices and presents a list containing information such as the client, invoice reference, and billing period.

The user can then stamp an individual invoice or select multiple documents.

This distinction provides a simple workflow for occasional processing while supporting larger document batches.

8. Processing a Monthly ZIP Archive

Batch processing adds an orchestration layer around the PDF engine.

Monthly ZIP archive │ ▼ Temporary extraction │ ▼ Invoice detection │ ▼ Display invoice list │ ▼ Document selection │ ▼ Generate paid PDFs │ ▼ Build output archive 

The input archive is not necessarily identical to the output archive.

The result contains the selected and stamped documents, organized by client reference.

The documented output structure is:

factures_acquittees.zip │ └── clientRef/ ├── F-2025-001_ACQUITTEE.pdf └── F-2025-002_ACQUITTEE.pdf 

This organization facilitates the transmission of documents to the batch invoicing system.

It also preserves a readable association between each invoice and its client without requiring an additional database.

9. ZIP Extraction and File Security

Extracting a user-supplied archive is a sensitive operation.

An archive can contain unexpected files, relative paths, or very large amounts of compressed data.

For example, a ZIP entry could contain a path such as:

../../outside.php 

Insufficiently controlled extraction could allow a file to be written outside the intended directory. This vulnerability category is commonly known as Zip Slip.

One possible strategy is to validate each entry before extraction.

function isSafeZipEntry(string $name): bool { $name = str_replace('\\', '/', $name); if ( str_starts_with($name, '/') || preg_match('/^[a-zA-Z]:/', $name) ) { return false; } $parts = explode('/', $name); return !in_array('..', $parts, true); } 

This example illustrates basic path validation.

It is not a complete security solution and does not reproduce the internal controls of the engine.

A robust implementation must also consider entry types, symbolic links, file size limits, file counts, and total decompressed size.

The documented engine uses a dedicated temporary directory for archive extraction and PDF generation, with protection against direct web access.

10. Individual Stamping and Multiple Selection

Batch mode supports stamping one invoice at a time or processing several documents with a shared payment date.

From an architectural perspective, individual document processing can be separated from batch orchestration.

Selected invoices │ ▼ Validate payment date │ ▼ For each invoice │ ├── Import PDF ├── Generate payment stamp └── Create output document │ ▼ Prepare result 

A conceptual selection model could look like this:

{ "payment_date": "2026-11-15", "invoices": [ { "reference": "F-2026-001", "client": "CLIENT-A" }, { "reference": "F-2026-002", "client": "CLIENT-A" } ] } 

This JSON is illustrative. It does not represent a public data exchange format used by the engine.

The shared payment date is applied to the selected documents.

In a real application, invoice references received from the browser must be validated on the server to ensure that they correspond to documents accessible within the current batch.

A reference supplied by the client should not be interpreted directly as a filesystem path.

11. Generating ZIP Archives in the Browser

The project also uses JSZip to generate ZIP archives on the client side.

This library allows an archive to be assembled directly in the browser from files that are already available.

An educational example could look like this:

const zip = new JSZip(); const clientFolder = zip.folder("CLIENT-A"); clientFolder.file( "F-2026-001_ACQUITTEE.pdf", firstPdfBlob ); clientFolder.file( "F-2026-002_ACQUITTEE.pdf", secondPdfBlob ); const archive = await zip.generateAsync({ type: "blob" }); 

The resulting archive can then be offered for download:

const url = URL.createObjectURL(archive); const link = document.createElement("a"); link.href = url; link.download = "factures_acquittees.zip"; document.body.appendChild(link); link.click(); link.remove(); setTimeout(() => { URL.revokeObjectURL(url); }, 1000); 

These examples assume that the generated PDF documents are already available as Blob objects.

They illustrate client-side ZIP generation without reproducing the protocol used by the engine to transfer the documents.

This approach separates two responsibilities:

PHP server └── Generate paid PDF documents Browser └── Assemble ZIP archive 

Client-side archive generation does not mean that all PDF processing occurs locally in the browser. The PDF documents are processed by the PHP engine within the client's environment.

JSZip is loaded from a CDN in the documented architecture. This dependency may therefore require network access unless it is hosted locally in a different deployment configuration.

12. Factur-X: Preserving the Original Does Not Mean Preserving XML in the Copy

The engine was originally designed as an add-on to a Factur-X EN16931 batch invoicing system.

This introduces an important technical constraint.

A Factur-X invoice combines two representations:

Original Factur-X invoice │ ├── PDF/A-3 │ └── Human-readable document │ └── Embedded XML └── Structured invoice data 

When an engine imports PDF pages to generate a new document, it may reproduce their appearance without preserving every internal element of the original file.

Original Factur-X invoice │ ▼ Import PDF pages │ ▼ Graphical reconstruction │ ▼ Paid PDF 

The generated document may therefore preserve the invoice's visual appearance without preserving its embedded XML.

It may also no longer satisfy the PDF/A-3 requirements of the source document.

The engine does not guarantee the preservation of embedded Factur-X XML data in reprocessed PDF files.

This limitation must remain explicit.

Document Role
Original Factur-X Invoice and structured data
Generated paid PDF Visual payment confirmation

The stamped PDF should therefore not automatically be presented as a new compliant Factur-X invoice.

Producing a genuinely modified Factur-X document would require preserving or reconstructing the required components, verifying data consistency, and validating the result against the applicable specifications.

That is not the objective of the payment stamping engine presented here.

13. Temporary Files and Local Archiving

The engine distinguishes between two storage categories.

Temporary files are required during ZIP extraction and document generation.

Paid archives contain copies retained when archiving is enabled.

Local storage │ ├── temp_runtime/ │ ├── ZIP extraction │ └── PDF generation │ ├── paid_archives/ │ └── Batch mode documents │ └── direct_archives/ └── Direct mode documents 

Temporary files are deleted after processing.

Optional archives remain separate from this working directory.

This separation prevents documents required for an active operation from being confused with documents intended for long-term retention.

Isolating Processing Jobs

An engine handling multiple documents can benefit from a separate working directory for each processing job.

For example:

$jobId = bin2hex(random_bytes(16)); $workDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'invoice-' . $jobId; if (!mkdir($workDir, 0700)) { throw new RuntimeException( 'Unable to create temporary directory.' ); } 

This example illustrates a generic job isolation strategy.

It does not describe the exact mechanism used within temp_runtime/.

Isolation can reduce filename collisions between concurrent operations and simplify the removal of files associated with a specific job.

Reliable cleanup must also account for processing errors and interrupted executions.

14. Operating Without a Database

The engine does not use a database to store documents.

PDF files and ZIP archives are the primary resources handled by the system.

Inputs ├── Individual PDF └── ZIP archive Processing ├── Temporary files ├── Payment information └── PDF generation Outputs ├── Paid PDF ├── Structured ZIP └── Optional local archive 

This architecture is suitable for a tool whose primary responsibility is transforming existing documents.

It avoids introducing a relational storage layer solely to retain files.

However, operating without a database does not mean operating without state.

Authentication, sessions, temporary files, and active processing jobs still require appropriate management mechanisms.

If the system were extended to provide comprehensive payment tracking, complex searches, or shared transaction history, its storage requirements could change.

The architectural choice therefore depends on the actual responsibility of the engine.

15. Authentication and Internal Resource Protection

The web interface is password-protected.

The documented engine includes several security mechanisms:

  • Protection against brute-force attempts
  • Secure session management
  • Protection against unauthorized file access
  • Payment data validation
  • Temporary file cleanup
  • HTTP response security controls

Separating the public entry point from the internal engine provides another architectural boundary.

Browser │ ▼ access.php │ ▼ Internal engine │ ├── Dependencies ├── Temporary files └── Archives 

The .htaccess rules help prevent direct access to internal resources in a correctly configured Apache environment.

Password protection alone is insufficient if processed files remain directly accessible through their URLs.

Security controls must therefore apply to both the interface and the resources it handles.

Payment Date Validation

A date received from a form must be validated on the server.

A generic PHP example could look like this:

function validatePaymentDate(string $value): bool { $date = DateTimeImmutable::createFromFormat( '!Y-m-d', $value ); return $date !== false && $date->format('Y-m-d') === $value; } 

This validation checks the format and calendar validity of the date.

It does not establish that a payment has actually occurred.

Business rules, such as whether future payment dates are permitted, must be defined separately according to the system's requirements.

16. A Payment Stamp Is Not a Bank Confirmation

The engine generates a document containing a payment confirmation stamp based on a date supplied by the user.

This operation must be distinguished from automated payment verification.

Payment completed │ ▼ User enters payment date │ ▼ Generate visual confirmation 

The engine does not independently establish the existence of a bank transaction.

The stamping operation alone does not perform accounting reconciliation or provide independent confirmation that a payment has been received.

Operation Responsibility
Bank verification Establish whether payment occurred
Reconciliation Associate payment with an invoice
Visual stamping Generate a paid document
Archiving Retain the generated document

This distinction defines the scope of the tool.

The engine transforms a document using supplied payment information. It is not, by itself, a complete financial management system.

17. Dependencies and Runtime Environment

The engine requires PHP 8.0 or later.

The documented requirements include:

Component Purpose
PHP 8.0+ Engine execution
zip ZIP archive processing
mbstring Multibyte string handling
Composer PHP dependency management
FPDI PDF page import
FPDF PDF document generation
JSZip Browser-side ZIP generation

The engine is designed to be installed within the client's environment.

It does not require a database or a permanent connection to the main invoicing service after installation.

PHP dependencies are stored in a dedicated directory.

The public documentation describes the components and operating principles without providing a public installation procedure or production files.

Conclusion

An invoice payment stamping engine involves more than adding red text to a PDF.

It must distinguish the original document from the generated copy, correctly import pages, process individual documents and archives, validate incoming data, and manage the lifecycle of temporary files.

FPDI and FPDF provide a relatively lightweight foundation for PDF reconstruction. JSZip complements batch processing by allowing archives to be assembled in the browser.

Operating without a database remains consistent with an engine specialized in document transformation and optional local archiving.

The Factur-X limitation remains essential: preserving the original invoice does not mean that the stamped copy automatically retains its embedded XML or PDF/A-3 compliance.

The responsibility of the engine is therefore clearly defined: generate a separate visual payment confirmation without modifying the source document.

Technical Documentation

The project architecture, dependencies, processing workflows, and security considerations are documented in the GitHub repository.

The repository provides technical documentation rather than downloadable source code or production files.

View the technical documentation on GitHub

https://palks-studio.com