rawmaker 2.40.3__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.
- letty/__init__.py +46 -0
- letty/cli.py +63 -0
- letty/optimizer.py +138 -0
- letty/quality/__init__.py +8 -0
- letty/quality/whitespace.py +50 -0
- letty/strategy.py +8 -0
- rawmaker/__init__.py +29 -0
- rawmaker/__main__.py +13 -0
- rawmaker/__patch__.py +36 -0
- rawmaker/cli.py +206 -0
- rawmaker/cli_automate.py +69 -0
- rawmaker/converter/__init__.py +8 -0
- rawmaker/converter/basic.py +174 -0
- rawmaker/converter/images.py +168 -0
- rawmaker/date.py +83 -0
- rawmaker/destination.py +202 -0
- rawmaker/error.py +34 -0
- rawmaker/features/__init__.py +138 -0
- rawmaker/features/annotation.py +254 -0
- rawmaker/features/border.py +172 -0
- rawmaker/features/boxes.py +153 -0
- rawmaker/features/figures.py +24 -0
- rawmaker/features/fonts.py +229 -0
- rawmaker/features/formula.py +16 -0
- rawmaker/features/horizontals.py +132 -0
- rawmaker/features/images.py +155 -0
- rawmaker/features/line.py +337 -0
- rawmaker/features/outlines.py +123 -0
- rawmaker/features/text.py +91 -0
- rawmaker/fonts/__init__.py +8 -0
- rawmaker/fonts/parser.py +354 -0
- rawmaker/images/__init__.py +8 -0
- rawmaker/images/info.py +35 -0
- rawmaker/miner/__init__.py +8 -0
- rawmaker/miner/char.py +42 -0
- rawmaker/miner/colorspace.py +75 -0
- rawmaker/miner/images.py +448 -0
- rawmaker/miner/position.py +121 -0
- rawmaker/miner/rawchar.py +207 -0
- rawmaker/miner/text.py +833 -0
- rawmaker/miner/underline.py +66 -0
- rawmaker/parameter.py +130 -0
- rawmaker/patch/__init__.py +8 -0
- rawmaker/patch/ltchar.py +79 -0
- rawmaker/reader.py +97 -0
- rawmaker/text/__init__.py +8 -0
- rawmaker/text/chars.py +24 -0
- rawmaker/text/data.py +47 -0
- rawmaker/text/superfast.py +91 -0
- rawmaker/text/wordbox.py +95 -0
- rawmaker/utils.py +44 -0
- rawmaker-2.40.3.dist-info/METADATA +51 -0
- rawmaker-2.40.3.dist-info/RECORD +63 -0
- rawmaker-2.40.3.dist-info/WHEEL +5 -0
- rawmaker-2.40.3.dist-info/entry_points.txt +6 -0
- rawmaker-2.40.3.dist-info/licenses/LICENSE +21 -0
- rawmaker-2.40.3.dist-info/top_level.txt +3 -0
- spacestation/__init__.py +18 -0
- spacestation/cli.py +51 -0
- spacestation/features/__init__.py +8 -0
- spacestation/features/chardist.py +85 -0
- spacestation/features/worddist.py +57 -0
- spacestation/features/wspace.py +130 -0
rawmaker/miner/images.py
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# C O P Y R I G H T
|
|
3
|
+
# -----------------------------------------------------------------------------
|
|
4
|
+
# Copyright (c) 2019-2023 by Helmut Konrad Schewe. All rights reserved.
|
|
5
|
+
# This file is property of Helmut Konrad Schewe. Any unauthorized copy,
|
|
6
|
+
# use or distribution is an offensive act against international law and may
|
|
7
|
+
# be prosecuted under federal law. Its content is company confidential.
|
|
8
|
+
# =============================================================================
|
|
9
|
+
"""Extract images out of a PDF-Page
|
|
10
|
+
|
|
11
|
+
There are 2 types of images to extract:
|
|
12
|
+
|
|
13
|
+
One the one hand, there is the image which stored in internal format
|
|
14
|
+
One the other hand, there is the image that is generated by a color table
|
|
15
|
+
|
|
16
|
+
Furthermore there are some images which are composed out of other
|
|
17
|
+
images. Sometimes one Image is splitted into two or three parts. The
|
|
18
|
+
maximum of this split is one image per pixel line.
|
|
19
|
+
|
|
20
|
+
NOTE Currently this feature is experimental.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import array
|
|
24
|
+
import collections
|
|
25
|
+
import io
|
|
26
|
+
import os
|
|
27
|
+
|
|
28
|
+
import pdfminer.converter
|
|
29
|
+
import pdfminer.image
|
|
30
|
+
import pdfminer.layout
|
|
31
|
+
import pdfminer.pdfdocument
|
|
32
|
+
import pdfminer.pdfinterp
|
|
33
|
+
import pdfminer.pdftypes
|
|
34
|
+
import pdfminer.psparser
|
|
35
|
+
import PIL.Image
|
|
36
|
+
import PIL.ImageDraw
|
|
37
|
+
import PIL.ImageDraw2
|
|
38
|
+
import PIL.PngImagePlugin
|
|
39
|
+
import utilo
|
|
40
|
+
|
|
41
|
+
import rawmaker.converter.images
|
|
42
|
+
import rawmaker.miner.colorspace
|
|
43
|
+
|
|
44
|
+
MergedImage = collections.namedtuple('MergedImage', 'image, ext, bounding')
|
|
45
|
+
WrittenImage = collections.namedtuple('WrittenImage', 'filename, bounding')
|
|
46
|
+
LTImages = list[pdfminer.layout.LTImage]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def extract_images(
|
|
50
|
+
document: pdfminer.pdfdocument.PDFDocument,
|
|
51
|
+
outputfolder,
|
|
52
|
+
pages: tuple = None,
|
|
53
|
+
) -> dict:
|
|
54
|
+
"""Extract all images of `document` of selected `pages`.
|
|
55
|
+
|
|
56
|
+
Hint: `Outputfolder` is only created if `document` contains some images.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
document: source to extract images from
|
|
60
|
+
outputfolder(str): write extracted images to
|
|
61
|
+
pages(tuple): selective list to process pages
|
|
62
|
+
Returns:
|
|
63
|
+
dict with one list per page with containing images of this page
|
|
64
|
+
"""
|
|
65
|
+
# ensure that page computation works correct
|
|
66
|
+
if pages:
|
|
67
|
+
pages = utilo.ensure_tuple(pages)
|
|
68
|
+
pages = sorted(pages)
|
|
69
|
+
# Processing layout
|
|
70
|
+
content = pdfminer.pdfpage.PDFPage.create_pages(document)
|
|
71
|
+
# setup collector
|
|
72
|
+
collect = CollectAndMerge(outputfolder)
|
|
73
|
+
firstpage = pages[0] if pages else 0
|
|
74
|
+
interpreter = rawmaker.converter.images.create_fastimageextractor(
|
|
75
|
+
collect.imagereciver,
|
|
76
|
+
firstpage=firstpage,
|
|
77
|
+
)
|
|
78
|
+
# iterate pages
|
|
79
|
+
with utilo.SkipCollector(pages) as collector:
|
|
80
|
+
for number, page in enumerate(content):
|
|
81
|
+
if collector.skip(number):
|
|
82
|
+
continue
|
|
83
|
+
page.pageid = number
|
|
84
|
+
interpreter.process_page(page)
|
|
85
|
+
# determine result
|
|
86
|
+
result = collect.merge_and_write()
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class CollectAndMerge:
|
|
91
|
+
|
|
92
|
+
def __init__(self, outputfolder):
|
|
93
|
+
self.outputfolder = outputfolder
|
|
94
|
+
self.to_merge = collections.defaultdict(list)
|
|
95
|
+
self.written = collections.defaultdict(list)
|
|
96
|
+
|
|
97
|
+
def imagereciver(self, page, image):
|
|
98
|
+
self.to_merge[page].append(image)
|
|
99
|
+
|
|
100
|
+
def merge_and_write(self) -> dict:
|
|
101
|
+
if not self.to_merge:
|
|
102
|
+
# no images given
|
|
103
|
+
return {}
|
|
104
|
+
os.makedirs(self.outputfolder, exist_ok=True)
|
|
105
|
+
merged = merge_document_images(self.to_merge)
|
|
106
|
+
# write merged images
|
|
107
|
+
for page, values in merged.items():
|
|
108
|
+
for index, extracted in enumerate(values):
|
|
109
|
+
if not extracted:
|
|
110
|
+
continue
|
|
111
|
+
written = write_image(
|
|
112
|
+
extracted,
|
|
113
|
+
write_to=self.outputfolder,
|
|
114
|
+
page=page,
|
|
115
|
+
index=index,
|
|
116
|
+
)
|
|
117
|
+
self.written[page].append(written)
|
|
118
|
+
self.to_merge.clear()
|
|
119
|
+
# convert defaultdict to normal dict, remove empty pages
|
|
120
|
+
result = {key: value for key, value in self.written.items() if value}
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
IMAGE_WIDTH_MAX = 2048
|
|
125
|
+
IMAGE_HEIGHT_MAX = 2048
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def write_image(extracted, write_to, page, index) -> WrittenImage:
|
|
129
|
+
"""Write image `extracted` to directory `write_to`.
|
|
130
|
+
|
|
131
|
+
The file is named {page}_{index}.{extracted.ext}.
|
|
132
|
+
"""
|
|
133
|
+
assert extracted
|
|
134
|
+
ext = extracted.ext
|
|
135
|
+
filename = f'{page}_{index}.{ext}'
|
|
136
|
+
if isinstance(extracted.image, PIL.Image.Image):
|
|
137
|
+
outpath = os.path.join(write_to, filename)
|
|
138
|
+
with open(outpath, mode='wb') as output:
|
|
139
|
+
ext = ext.replace('jpg', 'jpeg')
|
|
140
|
+
try:
|
|
141
|
+
extracted.image.save(output, format=ext)
|
|
142
|
+
except Exception: # pylint:disable=broad-except
|
|
143
|
+
utilo.error(f'could not use save method: {filename}')
|
|
144
|
+
else:
|
|
145
|
+
try:
|
|
146
|
+
# images writer add file extention bt themself
|
|
147
|
+
writer = pdfminer.image.ImageWriter(write_to)
|
|
148
|
+
rawimage = extracted.image
|
|
149
|
+
rawimage.name = f'{page}_{index}'
|
|
150
|
+
if rawimage.width < IMAGE_WIDTH_MAX and rawimage.height < IMAGE_HEIGHT_MAX:
|
|
151
|
+
raw_data = rawimage.stream.get_rawdata()
|
|
152
|
+
if not raw_data:
|
|
153
|
+
utilo.error(f'empty image data, {rawimage.name}')
|
|
154
|
+
else:
|
|
155
|
+
writer.export_image(rawimage)
|
|
156
|
+
else:
|
|
157
|
+
msg = f'skip image size: {rawimage.srcsize} name: {rawimage.name}'
|
|
158
|
+
utilo.info(msg)
|
|
159
|
+
except pdfminer.pdftypes.PDFNotImplementedError as error:
|
|
160
|
+
utilo.error(f'could not export: {error}')
|
|
161
|
+
except TypeError:
|
|
162
|
+
utilo.error(f'empty export: {extracted.image.name}')
|
|
163
|
+
except ValueError:
|
|
164
|
+
utilo.error(f'decompression error: {extracted.image.name}')
|
|
165
|
+
return WrittenImage(filename=filename, bounding=extracted.bounding)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def merge_document_images(items):
|
|
169
|
+
result = collections.defaultdict(list)
|
|
170
|
+
# merge pages by yposition
|
|
171
|
+
for page, content in items.items():
|
|
172
|
+
merged = merge_page(content, page)
|
|
173
|
+
result[page].extend(merged)
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def merge_page(images: LTImages, page: int):
|
|
178
|
+
todo = [
|
|
179
|
+
utilo.roundme((image.x0, image.y0, image.x1, image.y1))
|
|
180
|
+
for image in images
|
|
181
|
+
]
|
|
182
|
+
# cluster image parts into mergable image
|
|
183
|
+
lookup = {str(item): line for item, line in zip(todo, images)}
|
|
184
|
+
# assert len(lookup) == len(todo), f'{len(lookup)} != {len(todo)}'
|
|
185
|
+
grouped = group_rectangles(todo)
|
|
186
|
+
# convert back
|
|
187
|
+
lines = [[lookup[str(item)] for item in group] for group in grouped]
|
|
188
|
+
result = []
|
|
189
|
+
try:
|
|
190
|
+
result = [raw_images_merge(item) for item in lines]
|
|
191
|
+
except ValueError as error:
|
|
192
|
+
utilo.error(f'could not parse images on page: {page}')
|
|
193
|
+
utilo.error(error)
|
|
194
|
+
return result
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def group_rectangles(rectangles):
|
|
198
|
+
"""Split potential images by distance in y-coordiante."""
|
|
199
|
+
border = range(0, 1000, 10)
|
|
200
|
+
bucket = utilo.Buckets(border, sorting=True)
|
|
201
|
+
bucket.selector = lambda x: x[1] # TODO: REMOVE THIS HACK
|
|
202
|
+
for item in rectangles:
|
|
203
|
+
bucket.add(item)
|
|
204
|
+
grouped = utilo.groupby_empty(bucket)
|
|
205
|
+
if not grouped:
|
|
206
|
+
return []
|
|
207
|
+
# merge neighbors which are huger than bucket size
|
|
208
|
+
result = [list(grouped[0])]
|
|
209
|
+
for current in grouped[1:]:
|
|
210
|
+
before = result[-1][-1]
|
|
211
|
+
if utilo.near(before[3], current[0][1], diff=5.0):
|
|
212
|
+
result[-1].extend(current)
|
|
213
|
+
elif any(utilo.rectangles_intersecting(result[-1], item) for item in current): # yapf:disable
|
|
214
|
+
# verify if any rectangle intersects to detect rectangles
|
|
215
|
+
# inside each other
|
|
216
|
+
result[-1].extend(current)
|
|
217
|
+
else:
|
|
218
|
+
result.append(list(current))
|
|
219
|
+
return result
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
BITMAP = '1'
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# pylint:disable=R1260,R0914,R0915
|
|
226
|
+
def raw_images_merge(images: LTImages) -> MergedImage:
|
|
227
|
+
"""Merge list of images to one image."""
|
|
228
|
+
ext = extention(images[0])
|
|
229
|
+
bounding = tuple(images[0].bbox)
|
|
230
|
+
if ext == 'jbig2':
|
|
231
|
+
images = [jbig2(images[0])]
|
|
232
|
+
ext = 'jpg'
|
|
233
|
+
if len(images) == 1:
|
|
234
|
+
# TODO: png is not supported by pdfimage exporter properly
|
|
235
|
+
if ext != 'png':
|
|
236
|
+
# no merge required
|
|
237
|
+
return MergedImage(images[0], ext, bounding)
|
|
238
|
+
utilo.debug(f'extraction not supported: {images[0]}')
|
|
239
|
+
# determine rectangle bounding
|
|
240
|
+
x00 = min(item.x0 for item in images) # pylint:disable=no-member
|
|
241
|
+
x11 = max(item.x1 for item in images) # pylint:disable=no-member
|
|
242
|
+
y00 = min(item.y0 for item in images) # pylint:disable=no-member
|
|
243
|
+
y11 = max(item.y1 for item in images) # pylint:disable=no-member
|
|
244
|
+
# create empty image to render sub images into
|
|
245
|
+
image_width = x11 - x00
|
|
246
|
+
image_height = y11 - y00
|
|
247
|
+
size = (int(image_width), int(image_height))
|
|
248
|
+
mode = 'RGB'
|
|
249
|
+
result = PIL.Image.new(mode, size, color=0)
|
|
250
|
+
renderer = PIL.ImageDraw.Draw(result, mode=mode)
|
|
251
|
+
# render sub-images
|
|
252
|
+
for image in images:
|
|
253
|
+
ext = extention(image)
|
|
254
|
+
current = image_fromlt(image)
|
|
255
|
+
if not current:
|
|
256
|
+
continue
|
|
257
|
+
# render to common image
|
|
258
|
+
current = ensure_bitmap(current)
|
|
259
|
+
renderer.bitmap((image.x0 - x00, image.y0 - y00), bitmap=current) # pylint:disable=no-member
|
|
260
|
+
# update bottom bounding of merged rectangle
|
|
261
|
+
multi_bounding = (x00, y00, x11, y11)
|
|
262
|
+
return MergedImage(result, ext, multi_bounding)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def ensure_bitmap(image):
|
|
266
|
+
if isinstance(image, PIL.PngImagePlugin.PngImageFile):
|
|
267
|
+
image = image.convert(mode=BITMAP, colors=1024, palette='1')
|
|
268
|
+
return image
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def jbig2(image):
|
|
272
|
+
# convert size, cause later fill method requests int
|
|
273
|
+
size = (int(image.width), int(image.height))
|
|
274
|
+
|
|
275
|
+
monochrom = '1'
|
|
276
|
+
result = PIL.Image.new(mode=monochrom, size=size, color=1)
|
|
277
|
+
renderer = PIL.ImageDraw.Draw(result, mode=monochrom)
|
|
278
|
+
|
|
279
|
+
data = image.stream.get_data()
|
|
280
|
+
width = image.width
|
|
281
|
+
for cursor, item in enumerate(data):
|
|
282
|
+
cursor = cursor * 8
|
|
283
|
+
x, y = cursor % width, cursor // (width / 8)
|
|
284
|
+
for pos in range(8):
|
|
285
|
+
datum = item << pos & 0b00000001
|
|
286
|
+
renderer.point((x + pos, y), datum)
|
|
287
|
+
return result
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def image_fromlt(image) -> PIL.Image: # pylint:disable=R0912
|
|
291
|
+
try:
|
|
292
|
+
colorspace = rawmaker.miner.colorspace.parse(image.colorspace)
|
|
293
|
+
except AttributeError as error:
|
|
294
|
+
utilo.print_stacktrace()
|
|
295
|
+
utilo.error(error)
|
|
296
|
+
colorspace = 'DeviceRGB'
|
|
297
|
+
try:
|
|
298
|
+
data = image.stream.get_data()
|
|
299
|
+
except ValueError as error:
|
|
300
|
+
utilo.error(error)
|
|
301
|
+
return None
|
|
302
|
+
except pdfminer.pdftypes.PDFNotImplementedError as error:
|
|
303
|
+
if 'JPXDecode' in str(error):
|
|
304
|
+
utilo.debug(error)
|
|
305
|
+
utilo.debug('use own png converter')
|
|
306
|
+
rawdata = image.stream.get_rawdata()
|
|
307
|
+
return png_load(rawdata)
|
|
308
|
+
utilo.error(error)
|
|
309
|
+
return None
|
|
310
|
+
# try to load images
|
|
311
|
+
mode = '1' # default mode
|
|
312
|
+
size = image.srcsize
|
|
313
|
+
bits = image.bits
|
|
314
|
+
if colorspace == 'DeviceGray':
|
|
315
|
+
mode = BITMAP
|
|
316
|
+
elif colorspace:
|
|
317
|
+
data = rgb256_decoder(data, colorspace, bits=bits)
|
|
318
|
+
else:
|
|
319
|
+
# black and white
|
|
320
|
+
mode = BITMAP
|
|
321
|
+
|
|
322
|
+
if bits == 4:
|
|
323
|
+
# TODO Do not know why this is required
|
|
324
|
+
size = (size[0] + 1, size[1])
|
|
325
|
+
mode = 'RGB'
|
|
326
|
+
if colorspace == 'DeviceRGB':
|
|
327
|
+
try:
|
|
328
|
+
# open jpg etc.
|
|
329
|
+
buffer = io.BytesIO(data)
|
|
330
|
+
current = PIL.Image.open(buffer)
|
|
331
|
+
except IOError:
|
|
332
|
+
try:
|
|
333
|
+
current = PIL.Image.frombytes(mode, size, data)
|
|
334
|
+
except ValueError:
|
|
335
|
+
# TODO: REMOVE THIS DIRTY SHIT
|
|
336
|
+
current = PIL.Image.frombytes('1', size, data)
|
|
337
|
+
else:
|
|
338
|
+
try:
|
|
339
|
+
current = PIL.Image.frombytes(mode, size, data)
|
|
340
|
+
except ValueError:
|
|
341
|
+
utilo.error(f'could not decode: {image}')
|
|
342
|
+
return None
|
|
343
|
+
# convert to bitmap
|
|
344
|
+
try:
|
|
345
|
+
current = current.convert(mode=BITMAP, colors=1024, palette='1')
|
|
346
|
+
loaded = io.BytesIO(current.tobitmap())
|
|
347
|
+
current = PIL.Image.open(loaded)
|
|
348
|
+
except OSError:
|
|
349
|
+
current = PIL.Image.new(mode, size, color=0)
|
|
350
|
+
return current
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def png_load(rawdata) -> PIL.Image:
|
|
354
|
+
"""Convert JPEG2000 to png data."""
|
|
355
|
+
# TODO: MOVE THIS CODE TO PDFMINER
|
|
356
|
+
buffer = io.BytesIO(rawdata)
|
|
357
|
+
buffer.seek(0)
|
|
358
|
+
with PIL.Image.open(buffer) as fp:
|
|
359
|
+
converted = io.BytesIO()
|
|
360
|
+
try:
|
|
361
|
+
fp.save(converted, 'png')
|
|
362
|
+
except OSError:
|
|
363
|
+
utilo.error('invalid png file, maybe an other type')
|
|
364
|
+
utilo.error(rawdata[0:50])
|
|
365
|
+
return None
|
|
366
|
+
converted.seek(0)
|
|
367
|
+
loaded = PIL.Image.open(converted)
|
|
368
|
+
return loaded
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def rgb256_decoder(data, dataspace, bits=8):
|
|
372
|
+
# RGB
|
|
373
|
+
# TODO: FIX TABLE ERRORS
|
|
374
|
+
table = []
|
|
375
|
+
if isinstance(dataspace, pdfminer.pdftypes.PDFStream):
|
|
376
|
+
dataspace = dataspace.get_data()
|
|
377
|
+
for index in range(0, len(dataspace), 3):
|
|
378
|
+
try:
|
|
379
|
+
table.append([
|
|
380
|
+
dataspace[index],
|
|
381
|
+
dataspace[index + 1],
|
|
382
|
+
dataspace[index + 2],
|
|
383
|
+
])
|
|
384
|
+
except IndexError:
|
|
385
|
+
utilo.debug('rgb256 decoder out of bounds')
|
|
386
|
+
return data
|
|
387
|
+
result = []
|
|
388
|
+
for item in data:
|
|
389
|
+
try:
|
|
390
|
+
if bits == 4:
|
|
391
|
+
lower = table[item & (15)]
|
|
392
|
+
higher = table[item & (15 >> 4 - 1)]
|
|
393
|
+
result.extend(lower)
|
|
394
|
+
result.extend(higher)
|
|
395
|
+
elif bits == 8:
|
|
396
|
+
result.extend(table[item])
|
|
397
|
+
else:
|
|
398
|
+
raise ValueError(f'{bits} bits not supported')
|
|
399
|
+
except IndexError:
|
|
400
|
+
utilo.debug('rgb256 decoder out of bounds')
|
|
401
|
+
return data
|
|
402
|
+
try:
|
|
403
|
+
data = array.array("B", result).tobytes()
|
|
404
|
+
except TypeError:
|
|
405
|
+
return data
|
|
406
|
+
return data
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def extention(image) -> str:
|
|
410
|
+
"""\
|
|
411
|
+
#JBIG2Decode: monochrom 1bit per pixel data
|
|
412
|
+
"""
|
|
413
|
+
decoder = {
|
|
414
|
+
'DCTDecode': 'jpg',
|
|
415
|
+
'JPXDecode': 'png',
|
|
416
|
+
'CCITTFaxDecode': 'tiff',
|
|
417
|
+
'Default': 'png',
|
|
418
|
+
'FlateDecode': 'png',
|
|
419
|
+
'JBIG2Decode': 'jbig2',
|
|
420
|
+
'RunLengthDecode': 'png',
|
|
421
|
+
}
|
|
422
|
+
try:
|
|
423
|
+
filters = image.stream['Filter']
|
|
424
|
+
if isinstance(filters, list):
|
|
425
|
+
# TODO: SUPPORT MULTIPLE FILTER
|
|
426
|
+
if len(filters) > 1:
|
|
427
|
+
utilo.error(f'more than one filter: {filters}')
|
|
428
|
+
# assert len(filter_) == 1, str(filter_)
|
|
429
|
+
filters = filters[0]
|
|
430
|
+
imagefilter = filters.name
|
|
431
|
+
except KeyError:
|
|
432
|
+
imagefilter = 'Default'
|
|
433
|
+
ext = decoder[imagefilter]
|
|
434
|
+
return ext
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
# def extention(image) -> str:
|
|
438
|
+
# stream = image.stream
|
|
439
|
+
# filters = stream.get_filters()
|
|
440
|
+
# (width, height) = image.srcsize
|
|
441
|
+
# if len(filters) == 1 and filters[0][0] in LITERALS_DCT_DECODE:
|
|
442
|
+
# ext = 'jpg'
|
|
443
|
+
# elif (image.bits == 1 or image.bits == 8 and
|
|
444
|
+
# image.colorspace in (LITERAL_DEVICE_RGB, LITERAL_DEVICE_GRAY)):
|
|
445
|
+
# ext = 'bmp'
|
|
446
|
+
# else:
|
|
447
|
+
# ext = 'png'
|
|
448
|
+
# return ext
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# C O P Y R I G H T
|
|
3
|
+
# -----------------------------------------------------------------------------
|
|
4
|
+
# Copyright (c) 2019-2023 by Helmut Konrad Schewe. All rights reserved.
|
|
5
|
+
# This file is property of Helmut Konrad Schewe. Any unauthorized copy,
|
|
6
|
+
# use or distribution is an offensive act against international law and may
|
|
7
|
+
# be prosecuted under federal law. Its content is company confidential.
|
|
8
|
+
# =============================================================================
|
|
9
|
+
"""Save position of element by object hash"""
|
|
10
|
+
|
|
11
|
+
import contextlib
|
|
12
|
+
import statistics
|
|
13
|
+
|
|
14
|
+
import iamraw
|
|
15
|
+
import utilo
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DocumentItemHasher:
|
|
19
|
+
|
|
20
|
+
# TODO: REMOVE THIS SENSELESS CLASS?
|
|
21
|
+
def __init__(self, page: int = -1):
|
|
22
|
+
self.data = {}
|
|
23
|
+
self.page = page
|
|
24
|
+
|
|
25
|
+
def hashitem(self, item: str, position):
|
|
26
|
+
hashid = hash(item)
|
|
27
|
+
# assert that hashid is not saved before, 'collision %s' % item
|
|
28
|
+
# TODO: Investigate later, how to avoid collision
|
|
29
|
+
assert hashid not in self.data, f'collision "{item}"'
|
|
30
|
+
# while hashid in self.data:
|
|
31
|
+
# hashid += 1
|
|
32
|
+
self.data[hashid] = position
|
|
33
|
+
|
|
34
|
+
def position(self, item):
|
|
35
|
+
hashid = hash(item)
|
|
36
|
+
try:
|
|
37
|
+
current = self.data[hashid]
|
|
38
|
+
return current
|
|
39
|
+
except KeyError as error:
|
|
40
|
+
# TODO: CHANGE TO KEY ERROR
|
|
41
|
+
raise ItemNotFound(f'not stored: {item} {hashid}') from error
|
|
42
|
+
|
|
43
|
+
def __eq__(self, value):
|
|
44
|
+
return value and (str(self) == str(value))
|
|
45
|
+
|
|
46
|
+
def __hash__(self):
|
|
47
|
+
return hash(str(self))
|
|
48
|
+
|
|
49
|
+
def __str__(self):
|
|
50
|
+
result = [f'DocumentItemHasher, size: {len(self.data)}']
|
|
51
|
+
for key, value in self.data.items():
|
|
52
|
+
result.append(f'{key} {value}')
|
|
53
|
+
return utilo.NEWLINE.join(result)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def load_hasher(content: str) -> DocumentItemHasher:
|
|
57
|
+
loaded = utilo.yaml_load(content)
|
|
58
|
+
result = []
|
|
59
|
+
for page in loaded:
|
|
60
|
+
pagenumber = int(page['page'])
|
|
61
|
+
hasher = DocumentItemHasher(page=pagenumber)
|
|
62
|
+
for item in page['content']:
|
|
63
|
+
key, position = item.split(maxsplit=1)
|
|
64
|
+
hasher.data[int(key)] = iamraw.BoundingBox.from_str(position)
|
|
65
|
+
result.append(hasher)
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def hash_positions(
|
|
70
|
+
document: iamraw.Document,
|
|
71
|
+
pages=None,
|
|
72
|
+
) -> iamraw.PageContentTextPositions:
|
|
73
|
+
assert isinstance(document, iamraw.Document), type(document)
|
|
74
|
+
collected = []
|
|
75
|
+
with utilo.SkipCollector(pages) as collector:
|
|
76
|
+
for page in document:
|
|
77
|
+
pagenumber = page.page
|
|
78
|
+
if collector.skip(pagenumber):
|
|
79
|
+
continue
|
|
80
|
+
hasher = DocumentItemHasher(pagenumber)
|
|
81
|
+
collected.append(hasher)
|
|
82
|
+
index = 0
|
|
83
|
+
for item in page:
|
|
84
|
+
try:
|
|
85
|
+
# TODO: REMOVE?
|
|
86
|
+
# Not every element has text
|
|
87
|
+
_ = item.text
|
|
88
|
+
except AttributeError:
|
|
89
|
+
continue
|
|
90
|
+
# TODO: COMPUTE FOR OTHER LINES THAN ZERO
|
|
91
|
+
mean = mean_height(item.lines[0])
|
|
92
|
+
hasher.hashitem(
|
|
93
|
+
index,
|
|
94
|
+
iamraw.TextPosition(bounding=item.box, mean=mean),
|
|
95
|
+
)
|
|
96
|
+
index += 1
|
|
97
|
+
result = []
|
|
98
|
+
for page in collected:
|
|
99
|
+
pagenumber = page.page
|
|
100
|
+
content = dict(page.data)
|
|
101
|
+
result.append(
|
|
102
|
+
iamraw.PageContentTextPosition(
|
|
103
|
+
content=content,
|
|
104
|
+
page=pagenumber,
|
|
105
|
+
))
|
|
106
|
+
return result
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def mean_height(chars):
|
|
110
|
+
height = []
|
|
111
|
+
for char in chars:
|
|
112
|
+
with contextlib.suppress(AttributeError):
|
|
113
|
+
height.append(char.box.y1 - char.box.y0)
|
|
114
|
+
if not height:
|
|
115
|
+
return 0.0
|
|
116
|
+
mean = statistics.mean(height)
|
|
117
|
+
return utilo.roundme(mean)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ItemNotFound(ValueError):
|
|
121
|
+
pass
|