convert-stream 1.1__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.
Potentially problematic release.
This version of convert-stream might be problematic. Click here for more details.
- convert_stream/__init__.py +19 -0
- convert_stream/__main__.py +29 -0
- convert_stream/_version.py +7 -0
- convert_stream/convert.py +300 -0
- convert_stream/imagelib.py +493 -0
- convert_stream/library.py +5 -0
- convert_stream/models/__init__.py +10 -0
- convert_stream/models/m_imgelib.py +107 -0
- convert_stream/models/m_pdf_convert.py +171 -0
- convert_stream/models/m_pdf_document.py +139 -0
- convert_stream/models/m_pdf_page.py +88 -0
- convert_stream/models/m_text.py +480 -0
- convert_stream/pdf_convert.py +282 -0
- convert_stream/pdf_document.py +534 -0
- convert_stream/pdf_page.py +251 -0
- convert_stream/text.py +8 -0
- convert_stream-1.1.dist-info/METADATA +45 -0
- convert_stream-1.1.dist-info/RECORD +20 -0
- convert_stream-1.1.dist-info/WHEEL +5 -0
- convert_stream-1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Tuple
|
|
5
|
+
from io import BytesIO
|
|
6
|
+
from PIL import Image, ImageOps, ImageFilter
|
|
7
|
+
from cv2.typing import MatLike
|
|
8
|
+
import cv2
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from soup_files import File
|
|
12
|
+
from convertlib.models.m_imgelib import LibraryImage, ABCImageObject, ABCInvertColor
|
|
13
|
+
|
|
14
|
+
#=============================================================================#
|
|
15
|
+
# Inverter cores em imagens
|
|
16
|
+
#=============================================================================#
|
|
17
|
+
class ImplementInvertColorOpenCv(ABCInvertColor):
|
|
18
|
+
"""
|
|
19
|
+
Escurecer texto em imagens.
|
|
20
|
+
"""
|
|
21
|
+
def __init__(self, image_bytes):
|
|
22
|
+
super().__init__()
|
|
23
|
+
if not isinstance(image_bytes, bytes):
|
|
24
|
+
raise ValueError(f'{__class__.__name__}\nUse: bytes, não {type(image_bytes)}')
|
|
25
|
+
self.image_bytes:bytes = image_bytes
|
|
26
|
+
|
|
27
|
+
def _image_bytes_to_opencv(self, img_bytes:bytes) -> MatLike:
|
|
28
|
+
"""Converte os bytes de uma imagem em objeto opencv MatLike"""
|
|
29
|
+
nparr = np.frombuffer(img_bytes, np.uint8)
|
|
30
|
+
return cv2.imdecode(nparr, cv2.IMREAD_GRAYSCALE)
|
|
31
|
+
|
|
32
|
+
def _image_opencv_tobytes(self, img:MatLike) -> bytes:
|
|
33
|
+
"""Convert um objeto opencv MatLike em bytes de imagem"""
|
|
34
|
+
_, buffer = cv2.imencode('.png', img) # Codifica como PNG (ou use '.jpg' para JPEG)
|
|
35
|
+
return buffer.tobytes() # Obtém os bytes da imagem
|
|
36
|
+
|
|
37
|
+
def set_gaussian_blur(self):
|
|
38
|
+
# Aplica um filtro Gaussiano para reduzir o ruído
|
|
39
|
+
_blurred:MatLike = cv2.GaussianBlur(self._image_bytes_to_opencv(self.image_bytes), (3, 3), 0.5)
|
|
40
|
+
self.image_bytes = self._image_opencv_tobytes(_blurred)
|
|
41
|
+
|
|
42
|
+
def __set_black(self):
|
|
43
|
+
# Aplica binarização adaptativa (texto branco, fundo preto)
|
|
44
|
+
binary:MatLike = cv2.adaptiveThreshold(
|
|
45
|
+
self._image_bytes_to_opencv(self.image_bytes),
|
|
46
|
+
150,
|
|
47
|
+
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
|
48
|
+
cv2.THRESH_BINARY,
|
|
49
|
+
11,
|
|
50
|
+
2
|
|
51
|
+
)
|
|
52
|
+
self.image_bytes = self._image_opencv_tobytes(binary)
|
|
53
|
+
|
|
54
|
+
def __set_gray(self):
|
|
55
|
+
# Aplica binarização adaptativa (texto preto, fundo branco)
|
|
56
|
+
_binary = cv2.adaptiveThreshold(
|
|
57
|
+
self._image_bytes_to_opencv(self.image_bytes),
|
|
58
|
+
150,
|
|
59
|
+
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
|
60
|
+
cv2.THRESH_BINARY_INV, # Inverte o texto para ser branco inicialmente
|
|
61
|
+
11,
|
|
62
|
+
2
|
|
63
|
+
)
|
|
64
|
+
self.image_bytes = self._image_opencv_tobytes(_binary)
|
|
65
|
+
|
|
66
|
+
def set_background_black(self):
|
|
67
|
+
self.set_gaussian_blur()
|
|
68
|
+
self.__set_black()
|
|
69
|
+
_result:MatLike = cv2.bitwise_not(self._image_bytes_to_opencv(self.image_bytes))
|
|
70
|
+
self.image_bytes = self._image_opencv_tobytes(_result)
|
|
71
|
+
|
|
72
|
+
def set_background_gray(self):
|
|
73
|
+
self.set_gaussian_blur()
|
|
74
|
+
self.__set_gray()
|
|
75
|
+
_result:MatLike = cv2.bitwise_not(self._image_bytes_to_opencv(self.image_bytes))
|
|
76
|
+
self.image_bytes = self._image_opencv_tobytes(_result)
|
|
77
|
+
|
|
78
|
+
def to_file(self, output_path:File):
|
|
79
|
+
print(f'Salvando: {output_path.basename()}')
|
|
80
|
+
cv2.imwrite(output_path.absolute(), self._image_bytes_to_opencv(self.image_bytes))
|
|
81
|
+
|
|
82
|
+
def to_bytes(self) -> bytes:
|
|
83
|
+
return self.image_bytes
|
|
84
|
+
|
|
85
|
+
def to_pil(self) -> Image.Image:
|
|
86
|
+
"""Converte a propriedade bytes em imagem PIL"""
|
|
87
|
+
return Image.open(BytesIO(self.image_bytes))
|
|
88
|
+
|
|
89
|
+
def to_opencv(self) -> cv2.typing.MatLike:
|
|
90
|
+
"""Converte a propriedade bytes em imagem objeto opencv"""
|
|
91
|
+
return self._image_bytes_to_opencv(self.image_bytes)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class ImplementInvertColorPIL(ABCInvertColor):
|
|
95
|
+
"""
|
|
96
|
+
Implementação da inversão de cores usando PIL (Pillow).
|
|
97
|
+
"""
|
|
98
|
+
def __init__(self, image_bytes:bytes):
|
|
99
|
+
super().__init__()
|
|
100
|
+
if not isinstance(image_bytes, bytes):
|
|
101
|
+
raise ValueError(f'{__class__.__name__}\nUse: bytes, não {type(image_bytes)}')
|
|
102
|
+
self.image_bytes:bytes = image_bytes
|
|
103
|
+
|
|
104
|
+
def set_gaussian_blur(self):
|
|
105
|
+
img_pil = Image.open(BytesIO(self.image_bytes))
|
|
106
|
+
# Aplicar um desfoque semelhante ao cv2.GaussianBlur
|
|
107
|
+
blurred:Image.Image = img_pil.filter(ImageFilter.GaussianBlur(radius=1))
|
|
108
|
+
buff = BytesIO()
|
|
109
|
+
blurred.save(buff, 'PNG')
|
|
110
|
+
self.image_bytes = buff.getvalue()
|
|
111
|
+
buff.close()
|
|
112
|
+
|
|
113
|
+
def set_background_black(self):
|
|
114
|
+
img_pil = Image.open(BytesIO(self.image_bytes))
|
|
115
|
+
_result = ImageOps.invert(img_pil.convert("L")) # Converte para escala de cinza
|
|
116
|
+
buff = BytesIO()
|
|
117
|
+
_result.save(buff, 'PNG')
|
|
118
|
+
self.image_bytes = buff.getvalue()
|
|
119
|
+
buff.close()
|
|
120
|
+
|
|
121
|
+
def set_background_gray(self):
|
|
122
|
+
# Abrir a imagem a partir dos bytes
|
|
123
|
+
img_pil = Image.open(BytesIO(self.image_bytes))
|
|
124
|
+
|
|
125
|
+
# Converter para escala de cinza
|
|
126
|
+
gray_img = img_pil.convert("L")
|
|
127
|
+
|
|
128
|
+
# Inverter as cores
|
|
129
|
+
inverted_img = ImageOps.invert(gray_img)
|
|
130
|
+
|
|
131
|
+
# Aplicar um threshold invertido: força fundo branco e texto preto
|
|
132
|
+
threshold = 128 # Ajuste conforme necessário
|
|
133
|
+
binary_img = inverted_img.point(lambda x: 0 if x > threshold else 150) # Invertido aqui!
|
|
134
|
+
|
|
135
|
+
# Salvar de volta para bytes
|
|
136
|
+
buff = BytesIO()
|
|
137
|
+
binary_img.save(buff, 'PNG')
|
|
138
|
+
self.image_bytes = buff.getvalue()
|
|
139
|
+
buff.close()
|
|
140
|
+
|
|
141
|
+
def to_file(self, output_path: File):
|
|
142
|
+
img:Image.Image = Image.open(self.image_bytes)
|
|
143
|
+
img.save(output_path.absolute(), 'PNG')
|
|
144
|
+
|
|
145
|
+
def to_bytes(self) -> bytes:
|
|
146
|
+
buff_img = BytesIO()
|
|
147
|
+
Image.open(BytesIO(self.image_bytes)).save(buff_img, 'PNG')
|
|
148
|
+
bytes_img = buff_img.getvalue()
|
|
149
|
+
buff_img.close()
|
|
150
|
+
return bytes_img
|
|
151
|
+
|
|
152
|
+
def to_pil(self) -> Image:
|
|
153
|
+
return Image.open(BytesIO(self.image_bytes))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class ImageInverColor(ABCInvertColor):
|
|
157
|
+
"""
|
|
158
|
+
Escurecer texto em imagens.
|
|
159
|
+
"""
|
|
160
|
+
def __init__(self, invert_color:ABCInvertColor):
|
|
161
|
+
if not isinstance(invert_color, ABCInvertColor):
|
|
162
|
+
raise ValueError(f'{__class__.__name__}\nUse: ABCInvertColor(), não {type(invert_color)}')
|
|
163
|
+
self.invert_color:ABCInvertColor = invert_color
|
|
164
|
+
|
|
165
|
+
def set_gaussian_blur(self):
|
|
166
|
+
self.invert_color.set_gaussian_blur()
|
|
167
|
+
|
|
168
|
+
def set_background_black(self):
|
|
169
|
+
self.invert_color.set_background_black()
|
|
170
|
+
|
|
171
|
+
def set_background_gray(self):
|
|
172
|
+
self.invert_color.set_background_gray()
|
|
173
|
+
|
|
174
|
+
def to_file(self, output_path:File):
|
|
175
|
+
return self.invert_color.to_file(output_path)
|
|
176
|
+
|
|
177
|
+
def to_bytes(self):
|
|
178
|
+
return self.invert_color.to_bytes()
|
|
179
|
+
|
|
180
|
+
@classmethod
|
|
181
|
+
def create_from_file(cls, f:File, *, library:LibraryImage=LibraryImage.OPENCV) -> 'ImageInverColor':
|
|
182
|
+
img:ImageObject = ImageObject.create_from_file(f, library=library)
|
|
183
|
+
if library == LibraryImage.OPENCV:
|
|
184
|
+
inv:ABCInvertColor = ImplementInvertColorOpenCv(f.path.read_bytes())
|
|
185
|
+
elif library == LibraryImage.PIL:
|
|
186
|
+
inv:ABCInvertColor = ImplementInvertColorPIL(f.path.read_bytes())
|
|
187
|
+
else:
|
|
188
|
+
raise NotImplementedError(
|
|
189
|
+
f'{__class__.__name__}\nUse: LibraryImage.OPENCV, ou LibraryImage.PIL, não {type(library)}'
|
|
190
|
+
)
|
|
191
|
+
return cls(inv)
|
|
192
|
+
|
|
193
|
+
@classmethod
|
|
194
|
+
def create_from_bytes(cls, bt:bytes, *, library:LibraryImage=LibraryImage.OPENCV) -> 'ImageInverColor':
|
|
195
|
+
if library == LibraryImage.OPENCV:
|
|
196
|
+
invert_color:ABCInvertColor = ImplementInvertColorOpenCv(bt)
|
|
197
|
+
elif library == LibraryImage.PIL:
|
|
198
|
+
invert_color:ABCInvertColor = ImplementInvertColorPIL(bt)
|
|
199
|
+
else:
|
|
200
|
+
raise NotImplementedError(
|
|
201
|
+
f'{__class__.__name__}\nUse: LibraryImage.OPENCV, ou LibraryImage.PIL, não {type(library)}'
|
|
202
|
+
)
|
|
203
|
+
return cls(invert_color)
|
|
204
|
+
|
|
205
|
+
#=============================================================================#
|
|
206
|
+
# Manipulação de imagens
|
|
207
|
+
#=============================================================================#
|
|
208
|
+
class ImageObjectPIL(ABCImageObject):
|
|
209
|
+
"""
|
|
210
|
+
Implementação de ImageObject usando PIL.
|
|
211
|
+
"""
|
|
212
|
+
def __init__(self, image_bytes:bytes):
|
|
213
|
+
super().__init__()
|
|
214
|
+
if not isinstance(image_bytes, bytes):
|
|
215
|
+
raise ValueError(f'{__class__.__name__} Use: bytes, não {type(image_bytes)}')
|
|
216
|
+
self.current_library = LibraryImage.PIL
|
|
217
|
+
self.image_bytes = image_bytes
|
|
218
|
+
self.max_size:Tuple[int, int] = (1980, 720) # Dimensões máximas, altere se necessário.
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
img = Image.open(BytesIO(image_bytes))
|
|
222
|
+
except Exception as e:
|
|
223
|
+
raise ValueError(f"{__class__.__name__}\nPIL: Bytes de imagem inválidos")
|
|
224
|
+
else:
|
|
225
|
+
self.dimensions = img.size
|
|
226
|
+
|
|
227
|
+
# Redimensionar, se as dimensões forem maior que self.max_size.
|
|
228
|
+
if img.width > self.max_size[0] or img.height > self.max_size[1]:
|
|
229
|
+
#img.thumbnail(self.max_size, Image.LANCZOS) # Mantém proporção
|
|
230
|
+
buff_image:BytesIO = BytesIO()
|
|
231
|
+
img.save(buff_image, format='PNG', optimize=True, quality=80)
|
|
232
|
+
self.image_bytes = buff_image.getvalue()
|
|
233
|
+
self.dimensions = img.size
|
|
234
|
+
buff_image.seek(0)
|
|
235
|
+
buff_image.close()
|
|
236
|
+
del img
|
|
237
|
+
|
|
238
|
+
def is_paisagem(self) -> bool:
|
|
239
|
+
"""
|
|
240
|
+
Retorna True se a imagem estiver em modo paisagem.
|
|
241
|
+
"""
|
|
242
|
+
width, height = self.dimensions
|
|
243
|
+
return width > height
|
|
244
|
+
|
|
245
|
+
def set_rotation(self, rotation: LibraryImage = LibraryImage.ROTATION_180):
|
|
246
|
+
img = Image.open(BytesIO(self.image_bytes))
|
|
247
|
+
if rotation == LibraryImage.ROTATION_90:
|
|
248
|
+
img = img.transpose(Image.Transpose.ROTATE_90)
|
|
249
|
+
elif rotation == LibraryImage.ROTATION_180:
|
|
250
|
+
img = img.transpose(Image.Transpose.ROTATE_180)
|
|
251
|
+
elif rotation == LibraryImage.ROTATION_270:
|
|
252
|
+
img = img.transpose(Image.Transpose.ROTATE_270)
|
|
253
|
+
else:
|
|
254
|
+
return
|
|
255
|
+
new_bytes = BytesIO()
|
|
256
|
+
img.save(new_bytes, format='PNG')
|
|
257
|
+
self.image_bytes = new_bytes.getvalue()
|
|
258
|
+
|
|
259
|
+
def set_paisagem(self):
|
|
260
|
+
if not self.is_paisagem():
|
|
261
|
+
img = Image.open(BytesIO(self.image_bytes))
|
|
262
|
+
img = img.transpose(Image.Transpose.ROTATE_90) # Rotaciona -90 graus
|
|
263
|
+
new_bytes = BytesIO()
|
|
264
|
+
img.save(new_bytes, format='PNG')
|
|
265
|
+
self.image_bytes = new_bytes.getvalue()
|
|
266
|
+
|
|
267
|
+
def set_optimize(self):
|
|
268
|
+
optimized_bytes = BytesIO()
|
|
269
|
+
img = self.to_image_pil()
|
|
270
|
+
img.save(optimized_bytes, format='PNG', optimize=True, quality=80)
|
|
271
|
+
self.image_bytes = optimized_bytes.getvalue()
|
|
272
|
+
|
|
273
|
+
def set_background_black(self):
|
|
274
|
+
inv = ImageInverColor.create_from_bytes(self.to_bytes(), library=LibraryImage.PIL)
|
|
275
|
+
inv.set_background_black()
|
|
276
|
+
self.image_bytes = inv.to_bytes()
|
|
277
|
+
|
|
278
|
+
def set_background_gray(self):
|
|
279
|
+
inv = ImageInverColor.create_from_bytes(self.to_bytes(), library=LibraryImage.PIL)
|
|
280
|
+
inv.set_background_gray()
|
|
281
|
+
self.image_bytes = inv.to_bytes()
|
|
282
|
+
|
|
283
|
+
def set_gaussian(self):
|
|
284
|
+
inv = ImageInverColor.create_from_bytes(self.to_bytes(), library=LibraryImage.PIL)
|
|
285
|
+
inv.set_gaussian_blur()
|
|
286
|
+
self.image_bytes = inv.to_bytes()
|
|
287
|
+
|
|
288
|
+
def to_image_pil(self) -> Image.Image:
|
|
289
|
+
return Image.open(BytesIO(self.image_bytes))
|
|
290
|
+
|
|
291
|
+
def to_image_opencv(self) -> cv2.typing.MatLike:
|
|
292
|
+
nparr = np.frombuffer(self.image_bytes, np.uint8)
|
|
293
|
+
return cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
|
294
|
+
|
|
295
|
+
def to_bytes(self) -> bytes:
|
|
296
|
+
img = self.to_image_pil()
|
|
297
|
+
image_bytes = BytesIO()
|
|
298
|
+
img.save(image_bytes, format='PNG')
|
|
299
|
+
return image_bytes.getvalue()
|
|
300
|
+
|
|
301
|
+
def to_file(self, filepath: str):
|
|
302
|
+
try:
|
|
303
|
+
self.to_image_pil().save(filepath)
|
|
304
|
+
except ImportError:
|
|
305
|
+
raise ImportError("A biblioteca PIL não está instalada. Instale-a com 'pip install Pillow'.")
|
|
306
|
+
except Exception as e:
|
|
307
|
+
raise Exception(f"Erro ao salvar imagem PIL: {e}")
|
|
308
|
+
|
|
309
|
+
class ImageObjectOpenCV(ABCImageObject):
|
|
310
|
+
"""
|
|
311
|
+
Implementação de ImageObject usando OpenCV.
|
|
312
|
+
"""
|
|
313
|
+
def __init__(self, image_bytes):
|
|
314
|
+
super().__init__()
|
|
315
|
+
if not isinstance(image_bytes, bytes):
|
|
316
|
+
raise ValueError(f'{__class__.__name__} Use: bytes, não {type(image_bytes)}')
|
|
317
|
+
self.current_library = LibraryImage.OPENCV
|
|
318
|
+
self.image_bytes = image_bytes
|
|
319
|
+
self.max_size:Tuple[int, int] = (1980, 720)
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
nparr = np.frombuffer(self.image_bytes, np.uint8)
|
|
323
|
+
image_opencv:MatLike = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
|
324
|
+
except Exception as e:
|
|
325
|
+
print('-' * 80)
|
|
326
|
+
raise ValueError(f"\nCV2: Bytes de imagem OpenCV inválidos")
|
|
327
|
+
else:
|
|
328
|
+
self.dimensions = (image_opencv.shape[1], image_opencv.shape[0]) # (largura, altura)
|
|
329
|
+
|
|
330
|
+
# Redimensionar se necessário
|
|
331
|
+
if self.dimensions[0] > self.max_size[0] or self.dimensions[1] > self.max_size[1]:
|
|
332
|
+
h, w = image_opencv.shape[:2]
|
|
333
|
+
scale = min(self.max_size[0] / w, self.max_size[1] / h)
|
|
334
|
+
new_size = (int(w * scale), int(h * scale))
|
|
335
|
+
image_opencv = cv2.resize(image_opencv, new_size, interpolation=cv2.INTER_LANCZOS4)
|
|
336
|
+
# Converter a imagem de volta para bytes
|
|
337
|
+
_, encoded_img = cv2.imencode('.png', image_opencv)
|
|
338
|
+
self.image_bytes = encoded_img.tobytes()
|
|
339
|
+
# Atualizar dimensões após redimensionamento
|
|
340
|
+
self.dimensions = (image_opencv.shape[1], image_opencv.shape[0])
|
|
341
|
+
|
|
342
|
+
def is_paisagem(self) -> bool:
|
|
343
|
+
"""
|
|
344
|
+
Retorna True se a imagem estiver em modo paisagem.
|
|
345
|
+
"""
|
|
346
|
+
width, height = self.dimensions
|
|
347
|
+
return width > height
|
|
348
|
+
|
|
349
|
+
def set_rotation(self, rotation: LibraryImage = LibraryImage.ROTATION_180):
|
|
350
|
+
nparr = np.frombuffer(self.image_bytes, np.uint8)
|
|
351
|
+
img:MatLike = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
|
352
|
+
if rotation == LibraryImage.ROTATION_90:
|
|
353
|
+
img:MatLike = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)
|
|
354
|
+
elif rotation == LibraryImage.ROTATION_180:
|
|
355
|
+
img:MatLike = cv2.rotate(img, cv2.ROTATE_180)
|
|
356
|
+
elif rotation == LibraryImage.ROTATION_270:
|
|
357
|
+
img:MatLike = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)
|
|
358
|
+
img:MatLike = cv2.rotate(img, cv2.ROTATE_180)
|
|
359
|
+
else:
|
|
360
|
+
return
|
|
361
|
+
success, encoded_image = cv2.imencode('.png', img)
|
|
362
|
+
if success:
|
|
363
|
+
self.image_bytes = encoded_image.tobytes()
|
|
364
|
+
|
|
365
|
+
def set_paisagem(self):
|
|
366
|
+
if not self.is_paisagem():
|
|
367
|
+
nparr = np.frombuffer(self.image_bytes, np.uint8)
|
|
368
|
+
img:MatLike = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
|
369
|
+
img:MatLike = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE) # Rotaciona -90 graus
|
|
370
|
+
|
|
371
|
+
success, encoded_image = cv2.imencode('.png', img)
|
|
372
|
+
if success:
|
|
373
|
+
self.image_bytes = encoded_image.tobytes()
|
|
374
|
+
|
|
375
|
+
def set_optimize(self):
|
|
376
|
+
"""
|
|
377
|
+
Reduz o tamanho da imagem, e salva a imagen reduzida na propriedade bytes.
|
|
378
|
+
"""
|
|
379
|
+
imagem:MatLike = self.to_image_opencv()
|
|
380
|
+
_status, buffer = cv2.imencode(".png", imagem, [cv2.IMWRITE_JPEG_QUALITY, 80])
|
|
381
|
+
if _status:
|
|
382
|
+
self.image_bytes = buffer.tobytes()
|
|
383
|
+
|
|
384
|
+
def set_background_black(self):
|
|
385
|
+
inv = ImageInverColor.create_from_bytes(self.to_bytes(), library=LibraryImage.OPENCV)
|
|
386
|
+
inv.set_background_black()
|
|
387
|
+
self.image_bytes = inv.to_bytes()
|
|
388
|
+
|
|
389
|
+
def set_background_gray(self):
|
|
390
|
+
inv = ImageInverColor.create_from_bytes(self.to_bytes(), library=LibraryImage.OPENCV)
|
|
391
|
+
inv.set_background_gray()
|
|
392
|
+
self.image_bytes = inv.to_bytes()
|
|
393
|
+
|
|
394
|
+
def set_gaussian(self):
|
|
395
|
+
inv = ImageInverColor.create_from_bytes(self.to_bytes(), library=LibraryImage.OPENCV)
|
|
396
|
+
inv.set_gaussian_blur()
|
|
397
|
+
self.image_bytes = inv.to_bytes()
|
|
398
|
+
|
|
399
|
+
def to_image_pil(self) -> Image.Image:
|
|
400
|
+
return Image.open(BytesIO(self.image_bytes))
|
|
401
|
+
|
|
402
|
+
def to_image_opencv(self) -> cv2.typing.MatLike:
|
|
403
|
+
nparr = np.frombuffer(self.image_bytes, np.uint8)
|
|
404
|
+
return cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
|
405
|
+
|
|
406
|
+
def to_bytes(self) -> bytes:
|
|
407
|
+
_, buffer = cv2.imencode('.png', self.to_image_opencv()) # Codifica como PNG (ou use '.jpg' para JPEG)
|
|
408
|
+
return buffer.tobytes()
|
|
409
|
+
|
|
410
|
+
def to_file(self, filepath: str):
|
|
411
|
+
try:
|
|
412
|
+
img = self.to_image_opencv()
|
|
413
|
+
cv2.imwrite(filepath, img)
|
|
414
|
+
except ImportError:
|
|
415
|
+
raise ImportError("A biblioteca OpenCV não está instalada. Instale-a com 'pip install opencv-python'.")
|
|
416
|
+
except Exception as e:
|
|
417
|
+
raise Exception(f"Erro ao salvar imagem OpenCV: {e}")
|
|
418
|
+
|
|
419
|
+
class ImageObject(ABCImageObject):
|
|
420
|
+
"""
|
|
421
|
+
Facade para manipular imagens com PIL ou OPENCV
|
|
422
|
+
"""
|
|
423
|
+
def __init__(self, img_obj:ABCImageObject):
|
|
424
|
+
super().__init__()
|
|
425
|
+
self.img_obj:ABCImageObject = img_obj
|
|
426
|
+
self.current_library:LibraryImage = self.img_obj.current_library
|
|
427
|
+
|
|
428
|
+
def set_optimize(self):
|
|
429
|
+
"""
|
|
430
|
+
Reduz o tamanho da imagem, e salva a imagen reduzida na propriedade bytes.
|
|
431
|
+
"""
|
|
432
|
+
self.img_obj.set_optimize()
|
|
433
|
+
|
|
434
|
+
def set_rotation(self, rotation: LibraryImage = LibraryImage.ROTATION_180):
|
|
435
|
+
self.img_obj.set_rotation(rotation)
|
|
436
|
+
|
|
437
|
+
def set_paisagem(self):
|
|
438
|
+
self.img_obj.set_paisagem()
|
|
439
|
+
|
|
440
|
+
def set_background_gray(self):
|
|
441
|
+
self.img_obj.set_background_gray()
|
|
442
|
+
|
|
443
|
+
def set_background_black(self):
|
|
444
|
+
self.img_obj.set_background_black()
|
|
445
|
+
|
|
446
|
+
def set_gaussian(self):
|
|
447
|
+
self.img_obj.set_gaussian()
|
|
448
|
+
|
|
449
|
+
def to_image_pil(self) -> Image.Image:
|
|
450
|
+
return self.img_obj.to_image_pil()
|
|
451
|
+
|
|
452
|
+
def to_image_opencv(self) -> cv2.typing.MatLike:
|
|
453
|
+
return self.img_obj.to_image_opencv()
|
|
454
|
+
|
|
455
|
+
def to_file(self, f:File):
|
|
456
|
+
self.img_obj.to_file(f.absolute())
|
|
457
|
+
|
|
458
|
+
def is_paisagem(self) -> bool:
|
|
459
|
+
return self.img_obj.is_paisagem()
|
|
460
|
+
|
|
461
|
+
def to_bytes(self) -> bytes:
|
|
462
|
+
return self.img_obj.to_bytes()
|
|
463
|
+
|
|
464
|
+
@classmethod
|
|
465
|
+
def create_from_bytes(cls, image_bytes: bytes, *, library:LibraryImage=LibraryImage.OPENCV) -> 'ImageObject':
|
|
466
|
+
if library == LibraryImage.PIL:
|
|
467
|
+
img = ImageObjectPIL(image_bytes)
|
|
468
|
+
elif library == LibraryImage.OPENCV:
|
|
469
|
+
img = ImageObjectOpenCV(image_bytes)
|
|
470
|
+
else:
|
|
471
|
+
raise ValueError("Biblioteca de imagem inválida.")
|
|
472
|
+
return cls(img)
|
|
473
|
+
|
|
474
|
+
@classmethod
|
|
475
|
+
def create_from_file(cls, filepath: File, *, library:LibraryImage=LibraryImage.OPENCV) -> 'ImageObject':
|
|
476
|
+
print(f'Obtendo imagem do arquivo: {filepath.basename()}')
|
|
477
|
+
try:
|
|
478
|
+
if library == LibraryImage.PIL:
|
|
479
|
+
image = ImageObjectPIL(filepath.path.read_bytes())
|
|
480
|
+
elif library == LibraryImage.OPENCV:
|
|
481
|
+
image = ImageObjectOpenCV(filepath.path.read_bytes())
|
|
482
|
+
else:
|
|
483
|
+
raise ValueError("Biblioteca de imagem inválida.")
|
|
484
|
+
except FileNotFoundError:
|
|
485
|
+
print(f'O arquivo não existe: {filepath.absolute()}')
|
|
486
|
+
raise
|
|
487
|
+
except Exception as e:
|
|
488
|
+
print(e, '\n')
|
|
489
|
+
print('=' * 70)
|
|
490
|
+
raise ValueError(f'{__class__.__name__} Arquivo inválido: [{filepath.absolute()}]')
|
|
491
|
+
return cls(image)
|
|
492
|
+
|
|
493
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from .m_text import (
|
|
3
|
+
DataString, TableText, ConvertDate, ArrayString, LibraryDates,
|
|
4
|
+
print_line, print_title, save_data
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
from .m_imgelib import LibraryImage, ABCImageObject, ABCInvertColor
|
|
8
|
+
from .m_pdf_page import LibraryPDF, ABCPagePdf
|
|
9
|
+
from .m_pdf_document import ABCDocumentPdf
|
|
10
|
+
from .m_pdf_convert import ABCConvertPdf, ABCImageConvertPdf, ABCDocumentStream
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import List, Tuple
|
|
8
|
+
from PIL import Image
|
|
9
|
+
from cv2.typing import MatLike
|
|
10
|
+
import cv2
|
|
11
|
+
from soup_files import File
|
|
12
|
+
|
|
13
|
+
class LibraryImage(Enum):
|
|
14
|
+
PIL = 1
|
|
15
|
+
OPENCV = 2
|
|
16
|
+
ROTATION_90 = 90
|
|
17
|
+
ROTATION_180 = 180
|
|
18
|
+
ROTATION_270 = 270
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ABCInvertColor(ABC):
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def to_file(self, output_path:File):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def to_bytes(self) -> bytes:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def set_background_black(self):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def set_background_gray(self):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def set_gaussian_blur(self):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ABCImageObject(ABC):
|
|
46
|
+
"""
|
|
47
|
+
Classe abstrata base para objetos de imagem.
|
|
48
|
+
"""
|
|
49
|
+
def __init__(self):
|
|
50
|
+
super().__init__()
|
|
51
|
+
self.current_library: LibraryImage = None
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def is_paisagem(self) -> bool:
|
|
55
|
+
"""
|
|
56
|
+
Retorna True se a imagem estiver em modo paisagem.
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def set_optimize(self):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
@abstractmethod
|
|
65
|
+
def to_bytes(self) -> bytes:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def set_paisagem(self):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def set_rotation(self, rotation: LibraryImage = LibraryImage.ROTATION_180):
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def set_background_gray(self):
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def set_background_black(self):
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
@abstractmethod
|
|
85
|
+
def set_gaussian(self):
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def to_image_pil(self) -> Image.Image:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
@abstractmethod
|
|
93
|
+
def to_image_opencv(self) -> cv2.typing.MatLike:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
@abstractmethod
|
|
97
|
+
def to_file(self, filepath: object):
|
|
98
|
+
"""
|
|
99
|
+
Salva a imagem em um arquivo, garantindo modo paisagem.
|
|
100
|
+
"""
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|