File size: 6,528 Bytes
b411c37
 
 
 
 
 
 
 
98e1d9f
b411c37
98e1d9f
 
b411c37
98e1d9f
 
b411c37
 
98e1d9f
b411c37
 
 
98e1d9f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b411c37
 
 
 
98e1d9f
 
b411c37
 
 
 
 
 
98e1d9f
 
 
b411c37
 
 
 
 
 
 
 
 
 
 
 
98e1d9f
b411c37
 
 
 
 
 
 
 
98e1d9f
 
 
 
b411c37
98e1d9f
b411c37
 
 
98e1d9f
b411c37
 
 
98e1d9f
 
 
b411c37
 
 
 
 
98e1d9f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
from __future__ import annotations

from io import BytesIO
from pathlib import Path

from PIL import Image, ImageOps, UnidentifiedImageError

from ocr_studio.config import (
    ALLOWED_DOCUMENT_SUFFIXES,
    ALLOWED_IMAGE_FORMATS,
    ALLOWED_IMAGE_SUFFIXES,
    MAX_BATCH_ITEMS,
    MAX_IMAGE_SIDE,
    MAX_IMAGE_SIDE_ACCURATE,
    MAX_PDF_PAGES,
    MAX_UPLOAD_BYTES,
    MAX_UPLOAD_MB,
    PDF_RENDER_SCALE,
)
from ocr_studio.errors import OcrError

_HEIF_READY = False


def register_heif() -> None:
    global _HEIF_READY
    if _HEIF_READY:
        return
    try:
        from pillow_heif import register_heif_opener

        register_heif_opener()
        _HEIF_READY = True
    except Exception:
        _HEIF_READY = False


register_heif()


def _as_file_list(files: object) -> list[Path]:
    if files is None:
        return []
    if isinstance(files, (str, Path)):
        raw = str(files).strip()
        if not raw or raw in {".", "./"}:
            return []
        return [Path(raw)]
    if isinstance(files, dict) and files.get("path"):
        return [Path(str(files["path"]))]
    if isinstance(files, (list, tuple)):
        paths: list[Path] = []
        for item in files:
            paths.extend(_as_file_list(item))
        return paths
    return []


def _open_image(source: Image.Image | str | Path | bytes) -> Image.Image:
    if isinstance(source, Image.Image):
        image = source
    elif hasattr(source, "shape") and hasattr(source, "dtype"):
        image = Image.fromarray(source)
    elif isinstance(source, (str, Path)):
        path = Path(source)
        if not path.exists():
            raise OcrError("The uploaded image could not be read.")
        if path.stat().st_size > MAX_UPLOAD_BYTES:
            raise OcrError(f"Image is larger than {MAX_UPLOAD_MB} MB.")
        suffix = path.suffix.lower()
        if suffix not in ALLOWED_IMAGE_SUFFIXES:
            raise OcrError("Use JPG, PNG, WEBP, BMP, TIFF, or HEIC.")
        try:
            image = Image.open(path)
        except UnidentifiedImageError as exc:
            raise OcrError("The file is not a valid image.") from exc
    elif isinstance(source, (bytes, bytearray)):
        if len(source) > MAX_UPLOAD_BYTES:
            raise OcrError(f"Image is larger than {MAX_UPLOAD_MB} MB.")
        try:
            image = Image.open(BytesIO(source))
        except UnidentifiedImageError as exc:
            raise OcrError("The file is not a valid image.") from exc
    else:
        raise OcrError("Please upload one image.", key="err.no_input")

    try:
        image.load()
    except OSError as exc:
        raise OcrError("The image file is damaged.") from exc
    return image


def prepare_image(

    source: Image.Image | str | Path | bytes | None,

    accurate: bool = False,

) -> Image.Image:
    if source is None:
        raise OcrError("Please upload one image.", key="err.no_input")
    image = _open_image(source)
    fmt = (image.format or "").upper()
    if fmt and fmt not in ALLOWED_IMAGE_FORMATS:
        raise OcrError("Use JPG, PNG, WEBP, BMP, TIFF, or HEIC.")

    image = ImageOps.exif_transpose(image).convert("RGB")
    longest = max(image.size)
    limit = MAX_IMAGE_SIDE_ACCURATE if accurate else MAX_IMAGE_SIDE
    if longest > limit:
        scale = limit / float(longest)
        image = image.resize(
            (max(1, int(image.width * scale)), max(1, int(image.height * scale))),
            Image.Resampling.LANCZOS,
        )
    return image


def render_pdf_pages(path: Path, accurate: bool = False) -> tuple[list[Image.Image], int]:
    if path.stat().st_size > MAX_UPLOAD_BYTES:
        raise OcrError(f"PDF is larger than {MAX_UPLOAD_MB} MB.")
    try:
        import pypdfium2 as pdfium
    except Exception as exc:
        raise OcrError("PDF support is not available in this environment.") from exc

    try:
        document = pdfium.PdfDocument(str(path))
    except Exception as exc:
        raise OcrError("The PDF file could not be opened.") from exc

    total = len(document)
    count = min(total, MAX_PDF_PAGES)
    pages: list[Image.Image] = []
    try:
        for index in range(count):
            page = document[index]
            bitmap = page.render(scale=PDF_RENDER_SCALE)
            rendered = bitmap.to_pil()
            pages.append(prepare_image(rendered, accurate=accurate))
    finally:
        document.close()
    return pages, total


def collect_pages(

    image: Image.Image | str | Path | bytes | None,

    files: object,

    accurate: bool = False,

) -> tuple[list[Image.Image], int, bool]:
    pages: list[Image.Image] = []
    source_files = _as_file_list(files)
    if len(source_files) > MAX_BATCH_ITEMS:
            raise OcrError(
                f"Upload at most {MAX_BATCH_ITEMS} files per job.",
                key="err.too_many_files",
                max_items=MAX_BATCH_ITEMS,
            )

    if image is not None:
        pages.append(prepare_image(image, accurate=accurate))

    total_pdf_pages = 0
    truncated = False
    for path in source_files:
        if not path.exists():
            raise OcrError("The uploaded file could not be read.")
        if path.stat().st_size > MAX_UPLOAD_BYTES:
            raise OcrError(f"A file is larger than {MAX_UPLOAD_MB} MB.")
        suffix = path.suffix.lower()
        if suffix not in ALLOWED_DOCUMENT_SUFFIXES:
            raise OcrError("Use JPG, PNG, WEBP, BMP, TIFF, HEIC, or PDF.")
        if suffix == ".pdf":
            rendered, total = render_pdf_pages(path, accurate=accurate)
            total_pdf_pages += total
            if total > MAX_PDF_PAGES:
                truncated = True
            remaining = MAX_PDF_PAGES - len(pages)
            if remaining <= 0:
                truncated = True
                break
            pages.extend(rendered[:remaining])
            if total > remaining:
                truncated = True
        else:
            if len(pages) >= MAX_PDF_PAGES:
                truncated = True
                break
            pages.append(prepare_image(path, accurate=accurate))

    if not pages:
        raise OcrError("Please upload one image.", key="err.no_input")
    if len(pages) > MAX_PDF_PAGES:
        truncated = True
        pages = pages[:MAX_PDF_PAGES]
    return pages, max(total_pdf_pages, len(pages)), truncated