ibidem 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.
- ibidem/__init__.py +16 -0
- ibidem/cli.py +78 -0
- ibidem/config.py +15 -0
- ibidem/feature/__init__.py +8 -0
- ibidem/feature/highnote.py +42 -0
- ibidem/feature/legacy.py +14 -0
- ibidem/feature/plain.py +41 -0
- ibidem/feature/result.py +142 -0
- ibidem/layout.py +307 -0
- ibidem/parser/__init__.py +8 -0
- ibidem/parser/highnote.py +88 -0
- ibidem/parser/plain.py +113 -0
- ibidem/parser/textraw.py +111 -0
- ibidem/strategy/__init__.py +155 -0
- ibidem/strategy/highnote.py +29 -0
- ibidem/strategy/moving/__init__.py +8 -0
- ibidem/strategy/moving/finish.py +96 -0
- ibidem/strategy/moving/judge.py +127 -0
- ibidem/strategy/moving/run.py +175 -0
- ibidem/strategy/moving/separator.py +106 -0
- ibidem/strategy/moving/utils.py +38 -0
- ibidem/strategy/plainmoving.py +106 -0
- ibidem/utils.py +129 -0
- ibidem-1.0.0.dist-info/METADATA +29 -0
- ibidem-1.0.0.dist-info/RECORD +28 -0
- ibidem-1.0.0.dist-info/WHEEL +5 -0
- ibidem-1.0.0.dist-info/entry_points.txt +2 -0
- ibidem-1.0.0.dist-info/top_level.txt +1 -0
ibidem/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#==============================================================================
|
|
2
|
+
# C O P Y R I G H T
|
|
3
|
+
#------------------------------------------------------------------------------
|
|
4
|
+
# Copyright (c) 2022 by Helmut Konrad Fahrendholz. All rights reserved.
|
|
5
|
+
# This file is property of Helmut Konrad Fahrendholz. 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
|
+
|
|
10
|
+
import importlib.metadata
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
__version__ = importlib.metadata.version('ibidem')
|
|
14
|
+
|
|
15
|
+
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
|
16
|
+
PROCESS = 'footnote'
|
ibidem/cli.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#==============================================================================
|
|
2
|
+
# C O P Y R I G H T
|
|
3
|
+
#------------------------------------------------------------------------------
|
|
4
|
+
# Copyright (c) 2019-2022 by Helmut Konrad Fahrendholz. All rights reserved.
|
|
5
|
+
# This file is property of Helmut Konrad Fahrendholz. 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
|
+
|
|
10
|
+
import utilo
|
|
11
|
+
|
|
12
|
+
import ibidem
|
|
13
|
+
|
|
14
|
+
DESCRIPTION = 'TODO'
|
|
15
|
+
|
|
16
|
+
WORKPLAN = [
|
|
17
|
+
utilo.create_step(
|
|
18
|
+
'plain',
|
|
19
|
+
inputs=[
|
|
20
|
+
utilo.ResultFile(producer='rawmaker', name='text_text'),
|
|
21
|
+
utilo.ResultFile(producer='rawmaker', name='text_positions'),
|
|
22
|
+
utilo.ResultFile('rawmaker', name='horizontals_horizontals'),
|
|
23
|
+
],
|
|
24
|
+
output=('plain',),
|
|
25
|
+
),
|
|
26
|
+
utilo.create_step(
|
|
27
|
+
'highnote',
|
|
28
|
+
inputs=[
|
|
29
|
+
utilo.ResultFile(producer='rawmaker', name='text_text'),
|
|
30
|
+
utilo.ResultFile(producer='rawmaker', name='text_positions'),
|
|
31
|
+
utilo.ResultFile('rawmaker', name='horizontals_horizontals'),
|
|
32
|
+
],
|
|
33
|
+
output=('highnote',),
|
|
34
|
+
),
|
|
35
|
+
utilo.create_step(
|
|
36
|
+
'result',
|
|
37
|
+
inputs=[
|
|
38
|
+
utilo.ResultFile(producer='footnote', name='highnote_highnote'),
|
|
39
|
+
utilo.ResultFile(producer='footnote', name='plain_plain'),
|
|
40
|
+
],
|
|
41
|
+
output=('result',),
|
|
42
|
+
),
|
|
43
|
+
utilo.create_step(
|
|
44
|
+
'legacy',
|
|
45
|
+
inputs=[
|
|
46
|
+
utilo.ResultFile(producer='footnote', name='result_result'),
|
|
47
|
+
],
|
|
48
|
+
output=('legacy',),
|
|
49
|
+
),
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def rename(path):
|
|
54
|
+
if not isinstance(path, str):
|
|
55
|
+
path = [rename(item) for item in path]
|
|
56
|
+
return path
|
|
57
|
+
path = utilo.rreplace(
|
|
58
|
+
path,
|
|
59
|
+
pattern='footnote__legacy_legacy',
|
|
60
|
+
replace='groupme__footer_footerheader',
|
|
61
|
+
)
|
|
62
|
+
return path
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main():
|
|
66
|
+
utilo.featurepack(
|
|
67
|
+
workplan=WORKPLAN,
|
|
68
|
+
root=ibidem.ROOT,
|
|
69
|
+
featurepackage='ibidem.feature',
|
|
70
|
+
config=utilo.FeaturePackConfig(
|
|
71
|
+
description=DESCRIPTION,
|
|
72
|
+
multiprocessed=True,
|
|
73
|
+
name=ibidem.PROCESS,
|
|
74
|
+
pages=True,
|
|
75
|
+
rename=rename,
|
|
76
|
+
version=ibidem.__version__,
|
|
77
|
+
),
|
|
78
|
+
)
|
ibidem/config.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# C O P Y R I G H T
|
|
3
|
+
# -----------------------------------------------------------------------------
|
|
4
|
+
# Copyright (c) 2022 by Helmut Konrad Fahrendholz. All rights reserved.
|
|
5
|
+
# This file is property of Helmut Konrad Fahrendholz. 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
|
+
|
|
10
|
+
import configos
|
|
11
|
+
import texmex
|
|
12
|
+
|
|
13
|
+
FOOTER_SEPARATOR_WIDTH_MIN = configos.HV_INT_PLUS(default=70)
|
|
14
|
+
|
|
15
|
+
VISIBLE = texmex.TextState.VISIBLE | texmex.TextState.FOOTNOTE
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# C O P Y R I G H T
|
|
3
|
+
# -----------------------------------------------------------------------------
|
|
4
|
+
# Copyright (c) 2022 by Helmut Konrad Fahrendholz. All rights reserved.
|
|
5
|
+
# This file is property of Helmut Konrad Fahrendholz. 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
|
+
# =============================================================================
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# C O P Y R I G H T
|
|
3
|
+
# -----------------------------------------------------------------------------
|
|
4
|
+
# Copyright (c) 2022 by Helmut Konrad Fahrendholz. All rights reserved.
|
|
5
|
+
# This file is property of Helmut Konrad Fahrendholz. 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
|
+
|
|
10
|
+
import serializeraw
|
|
11
|
+
|
|
12
|
+
import ibidem.config
|
|
13
|
+
import ibidem.strategy.highnote
|
|
14
|
+
import ibidem.utils
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def work(
|
|
18
|
+
text: str,
|
|
19
|
+
textpositions: str,
|
|
20
|
+
horizontals: str,
|
|
21
|
+
pages=None,
|
|
22
|
+
) -> str:
|
|
23
|
+
# load
|
|
24
|
+
horizontals = serializeraw.load_horizontals(
|
|
25
|
+
horizontals,
|
|
26
|
+
pages=pages,
|
|
27
|
+
width_min=ibidem.config.FOOTER_SEPARATOR_WIDTH_MIN,
|
|
28
|
+
)
|
|
29
|
+
ptns = serializeraw.ptn_fromfile(
|
|
30
|
+
text,
|
|
31
|
+
textpositions,
|
|
32
|
+
pages=pages,
|
|
33
|
+
state=ibidem.config.VISIBLE,
|
|
34
|
+
)
|
|
35
|
+
ptns = ibidem.utils.rotate_ifrequired(ptns)
|
|
36
|
+
strategy = ibidem.strategy.highnote.HighnoteStrategy(
|
|
37
|
+
horizontals=horizontals,
|
|
38
|
+
ptns=ptns,
|
|
39
|
+
)
|
|
40
|
+
result = strategy.result()
|
|
41
|
+
dumped = serializeraw.dump_headerfooter(result)
|
|
42
|
+
return dumped
|
ibidem/feature/legacy.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# C O P Y R I G H T
|
|
3
|
+
# -----------------------------------------------------------------------------
|
|
4
|
+
# Copyright (c) 2022 by Helmut Konrad Fahrendholz. All rights reserved.
|
|
5
|
+
# This file is property of Helmut Konrad Fahrendholz. 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
|
+
|
|
10
|
+
import utilo
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def work(xresult) -> str:
|
|
14
|
+
return utilo.file_read(xresult)
|
ibidem/feature/plain.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# C O P Y R I G H T
|
|
3
|
+
# -----------------------------------------------------------------------------
|
|
4
|
+
# Copyright (c) 2022 by Helmut Konrad Fahrendholz. All rights reserved.
|
|
5
|
+
# This file is property of Helmut Konrad Fahrendholz. 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
|
+
|
|
10
|
+
import serializeraw
|
|
11
|
+
|
|
12
|
+
import ibidem.config
|
|
13
|
+
import ibidem.strategy.plainmoving
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def work(
|
|
17
|
+
text: str,
|
|
18
|
+
textpositions: str,
|
|
19
|
+
horizontals: str,
|
|
20
|
+
pages=None,
|
|
21
|
+
) -> str:
|
|
22
|
+
# load
|
|
23
|
+
horizontals = serializeraw.load_horizontals(
|
|
24
|
+
horizontals,
|
|
25
|
+
pages=pages,
|
|
26
|
+
width_min=ibidem.config.FOOTER_SEPARATOR_WIDTH_MIN,
|
|
27
|
+
)
|
|
28
|
+
ptns = serializeraw.ptn_fromfile(
|
|
29
|
+
text,
|
|
30
|
+
textpositions,
|
|
31
|
+
pages=pages,
|
|
32
|
+
state=ibidem.config.VISIBLE,
|
|
33
|
+
)
|
|
34
|
+
ptns = ibidem.utils.rotate_ifrequired(ptns)
|
|
35
|
+
strategy = ibidem.strategy.plainmoving.PlainMovingStrategy(
|
|
36
|
+
horizontals=horizontals,
|
|
37
|
+
ptns=ptns,
|
|
38
|
+
)
|
|
39
|
+
result = strategy.result()
|
|
40
|
+
dumped = serializeraw.dump_headerfooter(result)
|
|
41
|
+
return dumped
|
ibidem/feature/result.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# C O P Y R I G H T
|
|
3
|
+
# -----------------------------------------------------------------------------
|
|
4
|
+
# Copyright (c) 2019-2022 by Helmut Konrad Fahrendholz. All rights reserved.
|
|
5
|
+
# This file is property of Helmut Konrad Fahrendholz. 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
|
+
"""Footer Extraction Step
|
|
10
|
+
=============================
|
|
11
|
+
|
|
12
|
+
TODO:
|
|
13
|
+
what should we do with empty header/footer
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import collections
|
|
17
|
+
|
|
18
|
+
import iamraw
|
|
19
|
+
import serializeraw
|
|
20
|
+
import utilo
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def work(
|
|
24
|
+
xhighnote: str,
|
|
25
|
+
xplain: str,
|
|
26
|
+
pages=None,
|
|
27
|
+
) -> str:
|
|
28
|
+
"""Extract footer and header area out of horizontal lines.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Dumped list with top and bottom border, which separates the
|
|
32
|
+
content from the footer and or header, for every page
|
|
33
|
+
"""
|
|
34
|
+
highnote = serializeraw.load_headerfooter(
|
|
35
|
+
xhighnote,
|
|
36
|
+
pages=pages,
|
|
37
|
+
)
|
|
38
|
+
plain = serializeraw.load_headerfooter(
|
|
39
|
+
xplain,
|
|
40
|
+
pages=pages,
|
|
41
|
+
)
|
|
42
|
+
# select the best one
|
|
43
|
+
result = judge_strategy((
|
|
44
|
+
highnote,
|
|
45
|
+
plain,
|
|
46
|
+
))
|
|
47
|
+
validate(result)
|
|
48
|
+
# dump
|
|
49
|
+
dumped = serializeraw.dump_headerfooter(result)
|
|
50
|
+
return dumped
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def judge_strategy(
|
|
54
|
+
results: list[iamraw.PageContentFooterHeaders],
|
|
55
|
+
) -> iamraw.PageContentFooterHeaders:
|
|
56
|
+
"""Decide which results fits best.
|
|
57
|
+
|
|
58
|
+
Zip result of different strategies. Sometimes there are multiple
|
|
59
|
+
options, therefore we have to use the priorities below.
|
|
60
|
+
|
|
61
|
+
Sources/Concept:
|
|
62
|
+
|
|
63
|
+
- MovingFooter: footer (first prio)
|
|
64
|
+
- Pages: footer (second prio)
|
|
65
|
+
- FixedFooter: header and footer (third prio)
|
|
66
|
+
- Common: header (last prio)
|
|
67
|
+
- PlainMoving:
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
results: lists of `ibidem.FootnoteDetectionStrategy`.result
|
|
71
|
+
Returns:
|
|
72
|
+
list of zipped result
|
|
73
|
+
"""
|
|
74
|
+
assert results is not None, 'require list of strategy results'
|
|
75
|
+
result = []
|
|
76
|
+
for pdfpage, (
|
|
77
|
+
moving,
|
|
78
|
+
plainmoving,
|
|
79
|
+
) in utilo.sync_pages(results):
|
|
80
|
+
footer = moving.footer if moving else None
|
|
81
|
+
footer_best = 'moving' if moving else None
|
|
82
|
+
# strategy: moving
|
|
83
|
+
if moving and moving.footer and moving.footer.notes:
|
|
84
|
+
footer = moving.footer
|
|
85
|
+
footer_best = 'moving'
|
|
86
|
+
# strategy: plain
|
|
87
|
+
if not (moving and moving.footer) and plainmoving and plainmoving.footer: # yapf:disable
|
|
88
|
+
# use plain moving only if no other strategy works
|
|
89
|
+
footer = plainmoving.footer
|
|
90
|
+
footer_best = 'plain'
|
|
91
|
+
# log footer best
|
|
92
|
+
if footer_best:
|
|
93
|
+
utilo.verbose(f'footer: {pdfpage} {footer_best}')
|
|
94
|
+
current = iamraw.PageContentFooterHeader(
|
|
95
|
+
footer=footer,
|
|
96
|
+
page=pdfpage,
|
|
97
|
+
)
|
|
98
|
+
result.append(current)
|
|
99
|
+
|
|
100
|
+
page_order = [item.page for item in result]
|
|
101
|
+
assert utilo.isascending(page_order), page_order
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def quality(results: list) -> tuple:
|
|
106
|
+
"""Determine quality[0.0, 1.0] of every extraction strategy."""
|
|
107
|
+
# count number of page
|
|
108
|
+
pages = set()
|
|
109
|
+
# count result for every strategy
|
|
110
|
+
counter = collections.defaultdict(int)
|
|
111
|
+
for pdfpage, data in utilo.sync_pages(results):
|
|
112
|
+
pages.add(pdfpage)
|
|
113
|
+
for index, item in enumerate(data):
|
|
114
|
+
if not item:
|
|
115
|
+
continue
|
|
116
|
+
counter[index] += 1
|
|
117
|
+
result = tuple(counter[index] / len(pages) if pages else 0
|
|
118
|
+
for index, _ in enumerate(results))
|
|
119
|
+
return result
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def validate(items: list):
|
|
123
|
+
"""Validate list of pageable items.
|
|
124
|
+
|
|
125
|
+
If some `page` attribute is duplicated, raise ValueError.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
items(list): list of objects with <page,content>
|
|
129
|
+
Raises:
|
|
130
|
+
ValueError: if some page attribute is duplicated.
|
|
131
|
+
"""
|
|
132
|
+
# TODO: REMOVE AFTER UPGRADING IAMRAW
|
|
133
|
+
counter = collections.Counter()
|
|
134
|
+
for item in items:
|
|
135
|
+
counter[item.page] += 1
|
|
136
|
+
msg = []
|
|
137
|
+
for page, value in counter.most_common():
|
|
138
|
+
if value <= 1:
|
|
139
|
+
continue
|
|
140
|
+
msg.append(f'duplicated page: {page} ({value})')
|
|
141
|
+
if msg:
|
|
142
|
+
raise ValueError(utilo.NEWLINE.join(msg))
|
ibidem/layout.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# C O P Y R I G H T
|
|
3
|
+
# -----------------------------------------------------------------------------
|
|
4
|
+
# Copyright (c) 2021-2022 by Helmut Konrad Fahrendholz. All rights reserved.
|
|
5
|
+
# This file is property of Helmut Konrad Fahrendholz. 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
|
+
|
|
10
|
+
import dataclasses
|
|
11
|
+
import math
|
|
12
|
+
|
|
13
|
+
import configos
|
|
14
|
+
import iamraw
|
|
15
|
+
import texmex
|
|
16
|
+
import utilo
|
|
17
|
+
|
|
18
|
+
import ibidem.utils
|
|
19
|
+
|
|
20
|
+
HIGHNOTE_VERTICAL_LINE_DIFF_MAX = configos.HV_FLOAT_PLUS(default=15.0)
|
|
21
|
+
|
|
22
|
+
FOOTNOTE_X0_MAX = configos.HolyTable(items=(
|
|
23
|
+
(440, 100), # TODO: US Letter?
|
|
24
|
+
(550, 150), # DINA4
|
|
25
|
+
))
|
|
26
|
+
FOOTNOTE_X1_MAX = configos.HolyTable(items=(
|
|
27
|
+
(440, 100 + 200), # TODO: US Letter?
|
|
28
|
+
(550, 150 + 200), # DINA4
|
|
29
|
+
))
|
|
30
|
+
|
|
31
|
+
FOOTNOTE_RATE_MIN = configos.HV_PERCENT_PLUS(default=50)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def group_footnote_area(content) -> list:
|
|
35
|
+
connected_neighbors = connect_neighbors(content)
|
|
36
|
+
if not connected_neighbors:
|
|
37
|
+
return []
|
|
38
|
+
result = []
|
|
39
|
+
has_highnote = 0
|
|
40
|
+
for group in connected_neighbors:
|
|
41
|
+
splitted = split_textinfo(group)
|
|
42
|
+
if any(high for high, _ in splitted):
|
|
43
|
+
has_highnote += 1
|
|
44
|
+
merged = merge_online(splitted)
|
|
45
|
+
result.extend(merged)
|
|
46
|
+
result = merge_after(result)
|
|
47
|
+
rate = utilo.rate_rel(has_highnote, len(connected_neighbors))
|
|
48
|
+
if rate < FOOTNOTE_RATE_MIN:
|
|
49
|
+
if len(result) == 1:
|
|
50
|
+
return result
|
|
51
|
+
utilo.debug(f'no highnotes: {rate} detected, skip footnote result')
|
|
52
|
+
utilo.verbose(result)
|
|
53
|
+
return []
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def merge_after(items: list) -> list:
|
|
58
|
+
"""Make merger more robust against false formatted footnotes.
|
|
59
|
+
|
|
60
|
+
Merge footnote text which is under highnote on the left side.
|
|
61
|
+
|
|
62
|
+
See:
|
|
63
|
+
- bachelor041a p14
|
|
64
|
+
- master091b p54
|
|
65
|
+
"""
|
|
66
|
+
if not items:
|
|
67
|
+
return []
|
|
68
|
+
result = [items[0]]
|
|
69
|
+
for highnote, content in items[1:]:
|
|
70
|
+
if highnote is not None:
|
|
71
|
+
# normal footnote
|
|
72
|
+
result.append((highnote, content))
|
|
73
|
+
continue
|
|
74
|
+
highnote_before = result[-1][0] is not None
|
|
75
|
+
if highnote_before:
|
|
76
|
+
# merge them
|
|
77
|
+
for item in content.style.content:
|
|
78
|
+
item.start += len(result[-1][1].text)
|
|
79
|
+
item.end += len(result[-1][1].text)
|
|
80
|
+
result[-1][1].text += content.text
|
|
81
|
+
result[-1][1].style.content.extend(content.style.content)
|
|
82
|
+
continue
|
|
83
|
+
# footnote without highnote
|
|
84
|
+
result.append((None, content))
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def connect_neighbors(items) -> list:
|
|
89
|
+
if not items:
|
|
90
|
+
return []
|
|
91
|
+
result = [[items[0]]]
|
|
92
|
+
for item in items[1:]:
|
|
93
|
+
before = result[-1][-1]
|
|
94
|
+
if connected(before.bounding, item.bounding):
|
|
95
|
+
result[-1].append(item)
|
|
96
|
+
else:
|
|
97
|
+
result.append([item])
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def split_textinfo(content) -> list:
|
|
102
|
+
"""Split text by `hightnote` and preserve TextInfo.
|
|
103
|
+
|
|
104
|
+
Go line by line from top to bottom. Collect lines till highnote
|
|
105
|
+
occurs. If highnote occurs merge content and add to result.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
list of a tuple of highnote and content
|
|
109
|
+
"""
|
|
110
|
+
assert isinstance(content, list), type(content)
|
|
111
|
+
result = []
|
|
112
|
+
highnote = None
|
|
113
|
+
collected = []
|
|
114
|
+
for item in content:
|
|
115
|
+
for style in item.style.content:
|
|
116
|
+
if ishighnote(style, item.text):
|
|
117
|
+
if collected:
|
|
118
|
+
result.append((highnote, union(collected)))
|
|
119
|
+
collected = []
|
|
120
|
+
style = style.copy()
|
|
121
|
+
highnote = texmex.TextInfo(
|
|
122
|
+
text=item.text[style.start:style.end],
|
|
123
|
+
style=style,
|
|
124
|
+
bounding=char_bounding(item.bounding, item.text, style),
|
|
125
|
+
)
|
|
126
|
+
style.start = 0
|
|
127
|
+
style.end = len(highnote.text)
|
|
128
|
+
else:
|
|
129
|
+
bounding = iamraw.split_x(
|
|
130
|
+
bounding=item.bounding,
|
|
131
|
+
part=style.start,
|
|
132
|
+
parts=style.end,
|
|
133
|
+
width=len(item.text),
|
|
134
|
+
)
|
|
135
|
+
collected.append(TextChunk(
|
|
136
|
+
item.text,
|
|
137
|
+
style,
|
|
138
|
+
bounding=bounding,
|
|
139
|
+
))
|
|
140
|
+
if highnote or collected:
|
|
141
|
+
# there is always content left at the end
|
|
142
|
+
result.append((highnote, union(collected)))
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def merge_online(items) -> list:
|
|
147
|
+
"""Ensure that high notes are located on a vertical line.
|
|
148
|
+
|
|
149
|
+
Therefore we have to ignore highnotes which are located inside the
|
|
150
|
+
text and not part of the text flow.
|
|
151
|
+
|
|
152
|
+
Steps:
|
|
153
|
+
1. Determine the most left highnotes
|
|
154
|
+
2. Adjust highnotes on most left one
|
|
155
|
+
3. Merge other highnotes into text
|
|
156
|
+
"""
|
|
157
|
+
if not items:
|
|
158
|
+
return []
|
|
159
|
+
result = []
|
|
160
|
+
# skip None-Highnotes => if item
|
|
161
|
+
with_highnote = [high.bounding.x0 for high, _ in items if high]
|
|
162
|
+
mostleft = min(with_highnote) if with_highnote else None
|
|
163
|
+
high, collected = None, []
|
|
164
|
+
for highnote, content in items:
|
|
165
|
+
if not highnote:
|
|
166
|
+
result.append((None, content))
|
|
167
|
+
continue
|
|
168
|
+
diff = math.fabs(highnote.bounding.x0 - mostleft)
|
|
169
|
+
if diff > HIGHNOTE_VERTICAL_LINE_DIFF_MAX:
|
|
170
|
+
# highnote in content
|
|
171
|
+
collected.append(
|
|
172
|
+
shrink_tostyle(
|
|
173
|
+
highnote.text,
|
|
174
|
+
highnote.style,
|
|
175
|
+
bounding=highnote.bounding,
|
|
176
|
+
))
|
|
177
|
+
collected.extend([
|
|
178
|
+
shrink_tostyle(content.text, style, bounding=content.bounding)
|
|
179
|
+
for style in content.style
|
|
180
|
+
])
|
|
181
|
+
else:
|
|
182
|
+
# new highnotes
|
|
183
|
+
if high:
|
|
184
|
+
result.append((high, union(collected)))
|
|
185
|
+
high = highnote
|
|
186
|
+
collected = [
|
|
187
|
+
shrink_tostyle(content.text, style, bounding=content.bounding)
|
|
188
|
+
for style in content.style
|
|
189
|
+
]
|
|
190
|
+
notempty = collected or high
|
|
191
|
+
if notempty:
|
|
192
|
+
# do not add empty items
|
|
193
|
+
result.append((high, union(collected)))
|
|
194
|
+
return result
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# increase word diff at the start of the line to merge huge gap between
|
|
198
|
+
# footnote number and footnote text.
|
|
199
|
+
LEFTRIGHT_DIFF_MAX = configos.HolyTable(items=(
|
|
200
|
+
(0, 40),
|
|
201
|
+
(100, 40),
|
|
202
|
+
(120, 40),
|
|
203
|
+
(150, 30),
|
|
204
|
+
(200, 20),
|
|
205
|
+
(300, 20),
|
|
206
|
+
(400, 20),
|
|
207
|
+
(500, 20),
|
|
208
|
+
))
|
|
209
|
+
|
|
210
|
+
SAMEORIGIN_DIFF_MAX = configos.HV_FLOAT_PLUS(default=35.0)
|
|
211
|
+
|
|
212
|
+
SAMELINE_DIFF_MAX = configos.HV_FLOAT_PLUS(default=5.0)
|
|
213
|
+
|
|
214
|
+
UNDERFIRST_DIFF_MAX = configos.HV_FLOAT_PLUS(default=10.0)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def connected(first: tuple, second: tuple) -> bool:
|
|
218
|
+
leftright_diff_max = LEFTRIGHT_DIFF_MAX(first.x0)
|
|
219
|
+
# words are neighbors
|
|
220
|
+
leftright = utilo.near(
|
|
221
|
+
first.x1,
|
|
222
|
+
second.x0,
|
|
223
|
+
diff=leftright_diff_max,
|
|
224
|
+
)
|
|
225
|
+
# plus indention
|
|
226
|
+
sameorigin = utilo.near(
|
|
227
|
+
first.x0,
|
|
228
|
+
second.x0,
|
|
229
|
+
diff=SAMEORIGIN_DIFF_MAX,
|
|
230
|
+
)
|
|
231
|
+
sameline = utilo.near(
|
|
232
|
+
first.y0,
|
|
233
|
+
second.y0,
|
|
234
|
+
diff=SAMELINE_DIFF_MAX,
|
|
235
|
+
)
|
|
236
|
+
underfirst = utilo.near(
|
|
237
|
+
first.y1,
|
|
238
|
+
second.y0,
|
|
239
|
+
diff=UNDERFIRST_DIFF_MAX,
|
|
240
|
+
)
|
|
241
|
+
result = (leftright or sameorigin) and (sameline or underfirst)
|
|
242
|
+
return result
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@dataclasses.dataclass
|
|
246
|
+
class TextChunk:
|
|
247
|
+
text: str = None
|
|
248
|
+
style: texmex.TextStyle = None
|
|
249
|
+
bounding: iamraw.BoundingBox = None
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
TextChunks = list[TextChunk]
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def shrink_tostyle(text: str, style, bounding=None) -> TextChunk:
|
|
256
|
+
text = text[style.start:style.end]
|
|
257
|
+
style = style.copy()
|
|
258
|
+
style.start, style.end = 0, len(text)
|
|
259
|
+
# TODO: ADD BOUNDING ADJUSTMENT, IN THE CURRENT CASE, WE DO NOT SHRINK
|
|
260
|
+
# THE BOUNDING
|
|
261
|
+
return TextChunk(text, style, bounding)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def union(chunks: TextChunks) -> texmex.TextInfo:
|
|
265
|
+
raw = ''
|
|
266
|
+
content = []
|
|
267
|
+
for chunk in chunks: # pylint:disable=W0612
|
|
268
|
+
start = len(raw)
|
|
269
|
+
raw += chunk.text[chunk.style.start:chunk.style.end]
|
|
270
|
+
end = len(raw)
|
|
271
|
+
section_style = chunk.style.copy()
|
|
272
|
+
section_style.start, section_style.end = start, end
|
|
273
|
+
content.append(section_style)
|
|
274
|
+
bounding = [item.bounding for item in chunks if item.bounding]
|
|
275
|
+
bounding = utilo.rect_max(bounding) if bounding else None
|
|
276
|
+
result = texmex.TextInfo(
|
|
277
|
+
text=raw,
|
|
278
|
+
style=texmex.TextStyle(content=content),
|
|
279
|
+
bounding=bounding,
|
|
280
|
+
)
|
|
281
|
+
return result
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def char_bounding(
|
|
285
|
+
bounding: iamraw.BoundingBox,
|
|
286
|
+
text: str,
|
|
287
|
+
style: texmex.TextStyle,
|
|
288
|
+
) -> iamraw.BoundingBox:
|
|
289
|
+
width = bounding.x1 - bounding.x0
|
|
290
|
+
char_width = width / len(text)
|
|
291
|
+
x0 = bounding.x0 + char_width * style.start
|
|
292
|
+
x1 = bounding.x0 + char_width * style.end
|
|
293
|
+
result = iamraw.BoundingBox(x0, bounding.y0, x1, bounding.y1)
|
|
294
|
+
return result
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
HIGHNOTE_RISE_MIN = configos.HV_FLOAT_PLUS(default=3.0)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def ishighnote(style, text: str) -> bool:
|
|
301
|
+
highnote_occurs = style.rise >= HIGHNOTE_RISE_MIN
|
|
302
|
+
if not highnote_occurs:
|
|
303
|
+
return False
|
|
304
|
+
text = text[style.start:style.end].strip()
|
|
305
|
+
if ibidem.utils.NUMBER.match(text):
|
|
306
|
+
return True
|
|
307
|
+
return False
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# C O P Y R I G H T
|
|
3
|
+
# -----------------------------------------------------------------------------
|
|
4
|
+
# Copyright (c) 2022 by Helmut Konrad Fahrendholz. All rights reserved.
|
|
5
|
+
# This file is property of Helmut Konrad Fahrendholz. 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
|
+
# =============================================================================
|