tabella-docs 2.0.3__tar.gz → 2.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. {tabella_docs-2.0.3 → tabella_docs-2.0.4}/PKG-INFO +1 -1
  2. {tabella_docs-2.0.3 → tabella_docs-2.0.4}/pyproject.toml +1 -1
  3. {tabella_docs-2.0.3 → tabella_docs-2.0.4}/tabella/__init__.py +1 -1
  4. {tabella_docs-2.0.3 → tabella_docs-2.0.4}/tabella/read.py +10 -4
  5. {tabella_docs-2.0.3 → tabella_docs-2.0.4}/tabella_docs.egg-info/PKG-INFO +1 -1
  6. {tabella_docs-2.0.3 → tabella_docs-2.0.4}/tabella_docs.egg-info/SOURCES.txt +1 -0
  7. tabella_docs-2.0.4/tests/test_read_ocr_fallback.py +49 -0
  8. {tabella_docs-2.0.3 → tabella_docs-2.0.4}/LICENSE +0 -0
  9. {tabella_docs-2.0.3 → tabella_docs-2.0.4}/README.md +0 -0
  10. {tabella_docs-2.0.3 → tabella_docs-2.0.4}/setup.cfg +0 -0
  11. {tabella_docs-2.0.3 → tabella_docs-2.0.4}/tabella/bootstrap.py +0 -0
  12. {tabella_docs-2.0.3 → tabella_docs-2.0.4}/tabella/convert.py +0 -0
  13. {tabella_docs-2.0.3 → tabella_docs-2.0.4}/tabella/create.py +0 -0
  14. {tabella_docs-2.0.3 → tabella_docs-2.0.4}/tabella/utils.py +0 -0
  15. {tabella_docs-2.0.3 → tabella_docs-2.0.4}/tabella_docs.egg-info/dependency_links.txt +0 -0
  16. {tabella_docs-2.0.3 → tabella_docs-2.0.4}/tabella_docs.egg-info/requires.txt +0 -0
  17. {tabella_docs-2.0.3 → tabella_docs-2.0.4}/tabella_docs.egg-info/top_level.txt +0 -0
  18. {tabella_docs-2.0.3 → tabella_docs-2.0.4}/tests/test_bootstrap_paths.py +0 -0
  19. {tabella_docs-2.0.3 → tabella_docs-2.0.4}/tests/test_ensure_venv_win.py +0 -0
  20. {tabella_docs-2.0.3 → tabella_docs-2.0.4}/tests/test_odt_table.py +0 -0
  21. {tabella_docs-2.0.3 → tabella_docs-2.0.4}/tests/test_security_gates.py +0 -0
  22. {tabella_docs-2.0.3 → tabella_docs-2.0.4}/tests/test_text_section_alias.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tabella-docs
3
- Version: 2.0.3
3
+ Version: 2.0.4
4
4
  Summary: Portable document lifecycle suite — create (.docx/.xlsx/.pptx/.pdf/.odt/.ods/.odp), convert (pandoc), and read (anydoc + LiteParse OCR) documents. Agent Plugins v1 compatible.
5
5
  Author-email: D Yiapanis <d@yiapanis.co>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tabella-docs"
7
- version = "2.0.3"
7
+ version = "2.0.4"
8
8
  description = "Portable document lifecycle suite — create (.docx/.xlsx/.pptx/.pdf/.odt/.ods/.odp), convert (pandoc), and read (anydoc + LiteParse OCR) documents. Agent Plugins v1 compatible."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -13,7 +13,7 @@ from pathlib import Path
13
13
 
14
14
  from .bootstrap import resolve_venv_python
15
15
 
16
- __version__ = "2.0.3"
16
+ __version__ = "2.0.4"
17
17
 
18
18
  # ── Configuration ─────────────────────────────────────────────
19
19
 
