Ctrl K
ComunidadGuías…

tests/test_ocr_endtoend.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""OCR end to end: does a scanned statement actually come out readable?

`test_ocr.py` stubs the rasteriser and Tesseract, so it proves the decision
logic and nothing about readability. This is the other half.

The document is generated at test time from the shipped `generic-au.txt`
sample, drawn onto a bitmap and saved as a PDF with NO text layer — which is
what a registry posting scans sends. No binaries in the repository, no
licensing question about somebody else's document, and it tests the layout that
actually ships (decisions.md #98).

It asserts the exact values `generic-au.expected.yaml` records, so a regression
in Tesseract, in the rasterising resolution or in the template breaks it.
Skipped where Tesseract is absent.
"""

from __future__ import annotations

import io

import pytest
from ruamel.yaml import YAML

from app import docformats, ocr, statements

SAMPLES = docformats.BUILTIN_DIR / "samples"
STATEMENTS = docformats.BUILTIN_DIR / "statements"

pytestmark = pytest.mark.skipif(
    not ocr.available(),
    reason="tesseract is not installed here; this runs in the container image",
)


def scanned_pdf(text: str) -> bytes:
    """The text drawn onto a page and saved as an image-only PDF.

    Deliberately a bitmap: `PIL` writing text into a PDF would embed a font and
    produce a text layer, and a PDF with a text layer is the case OCR must NOT
    run on. Nothing here would be exercised.
    """
    from PIL import Image, ImageDraw, ImageFont

    lines = [ln for ln in text.splitlines() if not ln.lstrip().startswith("#")]
    # A4 at 200dpi, which is a common scanner setting and comfortably above
    # what Tesseract needs for 10pt print.
    image = Image.new("RGB", (1654, 2339), "white")
    draw = ImageDraw.Draw(image)
    try:
        font = ImageFont.truetype(
            "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 30)
    except OSError:  # pragma: no cover - depends on the host's fonts
        font = ImageFont.load_default(size=30)
    for number, line in enumerate(lines):
        draw.text((90, 90 + number * 46), line, fill="black", font=font)

    buffer = io.BytesIO()
    image.save(buffer, format="PDF", resolution=200.0)
    return buffer.getvalue()


def sample_names() -> list[str]:
    return sorted(p.stem for p in SAMPLES.glob("*.txt")) if SAMPLES.is_dir() else []


@pytest.mark.slow
@pytest.mark.parametrize("name", sample_names())
def test_a_scan_of_a_shipped_sample_still_reads_its_expected_values(name):
    """The regression this file exists for.

    Generated from whatever is in `app/formats/samples/`, so a contributed
    layout gets a scanned-PDF check for free alongside its text one.
    """
    sample = (SAMPLES / f"{name}.txt").read_text(encoding="utf-8")
    expected = YAML(typ="safe").load((SAMPLES / f"{name}.expected.yaml").read_text(encoding="utf-8"))
    template = docformats.load_statement_template(STATEMENTS / f"{name}.yaml")

    read = statements.extract(scanned_pdf(sample))

    assert read.used_ocr is True, "the generated PDF had a text layer, so nothing was tested"
    assert read.problem == ""
    assert template.matches(read.text), (
        f"{name}: OCR output no longer matches this template's markers.\n"
        f"OCR read:\n{read.text[:600]}")

    values = template.extract(read.text)

    # The SAME comparison the text-based harness makes, imported rather than
    # rewritten — `test_format_samples.py` is the definition, decisions.md #57.
    from test_format_samples import _assert_rows, _comparable

    for key, want in expected.items():
        if key == "rows":
            _assert_rows(values.get("rows"), want, SAMPLES / f"{name}.txt")
            continue
        assert _comparable(values.get(key)) == _comparable(want), (
            f"{name}: a scan of this layout reads {key} as "
            f"{values.get(key)!r}, expected {want!r}.\n\nOCR read:\n{read.text[:600]}")


@pytest.mark.slow
def test_a_pdf_with_a_text_layer_is_not_ocred_even_here(tmp_path):
    """Belt to the unit test's braces, against a real PDF rather than a stub:
    the expensive, lossy path must not run when the exact answer is in the
    file."""
    from PIL import Image, ImageDraw

    # An image PDF with a caption written as real text alongside it would be
    # ideal; simpler and just as decisive is to check the shipped sample text
    # goes straight through when handed over as a text layer.
    image = Image.new("RGB", (400, 200), "white")
    ImageDraw.Draw(image).text((10, 10), "x", fill="black")
    buffer = io.BytesIO()
    image.save(buffer, format="PDF")

    long_text = (SAMPLES / "generic-au.txt").read_text(encoding="utf-8")
    read = statements.extract(buffer.getvalue())
    assert read.used_ocr is True  # this one genuinely has no text layer

    # And the inverse, via the seam the whole decision hangs on.
    assert statements.looks_scanned("") is True
    assert statements.looks_scanned(long_text) is False


def test_the_generated_pdf_really_has_no_text_layer():
    """If PIL ever starts embedding a font, every test above would silently
    stop exercising OCR and keep passing."""
    pdf = scanned_pdf("Payment date 15 March 2026\nNet amount paid $1,042.75\n")

    assert statements._pdf_text(pdf).strip() == ""


def test_there_is_at_least_one_sample_to_check():
    """A parametrised test over an empty list passes loudly quietly."""
    assert sample_names(), "no shipped samples found — has app/formats/samples moved?"