Source code for cadrumo.application.ledger.evidence_textlayer
"""On-host text-layer extraction from in-memory evidence bytes.Runs the required application page-text capability over a resolved:class:`EvidenceInput`'s in-memory bytes, fully on-host. Nothing is written todisk and nothing leaves the machine (sensitive-financial-data-secure-storage-only).This is the cheapest on-host reader and covers text-native PDFs; image evidenceand scan-only PDFs have no usable text layer and must go through the on-hostvision reader instead. The concrete PDF reader is bound by outer composition.This module is the deterministic half of the ingestion pipeline's acquisitionstage: :func:`transcribe_text_layer` projects the extracted pages into a:class:`~cadrumo.application.ledger.document_transcription.DocumentTranscription`, the typed record everylater stage reads instead of the bytes. The projection **transforms nothing**.Reading order is the extractor's page order, and printed forms survive verbatim:``2.420,00`` stays ``2.420,00``, because the grounding stage verifies a candidatevalue by finding its printed form in this text, so normalising here would deletethe evidence that check runs against."""from__future__importannotationsfromimportlib.metadataimportversionfrom...core.document_shapeimportPDF_CONTAINER_SHAPESfrom...core.field_originimportFieldOriginfrom...core.provenance_stampimportLOCAL_TRANSPORT_LABELfrom.document_transcriptionimportDocumentTranscription,TranscriberIdentityfrom.evidence_errorsimportPurchaseInvoiceEvidenceInputErrorfrom.evidence_inputimportEvidenceInputfrom.evidence_textlayer_portsimportEvidenceTextLayerPortsfrom.preconditionsimportLedgerPreconditionCondition,ledger_no_recovery_verdict__all__=["TEXT_LAYER_TRANSCRIBER_NAME","extract_evidence_pages","extract_evidence_text","text_layer_transcriber_identity","transcribe_text_layer",]TEXT_LAYER_TRANSCRIBER_NAME="pdfplumber""""The concrete reader behind the deterministic acquisition path.Names the library that actually maps bytes to text rather than a coarse labellike ``text-layer``: two extractors disagree on reading order and on where aligature or a thousands separator lands, so which one produced a transcriptionis exactly what the provenance stamp exists to record."""def_text_layer_transcriber_revision()->str:"""Return the installed pdfplumber version, the extractor's real revision. Read from the installed distribution rather than restated as a constant. A hand-maintained revision is a stamp that can lie the moment the dependency moves, and the dependency moving is precisely what changes the output. """returnversion("pdfplumber")
[docs]deftext_layer_transcriber_identity()->TranscriberIdentity:"""Return the provenance stamp for the deterministic text-layer read."""returnTranscriberIdentity(origin=FieldOrigin.TEXT_LAYER,name=TEXT_LAYER_TRANSCRIBER_NAME,# Stated rather than defaulted, like every other axis here. This reader# is a deterministic in-process extractor with no transport at all, so# on-host is a fact about it rather than a fallback.transport=LOCAL_TRANSPORT_LABEL,revision=_text_layer_transcriber_revision(),)
[docs]defextract_evidence_pages(evidence:EvidenceInput,*,text_layer_ports:EvidenceTextLayerPorts,)->tuple[str,...]:"""Return the on-host text layer of a PDF ``EvidenceInput``, page by page. Args: evidence: Resolved in-memory evidence bytes. text_layer_ports: Required application capability for reading PDF pages. Returns: One string per page in the document's own page order. A page carrying no text keeps its slot as the empty string, so the tuple length is the page count and the reading order is positional. Raises: PurchaseInvoiceEvidenceInputError: When the evidence is not a PDF, or the PDF has no usable text layer (scan-only / XFA) -- the caller falls back to the on-host vision reader in that case. """ifevidence.document_shapenotinPDF_CONTAINER_SHAPES:raisePurchaseInvoiceEvidenceInputError("evidence has no text layer (not a PDF); use the on-host vision reader",precondition_verdict=ledger_no_recovery_verdict(LedgerPreconditionCondition.EVIDENCE_TEXT_LAYER_AVAILABLE,facts={"pdf_layer_present":False},),)returntext_layer_ports.extract_pages_text(evidence.data)
[docs]defextract_evidence_text(evidence:EvidenceInput,*,text_layer_ports:EvidenceTextLayerPorts,)->str:"""Return the on-host text layer of a PDF ``EvidenceInput`` as one string. Args: evidence: Resolved in-memory evidence bytes. text_layer_ports: Required application capability for reading PDF pages. Returns: The concatenated per-page text of the PDF (empty pages dropped). Raises: PurchaseInvoiceEvidenceInputError: When the evidence is not a PDF, or the PDF has no usable text layer (scan-only / XFA). """return"\n".join(pageforpageinextract_evidence_pages(evidence,text_layer_ports=text_layer_ports)ifpage)
[docs]deftranscribe_text_layer(evidence:EvidenceInput,*,text_layer_ports:EvidenceTextLayerPorts,)->DocumentTranscription:"""Return the acquisition-stage transcription of a text-native PDF. The deterministic acquisition path: it reads, it does not interpret. The text is the extractor's own output joined in page order with nothing rewritten, so every printed form the document carries reaches the grounding stage intact. Args: evidence: Resolved in-memory evidence bytes. text_layer_ports: Required application capability for reading PDF pages. Returns: The transcription, stamped :attr:`~cadrumo.core.field_origin.FieldOrigin.TEXT_LAYER` with the extractor that produced it and content-addressed to the source bytes. Raises: PurchaseInvoiceEvidenceInputError: When the evidence is not a PDF, or the PDF has no usable text layer (scan-only / XFA). """pages=extract_evidence_pages(evidence,text_layer_ports=text_layer_ports)returnDocumentTranscription(text="\n".join(pageforpageinpagesifpage),page_count=len(pages),source_content_sha256=evidence.content_sha256,transcriber=text_layer_transcriber_identity(),)