scanlayer 1.0.0__py3-none-any.whl
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.
- scanlayer/__init__.py +32 -0
- scanlayer/__main__.py +10 -0
- scanlayer/cli/__init__.py +10 -0
- scanlayer/cli/dry_run.py +191 -0
- scanlayer/cli/parser.py +199 -0
- scanlayer/cli/run.py +217 -0
- scanlayer/config.py +267 -0
- scanlayer/fonts/DejaVuSans.ttf +0 -0
- scanlayer/layout/__init__.py +0 -0
- scanlayer/layout/columns.py +264 -0
- scanlayer/main.py +666 -0
- scanlayer/ocr/__init__.py +5 -0
- scanlayer/ocr/engine.py +335 -0
- scanlayer/ocr/export.py +264 -0
- scanlayer/pdf/__init__.py +5 -0
- scanlayer/pdf/builder.py +309 -0
- scanlayer/pdf/fonts.py +110 -0
- scanlayer/preprocessing/__init__.py +5 -0
- scanlayer/preprocessing/enhance.py +367 -0
- scanlayer/utils/__init__.py +19 -0
- scanlayer/utils/debug_image.py +85 -0
- scanlayer/utils/errors.py +42 -0
- scanlayer/utils/logger.py +100 -0
- scanlayer/utils/validators.py +199 -0
- scanlayer-1.0.0.dist-info/METADATA +17 -0
- scanlayer-1.0.0.dist-info/RECORD +30 -0
- scanlayer-1.0.0.dist-info/WHEEL +5 -0
- scanlayer-1.0.0.dist-info/entry_points.txt +2 -0
- scanlayer-1.0.0.dist-info/licenses/LICENSE.md +21 -0
- scanlayer-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Input and runtime environment validation.
|
|
3
|
+
|
|
4
|
+
Centralizes pre-checks to fail early with clear messages when prerequisites
|
|
5
|
+
are missing. Each validator raises a specific ValidationError subclass.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from scanlayer import config
|
|
14
|
+
from scanlayer.utils.logger import get_logger
|
|
15
|
+
|
|
16
|
+
log = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ValidationError(Exception):
|
|
20
|
+
"""Base for user-facing validation errors with actionable messages."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class InputFileError(ValidationError):
|
|
24
|
+
"""The input file does not exist, is not readable, or has an unsupported format."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OutputPathError(ValidationError):
|
|
28
|
+
"""The output path is invalid (parent directory missing,
|
|
29
|
+
insufficient permissions, etc.)."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TesseractEnvironmentError(ValidationError):
|
|
33
|
+
"""Tesseract is missing, not found on PATH, or its tessdata is missing."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DependencyError(ValidationError):
|
|
37
|
+
"""A required Python dependency is not installed or broken."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
SUPPORTED_IMAGE_EXTENSIONS = {
|
|
41
|
+
".jpg", ".jpeg", ".png", ".tif", ".tiff", ".bmp",
|
|
42
|
+
".gif", ".webp", ".jp2", ".j2k",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_COMMON_LANG_FILES = {"eng.traineddata", "fra.traineddata", "osd.traineddata"}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def validate_input_file(path: str) -> str:
|
|
49
|
+
"""Check that the input file exists, is readable, and has a recognized extension.
|
|
50
|
+
|
|
51
|
+
Returns the resolved absolute path. Raises InputFileError on failure.
|
|
52
|
+
"""
|
|
53
|
+
if not path or not isinstance(path, str):
|
|
54
|
+
raise InputFileError("No input path provided.")
|
|
55
|
+
|
|
56
|
+
p = Path(path).expanduser()
|
|
57
|
+
if not p.exists():
|
|
58
|
+
raise InputFileError(
|
|
59
|
+
f"Input file does not exist: {path}\n"
|
|
60
|
+
f"Check the path and make sure you are in the correct directory."
|
|
61
|
+
)
|
|
62
|
+
if not p.is_file():
|
|
63
|
+
raise InputFileError(
|
|
64
|
+
f"The specified path is not a file: {path}"
|
|
65
|
+
)
|
|
66
|
+
if os.access(p, os.R_OK) is False:
|
|
67
|
+
raise InputFileError(
|
|
68
|
+
f"Input file is not readable (permissions): {path}"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
ext = p.suffix.lower()
|
|
72
|
+
if ext not in SUPPORTED_IMAGE_EXTENSIONS:
|
|
73
|
+
log.warning(
|
|
74
|
+
f"Extension '{ext}' not in the supported list "
|
|
75
|
+
f"({', '.join(sorted(SUPPORTED_IMAGE_EXTENSIONS))}). "
|
|
76
|
+
f"Attempting anyway, Pillow will reject if the format is unknown."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if p.stat().st_size == 0:
|
|
80
|
+
raise InputFileError(f"Input file is empty: {path}")
|
|
81
|
+
|
|
82
|
+
return str(p.resolve())
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def validate_output_path(path: str, expected_ext: str = ".pdf") -> str:
|
|
86
|
+
"""Check that the output path is valid (parent exists, is writable).
|
|
87
|
+
|
|
88
|
+
Returns the resolved absolute path. A mismatched extension only logs
|
|
89
|
+
a warning since the file is still written in the requested format.
|
|
90
|
+
Raises OutputPathError on failure.
|
|
91
|
+
"""
|
|
92
|
+
if not path or not isinstance(path, str):
|
|
93
|
+
raise OutputPathError("No output path provided.")
|
|
94
|
+
|
|
95
|
+
p = Path(path).expanduser()
|
|
96
|
+
if p.exists() and p.is_dir():
|
|
97
|
+
raise OutputPathError(
|
|
98
|
+
f"The output path points to a directory, not a file: {path}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
parent = p.parent
|
|
102
|
+
if not parent.exists():
|
|
103
|
+
raise OutputPathError(
|
|
104
|
+
f"Parent directory of the output file does not exist: {parent}\n"
|
|
105
|
+
f"Create it first, or fix the output path."
|
|
106
|
+
)
|
|
107
|
+
if os.access(parent, os.W_OK) is False:
|
|
108
|
+
raise OutputPathError(
|
|
109
|
+
f"Output directory is not writable: {parent}"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
ext = p.suffix.lower()
|
|
113
|
+
if ext != expected_ext.lower():
|
|
114
|
+
log.warning(
|
|
115
|
+
f"Output path does not have the {expected_ext} extension "
|
|
116
|
+
f"('{ext}'). The file will still be written in the "
|
|
117
|
+
f"requested format, but the extension is misleading."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return str(p.resolve())
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def validate_tesseract_environment() -> None:
|
|
124
|
+
"""Check that Tesseract is installed and tessdata is available.
|
|
125
|
+
|
|
126
|
+
Raises TesseractEnvironmentError with an actionable message if not.
|
|
127
|
+
"""
|
|
128
|
+
cmd = config.TESSERACT_CMD
|
|
129
|
+
|
|
130
|
+
if cmd not in ("tesseract",):
|
|
131
|
+
if not os.path.exists(cmd):
|
|
132
|
+
raise TesseractEnvironmentError(
|
|
133
|
+
f"Tesseract not found at the configured location: {cmd}\n"
|
|
134
|
+
f"Check config.DEV_TESSERACT_OVERRIDE, the "
|
|
135
|
+
f"TESSERACT_CMD environment variable, or scanlayer.configure().\n"
|
|
136
|
+
f"Install Tesseract via your package manager "
|
|
137
|
+
f"(apt install tesseract-ocr / brew install tesseract), "
|
|
138
|
+
f"or the Tesseract installer on Windows."
|
|
139
|
+
)
|
|
140
|
+
if not os.access(cmd, os.X_OK):
|
|
141
|
+
raise TesseractEnvironmentError(
|
|
142
|
+
f"Tesseract is not executable: {cmd} (permissions)"
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
import shutil
|
|
146
|
+
resolved = shutil.which("tesseract")
|
|
147
|
+
if resolved is None:
|
|
148
|
+
raise TesseractEnvironmentError(
|
|
149
|
+
"Tesseract not found on the system PATH.\n"
|
|
150
|
+
"Install Tesseract via your package manager "
|
|
151
|
+
"(apt install tesseract-ocr / brew install tesseract / "
|
|
152
|
+
"or download the tesseract-ocr installer on Windows).\n"
|
|
153
|
+
"Or override the path via the TESSERACT_CMD=/path/to/tesseract "
|
|
154
|
+
"environment variable."
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if config.TESSDATA_DIR:
|
|
158
|
+
td = Path(config.TESSDATA_DIR)
|
|
159
|
+
if not td.exists():
|
|
160
|
+
raise TesseractEnvironmentError(
|
|
161
|
+
f"Configured tessdata directory does not exist: {td}"
|
|
162
|
+
)
|
|
163
|
+
present = {f.name for f in td.glob("*.traineddata")}
|
|
164
|
+
missing_common = _COMMON_LANG_FILES - present
|
|
165
|
+
if missing_common:
|
|
166
|
+
log.warning(
|
|
167
|
+
f"Missing tessdata for: {', '.join(sorted(missing_common))}. "
|
|
168
|
+
f"OCR in those languages will fail."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def validate_image_readable(path: str) -> None:
|
|
173
|
+
"""Open the image with Pillow to verify it is not corrupted.
|
|
174
|
+
|
|
175
|
+
Raises InputFileError if Pillow cannot identify it.
|
|
176
|
+
"""
|
|
177
|
+
try:
|
|
178
|
+
from PIL import Image
|
|
179
|
+
with Image.open(path) as img:
|
|
180
|
+
img.verify() # does not load into memory, only checks the header
|
|
181
|
+
except Exception as exc:
|
|
182
|
+
raise InputFileError(
|
|
183
|
+
f"Unreadable or corrupted image: {path}\n"
|
|
184
|
+
f"Pillow could not identify it: {type(exc).__name__}: {exc}"
|
|
185
|
+
) from exc
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def validate_all(
|
|
189
|
+
input_path: str, output_path: str, output_format: str = "pdf"
|
|
190
|
+
) -> tuple[str, str]:
|
|
191
|
+
"""Run all validations in order of fastest first.
|
|
192
|
+
|
|
193
|
+
Returns (input_abs, output_abs) if everything is OK.
|
|
194
|
+
"""
|
|
195
|
+
input_abs = validate_input_file(input_path)
|
|
196
|
+
validate_tesseract_environment()
|
|
197
|
+
output_abs = validate_output_path(output_path, expected_ext=f".{output_format}")
|
|
198
|
+
validate_image_readable(input_abs)
|
|
199
|
+
return input_abs, output_abs
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scanlayer
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Image (scan/photo) to searchable PDF, or raw OCR export, via Tesseract.
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
License-File: LICENSE.md
|
|
7
|
+
Requires-Dist: pytesseract>=0.3.10
|
|
8
|
+
Requires-Dist: Pillow>=10.0.0
|
|
9
|
+
Requires-Dist: reportlab>=4.0.0
|
|
10
|
+
Requires-Dist: opencv-python-headless>=4.8.0
|
|
11
|
+
Requires-Dist: numpy>=1.24.0
|
|
12
|
+
Requires-Dist: pdf2image>=1.17.0
|
|
13
|
+
Provides-Extra: test
|
|
14
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
15
|
+
Provides-Extra: yaml
|
|
16
|
+
Requires-Dist: PyYAML>=6.0; extra == "yaml"
|
|
17
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
scanlayer/__init__.py,sha256=RVVGA5ggyXr5_dHpTZAlCk9tugT7ioqmUxW4we1Brr8,996
|
|
2
|
+
scanlayer/__main__.py,sha256=7dtsL73buULWsVpixlyOrhw_ldPrxAkTzeh3FLDE5II,207
|
|
3
|
+
scanlayer/config.py,sha256=HSndjcHm_teBEYgl9la0eFWuKwNrtUXWroDFh6QhlOw,7849
|
|
4
|
+
scanlayer/main.py,sha256=07XiZ9ouXop6BQVj9CZCtH01YlzED35_N50SVsZ9jvQ,23158
|
|
5
|
+
scanlayer/cli/__init__.py,sha256=fC3bR5kkWENgx31--7Y7tb3A8BGbXyg5D-z41aR9krY,205
|
|
6
|
+
scanlayer/cli/dry_run.py,sha256=ueXZSrOLm2qV0G1n0k-zwG9qLggaAPhnjH685fPPNqQ,6860
|
|
7
|
+
scanlayer/cli/parser.py,sha256=mUzE-qyliy9jGlEuUQ9HV8Pk-mWZWOQuRrvM2XEK07c,8570
|
|
8
|
+
scanlayer/cli/run.py,sha256=BC8iZQ9aM8HPkXLU9VbHuC4-gFADrH6gTPPYi5EX2D4,7624
|
|
9
|
+
scanlayer/fonts/DejaVuSans.ttf,sha256=rnt4VeEVpZZtixs_gPJUzMEX7Ib5ll4gLuKUBFODcoA,759720
|
|
10
|
+
scanlayer/layout/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
scanlayer/layout/columns.py,sha256=NuJHZ3AsbH7w2uXWriguS9VBNX94cAq9vXmOyd6mIuk,8596
|
|
12
|
+
scanlayer/ocr/__init__.py,sha256=GI3op-X4or5sZ48XJc_17br0-V3BuryJ4VNkXjVG8Hk,181
|
|
13
|
+
scanlayer/ocr/engine.py,sha256=FRkjsTK8cI0I8DWhWLViP8bTcQ-ZqdMvISkjx3_bHjc,11045
|
|
14
|
+
scanlayer/ocr/export.py,sha256=u09trX5LYlf_x9Ly8lJXwYbpnd7M6hWNTaRePziNZbA,8941
|
|
15
|
+
scanlayer/pdf/__init__.py,sha256=GCKBhmuxyriuas392LNtMVcdCM0_41wF3Nfi8dtXdjs,133
|
|
16
|
+
scanlayer/pdf/builder.py,sha256=As7AHHonKgh8Mq6sbEzRXmRMNWfIyPR96B7EWdKJl08,10258
|
|
17
|
+
scanlayer/pdf/fonts.py,sha256=Wh-_CypWvg4om3mif_cD1ml712fd8g6_547f5WLNKsc,3253
|
|
18
|
+
scanlayer/preprocessing/__init__.py,sha256=byrSD5vjFvwEg-92nMJxA9CWVmPxpNfyJ0medhzR-QA,188
|
|
19
|
+
scanlayer/preprocessing/enhance.py,sha256=877QZhnA2dnK93b3U0oCGsb6oHXAN27X1wB8FvTPFak,12640
|
|
20
|
+
scanlayer/utils/__init__.py,sha256=V1q0cs2APJ7keK4Y4uwfFfBVqwJmqHJE7LzNThhKCMA,332
|
|
21
|
+
scanlayer/utils/debug_image.py,sha256=z0dOuYwRmJedC7jvfbEJbGKA8NiHGpVsAO3WlhsyFV4,2564
|
|
22
|
+
scanlayer/utils/errors.py,sha256=jr2OUiIevFSUJlWO90etav-GUOnP9O-3bXkHjPPSKnw,1827
|
|
23
|
+
scanlayer/utils/logger.py,sha256=QIulvS3RjJ1fFkaNdlyaRM4wzVuZOg87hGnPsTmV4dA,3080
|
|
24
|
+
scanlayer/utils/validators.py,sha256=oNHEgwrcY2X0MkUKYUWxtic4UZL6zypUv3hdjlTuBqY,6801
|
|
25
|
+
scanlayer-1.0.0.dist-info/licenses/LICENSE.md,sha256=GnqaSHPX43GuJ4OQkCrW6zCf5eschtYCwHej5siK6w8,1079
|
|
26
|
+
scanlayer-1.0.0.dist-info/METADATA,sha256=UWrnIa62RpJpnpGHlFsCOAFKT28vDlaGQVq9nd1-lec,538
|
|
27
|
+
scanlayer-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
28
|
+
scanlayer-1.0.0.dist-info/entry_points.txt,sha256=1BhRpUFaKnTFgLWrRVAr8MAgpqmwH-8sz5kbmCkwf-s,49
|
|
29
|
+
scanlayer-1.0.0.dist-info/top_level.txt,sha256=52X241TR07ERaeDJP76tmilaQcqfpDCpndN0nQJlGSs,10
|
|
30
|
+
scanlayer-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 scanlayer contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
scanlayer
|