Skip to content

Commit 0e8feb4

Browse files
authored
🐛 💥 harmonize Crop and Split (#438)
1 parent 27a107f commit 0e8feb4

21 files changed

Lines changed: 218 additions & 232 deletions

mindee/image/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +0,0 @@
1-
from mindee.image.image_compressor import compress_image
2-
3-
__all__ = ["compress_image"]

mindee/image/extracted_image.py

Lines changed: 23 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
from __future__ import annotations
22

3-
import io
43
from pathlib import Path
5-
from typing import Any
4+
from typing import Any, BinaryIO
65

76
from mindee.dependencies.checkers import PILLOW_AVAILABLE
87
from mindee.dependencies.decorators import requires_pillow
98
from mindee.error.mindee_error import MindeeError
10-
from mindee.input.file_input import FileInput
11-
from mindee.input.local_input_source import LocalInputSource
9+
from mindee.input.bytes_input import BytesInput
1210
from mindee.logger import logger
1311

1412
if PILLOW_AVAILABLE:
@@ -21,78 +19,68 @@
2119
class ExtractedImage:
2220
"""Generic class for image extraction."""
2321

22+
buffer: BinaryIO
23+
filename: str
2424
_page_id: int
2525
"""Id of the page the image was extracted from."""
2626
_element_id: int
2727
"""Id of the element on a given page."""
28-
filename: str
29-
"""Name of the file the image was extracted from."""
3028

3129
def __init__(
32-
self, input_source: LocalInputSource, page_id: int, element_id: int
30+
self,
31+
img_byte_stream: BinaryIO,
32+
filename: str,
33+
page_id: int,
34+
element_id: int,
3335
) -> None:
3436
"""
3537
Initialize the ExtractedImage with a buffer and an internal file name.
3638
37-
:param input_source: Local source for input.
39+
:param img_byte_stream: The raw image bytes.
40+
:param filename: Name of the file.
3841
:param page_id: ID of the page the element was found on.
3942
:param element_id: ID of the element in a page.
4043
"""
41-
self.buffer = io.BytesIO(input_source.file_object.read())
42-
self.buffer.name = input_source.filename
43-
self.filename = input_source.filename
44-
if input_source.is_pdf():
45-
extension = "jpg"
46-
else:
47-
extension = Path(input_source.filename).resolve().suffix
44+
self.buffer = img_byte_stream
4845
self.buffer.seek(0)
49-
pg_number = str(page_id).zfill(3)
50-
elem_number = str(element_id).zfill(3)
51-
self.internal_file_name = (
52-
f"{input_source.filename}_page{pg_number}-{elem_number}.{extension}"
53-
)
46+
self.filename = filename
5447
self._page_id = page_id
5548
self._element_id = 0 if element_id is None else element_id
5649

5750
@requires_pillow
58-
def save_to_file(self, output_path: Path | str, file_format: str | None = None):
51+
def save_to_file(self, output_path: Path | str):
5952
"""
6053
Saves the document to a file.
6154
6255
:param output_path: Path to save the file to.
63-
:param file_format: Optional PIL-compatible format for the file. Inferred from file extension if not provided.
6456
:raises MindeeError: If an invalid path or filename is provided.
6557
"""
58+
out_path = Path(output_path)
59+
if not out_path.resolve().is_dir():
60+
raise MindeeError("Provided path is not a directory.")
61+
out_file_path = out_path / self.filename
6662
try:
67-
resolved_path = Path(output_path).resolve()
68-
if not file_format and len(resolved_path.suffix) < 1:
69-
raise ValueError("Invalid file format.")
7063
self.buffer.seek(0)
7164
image = Image.open(self.buffer)
72-
if file_format:
73-
image.save(resolved_path, format=file_format)
74-
else:
75-
image.save(resolved_path)
76-
logger.info("File saved successfully to '%s'.", resolved_path)
77-
except TypeError as e:
78-
raise MindeeError("Invalid path/filename provided.") from e
65+
image.save(out_file_path)
66+
logger.info("File saved successfully to '%s'.", out_file_path)
7967
except Exception as e:
8068
print(e)
8169
raise MindeeError(f"Could not save file {Path(output_path).name}.") from e
8270

83-
def as_input_source(self) -> FileInput:
71+
def as_input_source(self) -> BytesInput:
8472
"""
8573
Return the file as a Mindee-compatible BufferInput source.
8674
8775
:returns: A BufferInput source.
8876
"""
8977
self.buffer.seek(0)
90-
return FileInput(self.buffer)
78+
return BytesInput(self.buffer.read(), self.filename)
9179

9280
@property
9381
def page_id(self):
9482
"""
95-
ID of the page the receipt was found on.
83+
ID of the page the image was found on.
9684
9785
:return: A valid page ID.
9886
"""

mindee/image/extracted_images.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from pathlib import Path
2+
3+
from mindee.image.extracted_image import ExtractedImage
4+
5+
6+
class ExtractedImages(list[ExtractedImage]):
7+
"""List of extracted images."""
8+
9+
def save_all_to_disk(self, output_path: Path | str) -> None:
10+
"""Save all extracted images to disk."""
11+
for image in self:
12+
image.save_to_file(output_path)

mindee/image/image_extractor.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import io
4+
from pathlib import Path
45
from typing import Any, BinaryIO
56

67
from mindee.dependencies import requires_pypdfium2
@@ -10,7 +11,6 @@
1011
from mindee.geometry.point import Point
1112
from mindee.geometry.polygon import Polygon, get_min_max_x, get_min_max_y
1213
from mindee.image.extracted_image import ExtractedImage
13-
from mindee.input.bytes_input import BytesInput
1414
from mindee.input.local_input_source import LocalInputSource
1515

1616
if PYPDFIUM2_AVAILABLE:
@@ -29,7 +29,7 @@
2929

3030
@requires_pillow
3131
@requires_pypdfium2
32-
def attach_image_as_new_file( # type: ignore
32+
def _attach_image_as_new_file( # type: ignore
3333
input_buffer: BinaryIO,
3434
) -> pdfium.PdfDocument:
3535
"""
@@ -66,7 +66,7 @@ def extract_image_from_polygon(
6666
width: float,
6767
height: float,
6868
file_format: str,
69-
) -> bytes:
69+
) -> BinaryIO:
7070
"""
7171
Crops the image from the given polygon.
7272
@@ -87,11 +87,11 @@ def extract_image_from_polygon(
8787
int(min_max_y.max * height),
8888
)
8989
)
90-
return save_image_to_buffer(cropped_image, file_format)
90+
return _save_image_to_buffer(cropped_image, file_format)
9191

9292

9393
@requires_pillow
94-
def save_image_to_buffer(image: Image.Image, file_format: str) -> bytes:
94+
def _save_image_to_buffer(image: Image.Image, file_format: str) -> BinaryIO:
9595
"""
9696
Saves an image as a buffer.
9797
@@ -102,7 +102,7 @@ def save_image_to_buffer(image: Image.Image, file_format: str) -> bytes:
102102
buffer = io.BytesIO()
103103
image.save(buffer, format=file_format)
104104
buffer.seek(0)
105-
return buffer.read()
105+
return buffer
106106

107107

108108
@requires_pillow
@@ -145,7 +145,8 @@ def extract_multiple_images_from_source(
145145
:param polygons: List of coordinates to pull the elements from.
146146
:return: List of byte arrays representing the extracted elements.
147147
"""
148-
page = load_pdf_doc(input_source).get_page(page_id)
148+
stem = Path(input_source.filename).stem
149+
page = _load_pdf_doc(input_source).get_page(page_id)
149150
page_content = page.render().to_pil()
150151
width, height = page.get_size()
151152

@@ -159,20 +160,17 @@ def extract_multiple_images_from_source(
159160
)
160161
extracted_elements.append(
161162
ExtractedImage(
162-
BytesInput(
163-
image_data,
164-
f"{input_source.filename}_page{page_id + 1}-{element_id}.{file_extension}",
165-
),
163+
image_data,
164+
f"{stem}_page-{(page_id + 1):03d}-item-{(element_id + 1):03d}.{file_extension}",
166165
page_id,
167166
element_id,
168167
)
169168
)
170-
171169
return extracted_elements
172170

173171

174172
@requires_pypdfium2
175-
def load_pdf_doc(input_file: LocalInputSource) -> pdfium.PdfDocument: # type: ignore
173+
def _load_pdf_doc(input_file: LocalInputSource) -> pdfium.PdfDocument: # type: ignore
176174
"""
177175
Loads a PDF document from a local input source.
178176
@@ -183,4 +181,4 @@ def load_pdf_doc(input_file: LocalInputSource) -> pdfium.PdfDocument: # type: i
183181
input_file.file_object.seek(0)
184182
return pdfium.PdfDocument(input_file.file_object.read())
185183

186-
return attach_image_as_new_file(input_file.file_object)
184+
return _attach_image_as_new_file(input_file.file_object)

mindee/input/local_input_source.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from mindee.dependencies.checkers import PYPDFIUM2_AVAILABLE
1111
from mindee.error.mimetype_error import MimeTypeError
1212
from mindee.error.mindee_error import MindeeError, MindeeSourceError
13-
from mindee.image import compress_image
13+
from mindee.image.image_compressor import compress_image
1414
from mindee.input.page_options import KEEP_ONLY, REMOVE, PageOptions
1515
from mindee.logger import logger
1616
from mindee.pdf.pdf_compressor import compress_pdf

mindee/pdf/extracted_pdf.py

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,22 @@
1818
class ExtractedPDF:
1919
"""An extracted sub-Pdf."""
2020

21-
pdf_bytes: BinaryIO
21+
buffer: BinaryIO
2222
filename: str
23+
_page_indexes: tuple[int, int]
2324

24-
def __init__(self, pdf_bytes: BinaryIO, filename: str):
25-
self.pdf_bytes = pdf_bytes
25+
def __init__(
26+
self, pdf_byte_stream: BinaryIO, filename: str, page_indexes: tuple[int, int]
27+
):
28+
self.buffer = pdf_byte_stream
2629
self.filename = filename
30+
self._page_indexes = page_indexes
2731

2832
@requires_pypdfium2
2933
def get_page_count(self) -> int:
3034
"""Get the number of pages in the PDF file."""
3135
try:
32-
pdf = pdfium.PdfDocument(self.pdf_bytes)
36+
pdf = pdfium.PdfDocument(self.buffer)
3337
return len(pdf)
3438
except Exception as e:
3539
raise MindeeError(
@@ -40,21 +44,28 @@ def save_to_file(self, output_path: Path | str):
4044
"""
4145
Writes the contents of the current PDF object to a file.
4246
43-
:param output_path: Path of the destination file. If
44-
not extension is provided, pdf will be appended by default.
47+
:param output_path: Path of the destination file.
48+
If no extension is provided, '.pdf' will be appended by default.
4549
"""
4650
out_path = Path(output_path)
47-
if out_path.resolve().is_dir():
48-
raise MindeeError("Provided path is not a file.")
49-
if not output_path or not out_path.parent.exists():
50-
raise MindeeError("Invalid save path provided {}.")
51-
if out_path.suffix.lower() != "pdf":
52-
out_path = out_path.parent / (out_path.stem + "." + "pdf")
53-
self.pdf_bytes.seek(0)
54-
with open(out_path, "wb") as out_file:
55-
out_file.write(self.pdf_bytes.read())
51+
if not out_path.resolve().is_dir():
52+
raise MindeeError("Provided path is not a directory.")
53+
out_file_path = out_path / self.filename
54+
55+
try:
56+
self.buffer.seek(0)
57+
with open(out_file_path, "wb") as out_file:
58+
out_file.write(self.buffer.read())
59+
except Exception as e:
60+
print(e)
61+
raise MindeeError(f"Could not save file {out_file_path}.") from e
5662

5763
def as_input_source(self) -> BytesInput:
5864
"""Returns the current PDF object as a usable BytesInput source."""
59-
self.pdf_bytes.seek(0)
60-
return BytesInput(self.pdf_bytes.read(), self.filename)
65+
self.buffer.seek(0)
66+
return BytesInput(self.buffer.read(), self.filename)
67+
68+
@property
69+
def page_indexes(self) -> tuple[int, int]:
70+
"""This PDF was extracted from this page range of the original PDF."""
71+
return self._page_indexes

mindee/pdf/extracted_pdfs.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from pathlib import Path
2+
3+
from mindee.pdf.extracted_pdf import ExtractedPDF
4+
5+
6+
class ExtractedPDFs(list[ExtractedPDF]):
7+
"""List of extracted PDFs."""
8+
9+
def save_all_to_disk(self, output_path: Path | str) -> None:
10+
"""Save all extracted images to disk."""
11+
12+
for image in self:
13+
image.save_to_file(output_path)

mindee/pdf/pdf_extractor.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def extract_sub_documents(
6868
"""
6969
Extract the sub-documents from the main pdf, based on the given list of page indexes.
7070
71-
:param page_indexes: List of list of numbers, representing page indexes.
71+
:param page_indexes: 2D list of numbers, representing page indexes.
7272
:return: A list of created PDFS.
7373
"""
7474
extracted_pdfs: list[ExtractedPDF] = []
@@ -80,10 +80,12 @@ def extract_sub_documents(
8080
for page_index in page_index_elem:
8181
if page_index > self.get_page_count():
8282
raise MindeeError(f"Index {page_index} is out of range.")
83-
formatted_max_index = f"{page_index_elem[len(page_index_elem) - 1] + 1:03d}"
84-
field_filename = f"{stem}_{(page_index_elem[0] + 1):03d}-{formatted_max_index}{extension}"
83+
first_page = page_index_elem[0]
84+
last_page = page_index_elem[len(page_index_elem) - 1]
8585
extracted_pdf = ExtractedPDF(
86-
self.cut_pages(page_index_elem), field_filename
86+
self.cut_pages(page_index_elem),
87+
f"{stem}_pages-{(first_page + 1):03d}-{(last_page + 1):03d}{extension}",
88+
(first_page, last_page),
8789
)
8890
extracted_pdfs.append(extracted_pdf)
8991
return extracted_pdfs

mindee/v2/file_operations/crop.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from mindee.error import MindeeError
22
from mindee.geometry import Point, Polygon
33
from mindee.image.extracted_image import ExtractedImage
4+
from mindee.image.extracted_images import ExtractedImages
45
from mindee.image.image_extractor import extract_multiple_images_from_source
56
from mindee.input.local_input_source import LocalInputSource
6-
from mindee.v2.file_operations.crop_files import CropFiles
77
from mindee.v2.parsing.inference.field import FieldLocation
88
from mindee.v2.product.crop.crop_item import CropItem
99

@@ -25,7 +25,7 @@ def extract_single_crop(
2525

2626
def extract_multiple_crops(
2727
input_source: LocalInputSource, crops: list[CropItem]
28-
) -> CropFiles:
28+
) -> ExtractedImages:
2929
"""
3030
Extracts individual receipts from multi-receipts documents.
3131
@@ -49,4 +49,4 @@ def extract_multiple_crops(
4949
polygon,
5050
)
5151
)
52-
return CropFiles(images)
52+
return ExtractedImages(images)

mindee/v2/file_operations/crop_files.py

Lines changed: 0 additions & 20 deletions
This file was deleted.

0 commit comments

Comments
 (0)