@@ -104,7 +104,8 @@ def read(path: str, max_chars: Optional[int] = None, extract_images: bool = True
104
104
  1. Plain text → direct file read (fast path)
105
105
  2. Office formats + text PDFs → anydoc (Markdown output)
106
106
  3. Images → LiteParse (OCR)
107
- 4. PDFs that anydoc returns empty → LiteParse (OCR fallback for scanned PDFs)
107
+ 4. PDFs with no text layer — anydoc returns empty or raises NeedsOcrError →
108
+ LiteParse (OCR fallback for scanned PDFs)
108
109
  """
109
110
  suffix = Path(path).suffix.lower()
110
111
 
@@ -126,15 +127,20 @@ def read(path: str, max_chars: Optional[int] = None, extract_images: bool = True
126
127
  anydoc_error = None
127
128
  try:
128
129
  text = _run_anydoc(path)
129
- except (RuntimeError, ImportError) as e:
130
- anydoc_error = str(e)
130
+ except Exception as e:
131
+ # A fallback boundary: any primary-engine failure means "try the
132
+ # other engine". anydoc raises NeedsOcrError for a PDF with no text
133
+ # layer, and its hierarchy (NeedsOcrError -> ConvertError ->
134
+ # Exception) is theirs, not ours — enumerating it is what made the
135
+ # OCR lane below unreachable for scanned PDFs.
136
+ anydoc_error = f"{type(e).__name__}: {e}"
131
137
 
132
138
  # 4. If anydoc returned empty text (scanned PDF) or failed, try LiteParse OCR
133
139
  if not text.strip():
134
140
  try:
135
141
  data = _run_liteparse(path, extract_images=extract_images)
136
142
  text = data.get("text") or ""
137
- except (RuntimeError, ImportError) as liteparse_err:
143
+ except Exception as liteparse_err:
138
144
  if anydoc_error:
139
145
  raise RuntimeError(f"anydoc failed: {anydoc_error}; liteparse fallback also failed: {liteparse_err}")
140
146
  raise RuntimeError(f"both anydoc and liteparse returned no text. liteparse: {liteparse_err}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tabella-docs
3
- Version: 2.0.3
3
+ Version: 2.0.4
4
4
  Summary: Portable document lifecycle suite — create (.docx/.xlsx/.pptx/.pdf/.odt/.ods/.odp), convert (pandoc), and read (anydoc + LiteParse OCR) documents. Agent Plugins v1 compatible.
5
5
  Author-email: D Yiapanis <d@yiapanis.co>
6
6
  License-Expression: MIT
@@ -15,5 +15,6 @@ tabella_docs.egg-info/top_level.txt
15
15
  tests/test_bootstrap_paths.py
16
16
  tests/test_ensure_venv_win.py
17
17
  tests/test_odt_table.py
18
+ tests/test_read_ocr_fallback.py
18
19
  tests/test_security_gates.py
19
20
  tests/test_text_section_alias.py
@@ -0,0 +1,49 @@
1
+ """doc_read on an image-only PDF must reach the OCR lane, not raise.
2
+
3
+ anydoc signals a missing text layer by *raising* NeedsOcrError (MRO:
4
+ NeedsOcrError -> ConvertError -> Exception, so not a RuntimeError), where the
5
+ fallback was gated on empty text and caught only RuntimeError/ImportError.
6
+ Net effect: the fallback was unreachable for exactly the input it exists for.
7
+
8
+ Runs the real pipeline — PIL makes the page, Tesseract reads it — because a
9
+ mocked empty return is what let this ship.
10
+ """
11
+
12
+ from pathlib import Path
13
+
14
+ import pytest
15
+
16
+ pytest.importorskip("PIL")
17
+ pytest.importorskip("anydoc")
18
+ pytest.importorskip("liteparse")
19
+
20
+ from PIL import Image, ImageDraw, ImageFont # noqa: E402
21
+
22
+
23
+ def _page(tmp_path: Path):
24
+ img = Image.new("RGB", (1200, 400), "white")
25
+ draw = ImageDraw.Draw(img)
26
+ try:
27
+ font = ImageFont.truetype(
28
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 64
29
+ )
30
+ except OSError:
31
+ font = ImageFont.load_default(size=64)
32
+ draw.text((60, 120), "SCANNED HELLO 42", fill="black", font=font)
33
+ return img
34
+
35
+
36
+ def test_image_only_pdf_falls_back_to_ocr(tmp_path):
37
+ png, pdf = tmp_path / "scan.png", tmp_path / "scan.pdf"
38
+ img = _page(tmp_path)
39
+ img.save(png)
40
+ img.save(pdf, "PDF", resolution=150.0)
41
+
42
+ from tabella.read import read
43
+
44
+ from_png = " ".join(read(str(png)).split())
45
+ from_pdf = " ".join(read(str(pdf)).split())
46
+
47
+ assert "HELLO" in from_png.upper(), f"OCR lane itself is broken: {from_png!r}"
48
+ assert from_pdf, "image-only PDF returned no text"
49
+ assert from_pdf == from_png, f"PDF and PNG disagree: {from_pdf!r} vs {from_png!r}"
File without changes
File without changes
File without changes