Metadata-Version: 2.4
Name: pyfcomp
Version: 0.1.1
Summary: Region-based validation of PDF and image forms
Project-URL: Homepage, https://github.com/kentprimrose/pyfcomp
Project-URL: Repository, https://github.com/kentprimrose/pyfcomp
Project-URL: Issues, https://github.com/kentprimrose/pyfcomp/issues
License: MIT
License-File: LICENSE
Keywords: forms,image-comparison,pdf,validation
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Graphics
Requires-Python: >=3.10
Requires-Dist: pillow
Requires-Dist: pixelmatch
Requires-Dist: pymupdf
Requires-Dist: pytesseract>=0.3.10
Description-Content-Type: text/markdown

# pyfcomp

`pyfcomp` validates a completed image or PDF form against a trusted basis form.
You declare the meaningful rectangular regions and whether each one must change,
may change, or must not change. All differences outside declared regions are
ignored.

It accepts paths or document bytes, handles multi-page PDFs, and returns
structured per-page and per-region diagnostics.

## Installation

```bash
pip install pyfcomp
```

Python 3.10 or later is required.

## Quick start

```python
from pyfcomp import ChangeRule, FormRules, Region, validate_forms

rules = FormRules(
    (
        # A signature is required here.
        Region("signature", 0, 0.10, 0.70, 0.30, 0.12, ChangeRule.MUST_CHANGE),
        # This pre-printed value must not be altered.
        Region(
            "account_number",
            0,
            0.55,
            0.20,
            0.25,
            0.06,
            ChangeRule.MUST_NOT_CHANGE,
        ),
    )
)

result = validate_forms("basis.pdf", "candidate.pdf", rules)

if result.valid:
    print("The candidate is valid.")
else:
    print(result.issues)
    for page in result.page_results:
        for region in page.regions:
            if not region.valid:
                print(f"{region.region.name}: {region.changed_ratio:.1%} changed")
```

Inputs may be `str` or `pathlib.Path` filenames, or raw document `bytes`.
Raster images are one-page documents. PDFs are rendered page by page, and the
basis and candidate must have equal page counts.

## Extract form fields

Use `extract_form_fields()` to turn a fillable PDF's widget locations into the
same zero-based, normalized, top-left-origin coordinates used by `Region`:

```python
from pyfcomp import extract_form_fields

for field in extract_form_fields("form.pdf"):
    print(field.name, field.page, field.x, field.y, field.width, field.height)
```

The extractor uses a PDF's native AcroForm widgets when they are present, so
their coordinates are exact. `name` uses the nearby printed label when one is
available and `source_name` preserves the PDF's internal field name. A flat PDF
has no widgets; it is rendered and processed with Tesseract OCR automatically.

### OCR dependency

Flat-PDF extraction requires the [Tesseract OCR](https://tesseract-ocr.github.io/)
executable to be installed and available on `PATH`. The Python package declares
its `pytesseract` binding as a dependency, but it cannot install the Tesseract
executable itself. For example, install it with `brew install tesseract` on
macOS, or your system package manager on Linux.

If you use an existing editable checkout in Jupyter, install the updated Python
dependencies into that notebook's kernel with `%pip install -e .`, then restart
the kernel.

OCR returns field candidates from recognized text and their bounding boxes. Use
an `image_detector` for a form-specific OCR/vision integration when you need
more precise input-area detection or custom naming.

Raster images have no embedded field names or rectangles. Supply an OCR/vision
adapter with `image_detector`; it receives an RGB Pillow image and page `0`, and
returns `FormField` values in normalized coordinates:

```python
from pyfcomp import FormField, extract_form_fields


def detect(image, page):
    # Call the OCR/vision service of your choice, then normalize its pixel box.
    return [FormField("signature", page, 0.10, 0.70, 0.30, 0.12)]


fields = extract_form_fields("scanned-form.png", image_detector=detect)

# Override the built-in Tesseract fallback for a flat PDF.
fields = extract_form_fields("flat-form.pdf", image_detector=detect)
```

## Define regions

```python
Region(name, page, x, y, width, height, rule)
```

`page` is zero-based. The rectangle values are normalized from `0` to `1`, with
the origin at the top-left of the page. Therefore `x=0.5` begins at the
horizontal midpoint regardless of a page's physical size or image resolution.

| Rule | Meaning |
| --- | --- |
| `ChangeRule.MUST_CHANGE` | Invalid unless the region changed enough. |
| `ChangeRule.MAY_CHANGE` | Always valid; metrics are still reported. |
| `ChangeRule.MUST_NOT_CHANGE` | Invalid if the region changed enough. |

Regions may overlap; each is evaluated independently.

## Load JSON rules

Store rules in JSON when they should be configured outside Python:

```json
{
  "regions": [
    {
      "name": "signature",
      "page": 0,
      "x": 0.1,
      "y": 0.7,
      "width": 0.3,
      "height": 0.12,
      "rule": "must_change"
    }
  ]
}
```

```python
from pyfcomp import FormRules, validate_forms

rules = FormRules.from_json_file("rules.json")
result = validate_forms("basis.pdf", "candidate.pdf", rules)
```

Use `FormRules.from_json(...)` for a JSON string or bytes, and `to_json()` to
write Python rules in the same format.

## Tune comparison

The default comparison renders PDFs at 200 DPI. A region counts as changed when
at least 1% of its pixels differ beyond a visual per-pixel tolerance. Configure
these values for your documents:

```python
from pyfcomp import ValidationOptions

options = ValidationOptions(
    dpi=200,
    auto_align=True,
    max_translation_ratio=0.02,
    pixel_threshold=0.1,
    changed_pixel_ratio=0.01,
)

result = validate_forms("basis.pdf", "candidate.pdf", rules, options=options)
```

Candidate pages are resized to the basis raster dimensions. Auto-alignment is
enabled by default to correct small x/y scan or print shifts; set
`auto_align=False` to require exact placement. Rotation, scale, and perspective
correction are not supported.

## Results and diagnostics

`ValidationResult` provides the overall `valid` flag, `issues`, and a
`PageResult` for every matched page. Each `RegionResult` contains its validity,
changed and total pixel counts, changed ratio, and difference bounding box.

Request an in-memory Pillow overlay for every compared region:

```python
result = validate_forms("basis.pdf", "candidate.pdf", rules, include_diagnostics=True)

for page in result.page_results:
    for region in page.regions:
        if region.diagnostic is not None:
            region.diagnostic.save(f"{region.region.name}-diff.png")
```

The library does not write files unless your application saves a diagnostic.

## Errors

Invalid rules raise `InvalidRulesError`. Unreadable or unsupported inputs raise
`UnsupportedDocumentError`. A page-count mismatch returns an invalid result so
batch jobs can report it as a normal validation failure.

## Development

```bash
uv sync --all-groups
uv run pytest --cov=pyfcomp --cov-report=term-missing
uv run ruff check .
uv run ruff format --check .
uv run ty check
```
