vlmparse 0.1.0__py3-none-any.whl → 0.1.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.
- vlmparse/benchpdf2md/bench_tests/benchmark_tsts.py +1763 -0
- vlmparse/benchpdf2md/bench_tests/utils.py +0 -0
- vlmparse/benchpdf2md/create_dataset.py +60 -0
- vlmparse/benchpdf2md/olmocrbench/katex/__init__.py +1 -0
- vlmparse/benchpdf2md/olmocrbench/katex/render.py +592 -0
- vlmparse/benchpdf2md/olmocrbench/repeatdetect.py +175 -0
- vlmparse/benchpdf2md/olmocrbench/run_olmocr_bench.py +256 -0
- vlmparse/benchpdf2md/olmocrbench/tests.py +1334 -0
- vlmparse/benchpdf2md/run_benchmark.py +296 -0
- vlmparse/benchpdf2md/st_visu_benchmark/app.py +271 -0
- vlmparse/benchpdf2md/st_visu_benchmark/highligh_text.py +117 -0
- vlmparse/benchpdf2md/st_visu_benchmark/test_form.py +95 -0
- vlmparse/benchpdf2md/st_visu_benchmark/ui_elements.py +20 -0
- vlmparse/benchpdf2md/st_visu_benchmark/utils.py +50 -0
- vlmparse/benchpdf2md/utils.py +56 -0
- vlmparse/clients/chandra.py +323 -0
- vlmparse/clients/deepseekocr.py +52 -0
- vlmparse/clients/docling.py +146 -0
- vlmparse/clients/dotsocr.py +277 -0
- vlmparse/clients/granite_docling.py +132 -0
- vlmparse/clients/hunyuanocr.py +45 -0
- vlmparse/clients/lightonocr.py +43 -0
- vlmparse/clients/mineru.py +119 -0
- vlmparse/clients/nanonetocr.py +29 -0
- vlmparse/clients/olmocr.py +46 -0
- vlmparse/clients/openai_converter.py +173 -0
- vlmparse/clients/paddleocrvl.py +48 -0
- vlmparse/clients/pipe_utils/cleaner.py +74 -0
- vlmparse/clients/pipe_utils/html_to_md_conversion.py +136 -0
- vlmparse/clients/pipe_utils/utils.py +12 -0
- vlmparse/clients/prompts.py +66 -0
- vlmparse/data_model/box.py +551 -0
- vlmparse/data_model/document.py +148 -0
- vlmparse/servers/docker_server.py +199 -0
- vlmparse/servers/utils.py +250 -0
- vlmparse/st_viewer/fs_nav.py +53 -0
- vlmparse/st_viewer/st_viewer.py +80 -0
- {vlmparse-0.1.0.dist-info → vlmparse-0.1.3.dist-info}/METADATA +12 -1
- vlmparse-0.1.3.dist-info/RECORD +50 -0
- vlmparse-0.1.0.dist-info/RECORD +0 -13
- {vlmparse-0.1.0.dist-info → vlmparse-0.1.3.dist-info}/WHEEL +0 -0
- {vlmparse-0.1.0.dist-info → vlmparse-0.1.3.dist-info}/entry_points.txt +0 -0
- {vlmparse-0.1.0.dist-info → vlmparse-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {vlmparse-0.1.0.dist-info → vlmparse-0.1.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
PDF2MD_PROMPT = r"""Please follow these instructions for the conversion:
|
|
2
|
+
1. Text Processing:
|
|
3
|
+
- Accurately recognize all text content in the PDF image without guessing or inferring. Strictly adhere to the text of the PDF image without reformulating.
|
|
4
|
+
- Convert the recognized text into Markdown format.
|
|
5
|
+
- Maintain the original document structure, including headings, paragraphs, lists, etc.
|
|
6
|
+
- Convert Tables of Content (TOC) as numbered lists.
|
|
7
|
+
- For footnotes references with uppercase letters, use the following syntax: [^1]
|
|
8
|
+
|
|
9
|
+
2. Mathematical Formula Processing:
|
|
10
|
+
- Convert all mathematical formulas to LaTeX format.
|
|
11
|
+
- Enclose inline formulas with $. For example: This is an inline formula $ E = mc^2 $
|
|
12
|
+
- Enclose block formulas with $$ $$. For example: $$ \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} $$
|
|
13
|
+
|
|
14
|
+
3. Table Processing:
|
|
15
|
+
- Tables should be formatted as HTML with <table> tags.
|
|
16
|
+
|
|
17
|
+
4. Figure Handling:
|
|
18
|
+
- For any images encountered, insert an HTML <img> tag:
|
|
19
|
+
- Use "[image-placeholder]" as the source.
|
|
20
|
+
- Include an alt attribute with the executive summary of the image content.
|
|
21
|
+
- Be very descriptive only if the image is a graph or chart (translate the image as a table, a mermaid diagram or any other format suitable).
|
|
22
|
+
|
|
23
|
+
5. Lists:
|
|
24
|
+
- Use * for unordered lists.
|
|
25
|
+
|
|
26
|
+
6. Output Format:
|
|
27
|
+
- Ensure the output Markdown document has a clear structure with appropriate line breaks between elements.
|
|
28
|
+
- For complex layouts, try to maintain the original document's structure and format as closely as possible.
|
|
29
|
+
- Ignore headers or footers.
|
|
30
|
+
- Do not surround your output with triple backticks.
|
|
31
|
+
- If there is nothing on the image, just return ONLY the tag <blank> without anything else.
|
|
32
|
+
- Convert checked and unchecked boxes to [x] and [ ] respectively.
|
|
33
|
+
- For form fields with individual character boxes, transcribe the text as a continuous string instead of splitting characters into separate table cells or using '|' as a separator.
|
|
34
|
+
|
|
35
|
+
Please strictly follow these guidelines to ensure accuracy and consistency in the conversion. Your task is to accurately convert the content of the PDF image into Markdown format without adding any extra explanations or comments."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
PDF2HTML_PROMPT = r"""You are an AI assistant specialized in converting PDF images to HTML format. Please follow these instructions for the conversion:
|
|
39
|
+
|
|
40
|
+
1. Text Processing:
|
|
41
|
+
- Accurately recognize all text content in the PDF image without guessing or inferring. Strictly adhere to the text of the PDF image without reformulating.
|
|
42
|
+
- Convert the recognized text into HTML format.
|
|
43
|
+
- For footnotes references with uppercase letters, use the <sup> tag. For example: <sup>1</sup>
|
|
44
|
+
- Do not add a headers or footers .
|
|
45
|
+
- Return only the part of the html page between <body> and </body> tags, the html string returned should therefore be: "<body>html content</body>".
|
|
46
|
+
- Pay attention to the titles, sections and sub-sections, they should be with <h1>, <h2>, <h3> etc...
|
|
47
|
+
|
|
48
|
+
2. Figure Handling:
|
|
49
|
+
- For any images encountered, insert an HTML <img> tag:
|
|
50
|
+
- Use "[image-placeholder]" as the source.
|
|
51
|
+
- Include an alt attribute with the executive summary of the image content.
|
|
52
|
+
- Be very descriptive only if the image is a graph or chart (translate the image as a table, a mermaid diagram or any other format suitable).
|
|
53
|
+
|
|
54
|
+
3. Lists:
|
|
55
|
+
- Use `<ul>` and `<li>` for unordered lists, and `<ol>` and `<li>` for ordered lists.
|
|
56
|
+
|
|
57
|
+
5. Output Format:
|
|
58
|
+
- Ensure the output HTML document has a simple linear structure without complex hierarchy such as <div> tags.
|
|
59
|
+
- Do not translate formatting such as color, bold, italic, etc.
|
|
60
|
+
- Do not surround your output with triple backticks.
|
|
61
|
+
- If there is nothing on the image, just return ONLY the tag <blank> without anything else.
|
|
62
|
+
- Ignore headers or footers, do not add them to the transcription.
|
|
63
|
+
- Convert checked and unchecked boxes to [x] and [ ] respectively.
|
|
64
|
+
- For form fields with individual character boxes, transcribe the text as a continuous string instead of splitting characters into separate table cells or using '|' as a separator.
|
|
65
|
+
|
|
66
|
+
Please strictly follow these guidelines to ensure accuracy and consistency in the conversion. Your task is to accurately convert the content of the PDF image into HTML format without adding any extra explanations or comments."""
|
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
# from docling-core
|
|
2
|
+
"""Models for the base data types."""
|
|
3
|
+
|
|
4
|
+
import copy
|
|
5
|
+
import random
|
|
6
|
+
from typing import Literal, Tuple
|
|
7
|
+
|
|
8
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
10
|
+
from pydantic_core import PydanticCustomError
|
|
11
|
+
|
|
12
|
+
FONT_SIZE = 20
|
|
13
|
+
FONT_CLASS_SIZE = 20
|
|
14
|
+
WIDTH_BOXES = 5
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Size(BaseModel):
|
|
18
|
+
"""Size."""
|
|
19
|
+
|
|
20
|
+
width: float = 0.0
|
|
21
|
+
height: float = 0.0
|
|
22
|
+
|
|
23
|
+
def as_tuple(self):
|
|
24
|
+
return (self.width, self.height)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def clip(val, minimum, maximum):
|
|
28
|
+
return max(min(val, maximum), minimum)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
CoordOrigin = Literal["TOPLEFT", "BOTTOMLEFT"]
|
|
32
|
+
|
|
33
|
+
AngleDirection = Literal["COUNTERCLOCKWISE"] ### "CLOCKWISE" is not supported !
|
|
34
|
+
|
|
35
|
+
AngleType = Literal["DEGREES"] ### "RADIANS" is not supported !
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class InvalidBoundingBoxCoordinates(PydanticCustomError):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BoundingBox(BaseModel):
|
|
43
|
+
model_config = ConfigDict(
|
|
44
|
+
arbitrary_types_allowed=True,
|
|
45
|
+
validate_assignment=True,
|
|
46
|
+
strict=True,
|
|
47
|
+
)
|
|
48
|
+
"""BoundingBox."""
|
|
49
|
+
|
|
50
|
+
l: float = Field(ge=0) # left
|
|
51
|
+
t: float = Field(ge=0) # top
|
|
52
|
+
r: float = Field(ge=0) # right
|
|
53
|
+
b: float = Field(ge=0) # bottom
|
|
54
|
+
|
|
55
|
+
coord_origin: CoordOrigin = "TOPLEFT"
|
|
56
|
+
relative: bool = False
|
|
57
|
+
reference: Size | None = None
|
|
58
|
+
|
|
59
|
+
@model_validator(mode="before")
|
|
60
|
+
@classmethod
|
|
61
|
+
def validate_bbox(cls, field_values):
|
|
62
|
+
if "coord_origin" not in field_values:
|
|
63
|
+
field_values["coord_origin"] = "TOPLEFT"
|
|
64
|
+
cond = True
|
|
65
|
+
if field_values["coord_origin"] == "TOPLEFT":
|
|
66
|
+
cond &= field_values["l"] < field_values["r"]
|
|
67
|
+
cond &= field_values["t"] < field_values["b"]
|
|
68
|
+
elif field_values["coord_origin"] == "BOTTOMLEFT":
|
|
69
|
+
cond &= field_values["l"] < field_values["r"]
|
|
70
|
+
cond &= field_values["t"] > field_values["b"]
|
|
71
|
+
if not cond:
|
|
72
|
+
raise InvalidBoundingBoxCoordinates(
|
|
73
|
+
"InvalidBoundingBoxCoordinates",
|
|
74
|
+
f"Invalid BoundingBox coordinates: left={field_values['l']}, top={field_values['t']}, right={field_values['r']}, bottom={field_values['b']}",
|
|
75
|
+
)
|
|
76
|
+
return field_values
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def width(self):
|
|
80
|
+
"""width."""
|
|
81
|
+
return self.r - self.l
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def height(self):
|
|
85
|
+
"""height."""
|
|
86
|
+
return abs(self.t - self.b)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def center(self):
|
|
90
|
+
"""center"""
|
|
91
|
+
return (float(self.r - self.l) / 2.0, float(self.b - self.t) / 2.0)
|
|
92
|
+
|
|
93
|
+
def scaled(self, scale: float) -> "BoundingBox":
|
|
94
|
+
"""scaled.
|
|
95
|
+
|
|
96
|
+
:param scale: float:
|
|
97
|
+
|
|
98
|
+
"""
|
|
99
|
+
out_bbox = self.model_dump()
|
|
100
|
+
out_bbox["l"] *= scale # noqa
|
|
101
|
+
out_bbox["r"] *= scale
|
|
102
|
+
out_bbox["t"] *= scale
|
|
103
|
+
out_bbox["b"] *= scale
|
|
104
|
+
|
|
105
|
+
return BoundingBox.model_validate(out_bbox)
|
|
106
|
+
|
|
107
|
+
def normalized(self, page_size: Size) -> "BoundingBox":
|
|
108
|
+
"""normalized.
|
|
109
|
+
|
|
110
|
+
:param page_size: Size:
|
|
111
|
+
|
|
112
|
+
"""
|
|
113
|
+
out_bbox = self.model_dump()
|
|
114
|
+
out_bbox["l"] /= page_size.width # noqa
|
|
115
|
+
out_bbox["r"] /= page_size.width
|
|
116
|
+
out_bbox["t"] /= page_size.height
|
|
117
|
+
out_bbox["b"] /= page_size.height
|
|
118
|
+
out_bbox["relative"] = True
|
|
119
|
+
out_bbox["reference"] = page_size
|
|
120
|
+
return BoundingBox.model_validate(out_bbox)
|
|
121
|
+
|
|
122
|
+
def unormalized(self, page_size: Size) -> "BoundingBox":
|
|
123
|
+
"""normalized.
|
|
124
|
+
|
|
125
|
+
:param page_size: Size:
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
out_bbox = self.model_dump()
|
|
129
|
+
out_bbox["l"] *= page_size.width # noqa
|
|
130
|
+
out_bbox["r"] *= page_size.width
|
|
131
|
+
out_bbox["t"] *= page_size.height
|
|
132
|
+
out_bbox["b"] *= page_size.height
|
|
133
|
+
out_bbox["relative"] = False
|
|
134
|
+
out_bbox["reference"] = None
|
|
135
|
+
return BoundingBox.model_validate(out_bbox)
|
|
136
|
+
|
|
137
|
+
def to_relative(self, page_size: Size, inplace: bool = False) -> "BoundingBox":
|
|
138
|
+
"""to_relative. INPLACE
|
|
139
|
+
|
|
140
|
+
:param page_size: Size:
|
|
141
|
+
|
|
142
|
+
"""
|
|
143
|
+
if inplace:
|
|
144
|
+
if self.relative:
|
|
145
|
+
return self
|
|
146
|
+
assert self.reference is None
|
|
147
|
+
self = self.normalized(page_size)
|
|
148
|
+
return self
|
|
149
|
+
else:
|
|
150
|
+
out_bbox = copy.deepcopy(self)
|
|
151
|
+
if out_bbox.relative:
|
|
152
|
+
return out_bbox
|
|
153
|
+
assert out_bbox.reference is None
|
|
154
|
+
out_bbox = out_bbox.normalized(page_size)
|
|
155
|
+
return out_bbox
|
|
156
|
+
|
|
157
|
+
def to_absolute(self, page_size: Size, inplace: bool = False) -> "BoundingBox":
|
|
158
|
+
"""to_absolute. INPLACE
|
|
159
|
+
|
|
160
|
+
:param page_size: Size:
|
|
161
|
+
|
|
162
|
+
"""
|
|
163
|
+
if inplace:
|
|
164
|
+
if not self.relative:
|
|
165
|
+
return self
|
|
166
|
+
assert self.reference is not None
|
|
167
|
+
self = self.unormalized(page_size)
|
|
168
|
+
return self
|
|
169
|
+
else:
|
|
170
|
+
if not self.relative:
|
|
171
|
+
return BoundingBox.model_validate(self.model_dump())
|
|
172
|
+
out_bbox = self.unormalized(page_size)
|
|
173
|
+
return out_bbox
|
|
174
|
+
|
|
175
|
+
def clip(self, page_size: Size) -> "BoundingBox":
|
|
176
|
+
out_bbox = copy.deepcopy(self)
|
|
177
|
+
out_bbox.l = clip(out_bbox.l, 0, page_size.width)
|
|
178
|
+
out_bbox.r = clip(out_bbox.r, 0, page_size.width)
|
|
179
|
+
out_bbox.t = clip(out_bbox.t, 0, page_size.height)
|
|
180
|
+
out_bbox.b = clip(out_bbox.b, 0, page_size.height)
|
|
181
|
+
return out_bbox
|
|
182
|
+
|
|
183
|
+
def get_relative_box(self, reference_box: "BoundingBox") -> "BoundingBox":
|
|
184
|
+
l = max(0, self.l - reference_box.l)
|
|
185
|
+
r = max(0, self.r - reference_box.l)
|
|
186
|
+
|
|
187
|
+
if self.coord_origin == "TOPLEFT":
|
|
188
|
+
t = max(0, self.t - reference_box.t)
|
|
189
|
+
b = max(0, self.b - reference_box.t)
|
|
190
|
+
else: # BOTTOMLEFT
|
|
191
|
+
t = max(0, self.t - reference_box.b)
|
|
192
|
+
b = max(0, self.b - reference_box.b)
|
|
193
|
+
|
|
194
|
+
return type(self)(l=l, t=t, r=r, b=b, coord_origin=self.coord_origin)
|
|
195
|
+
|
|
196
|
+
def as_tuple(self, output_format: Literal["TOPLEFT", "BOTTOMLEFT"] = "TOPLEFT"):
|
|
197
|
+
"""as_tuple."""
|
|
198
|
+
if output_format == "TOPLEFT":
|
|
199
|
+
return (self.l, self.t, self.r, self.b)
|
|
200
|
+
elif output_format == "BOTTOMLEFT":
|
|
201
|
+
return (self.l, self.b, self.r, self.t)
|
|
202
|
+
|
|
203
|
+
def as_tuple_xywh(self):
|
|
204
|
+
return (self.l, self.t, self.width, self.height)
|
|
205
|
+
|
|
206
|
+
@classmethod
|
|
207
|
+
def from_tuple(cls, coord: Tuple[float, ...], origin: CoordOrigin):
|
|
208
|
+
"""from_tuple.
|
|
209
|
+
|
|
210
|
+
:param coord: Tuple[float:
|
|
211
|
+
:param ...]:
|
|
212
|
+
:param origin: CoordOrigin:
|
|
213
|
+
|
|
214
|
+
"""
|
|
215
|
+
if origin == "TOPLEFT":
|
|
216
|
+
l, t, r, b = coord[0], coord[1], coord[2], coord[3]
|
|
217
|
+
if r < l:
|
|
218
|
+
l, r = r, l
|
|
219
|
+
if b < t:
|
|
220
|
+
b, t = t, b
|
|
221
|
+
|
|
222
|
+
return BoundingBox(l=l, t=t, r=r, b=b, coord_origin=origin)
|
|
223
|
+
elif origin == "BOTTOMLEFT":
|
|
224
|
+
l, b, r, t = coord[0], coord[1], coord[2], coord[3]
|
|
225
|
+
if r < l:
|
|
226
|
+
l, r = r, l
|
|
227
|
+
if b > t:
|
|
228
|
+
b, t = t, b
|
|
229
|
+
|
|
230
|
+
return BoundingBox(l=l, t=t, r=r, b=b, coord_origin=origin)
|
|
231
|
+
|
|
232
|
+
def area(self) -> float:
|
|
233
|
+
"""area."""
|
|
234
|
+
area = (self.r - self.l) * (self.b - self.t)
|
|
235
|
+
if self.coord_origin == "BOTTOMLEFT":
|
|
236
|
+
area = -area
|
|
237
|
+
return area
|
|
238
|
+
|
|
239
|
+
def intersection_area_with(self, other: "BoundingBox") -> float:
|
|
240
|
+
"""intersection_area_with.
|
|
241
|
+
|
|
242
|
+
:param other: "BoundingBox":
|
|
243
|
+
|
|
244
|
+
"""
|
|
245
|
+
# Calculate intersection coordinates
|
|
246
|
+
left = max(self.l, other.l)
|
|
247
|
+
top = max(self.t, other.t)
|
|
248
|
+
right = min(self.r, other.r)
|
|
249
|
+
bottom = min(self.b, other.b)
|
|
250
|
+
|
|
251
|
+
# Calculate intersection dimensions
|
|
252
|
+
width = right - left
|
|
253
|
+
height = bottom - top
|
|
254
|
+
|
|
255
|
+
# If the bounding boxes do not overlap, width or height will be negative
|
|
256
|
+
if width <= 0 or height <= 0:
|
|
257
|
+
return 0.0
|
|
258
|
+
|
|
259
|
+
return width * height
|
|
260
|
+
|
|
261
|
+
def intersection_over_self_area(self, other: "BoundingBox") -> float:
|
|
262
|
+
"""intersection_over_self_area.
|
|
263
|
+
|
|
264
|
+
:param other: "BoundingBox":
|
|
265
|
+
|
|
266
|
+
"""
|
|
267
|
+
intersection_area = self.intersection_area_with(other)
|
|
268
|
+
return intersection_area / self.area()
|
|
269
|
+
|
|
270
|
+
def intersection_over_union(self, other: "BoundingBox") -> float:
|
|
271
|
+
"""intersection_over_union.
|
|
272
|
+
|
|
273
|
+
:param other: "BoundingBox":
|
|
274
|
+
|
|
275
|
+
"""
|
|
276
|
+
intersection_area = self.intersection_area_with(other)
|
|
277
|
+
union_area = self.area() + other.area() - intersection_area
|
|
278
|
+
return intersection_area / union_area
|
|
279
|
+
|
|
280
|
+
def intersection_over_minimum(self, other: "BoundingBox") -> float:
|
|
281
|
+
"""intersection_over_minimum.
|
|
282
|
+
|
|
283
|
+
:param other: "BoundingBox":
|
|
284
|
+
|
|
285
|
+
"""
|
|
286
|
+
intersection_area = self.intersection_area_with(other)
|
|
287
|
+
return intersection_area / min(self.area(), other.area())
|
|
288
|
+
|
|
289
|
+
def vertical_distance(self, box: "BoundingBox") -> float:
|
|
290
|
+
return min(abs(box.t - self.b), abs(self.t - box.b))
|
|
291
|
+
|
|
292
|
+
def horizontal_distance(self, box: "BoundingBox") -> float:
|
|
293
|
+
return min(abs(box.t - self.b), abs(self.t - box.b))
|
|
294
|
+
|
|
295
|
+
def is_not_too_low(self, box: "BoundingBox", threshold: float = 0.01) -> bool:
|
|
296
|
+
return max(self.b - box.b, 0) <= threshold * self.height
|
|
297
|
+
|
|
298
|
+
def is_not_too_high(self, box: "BoundingBox", threshold: float = 0.01) -> bool:
|
|
299
|
+
return max(box.t - self.t, 0) <= threshold * self.height
|
|
300
|
+
|
|
301
|
+
def is_inside(self, box: "BoundingBox", threshold: float = 0.2) -> bool:
|
|
302
|
+
cond_left = False
|
|
303
|
+
if self.l >= box.l:
|
|
304
|
+
cond_left = abs(self.t - box.t) <= threshold * self.height
|
|
305
|
+
else:
|
|
306
|
+
cond_left = True
|
|
307
|
+
cond_right = False
|
|
308
|
+
if self.r <= box.r:
|
|
309
|
+
cond_right = abs(self.r - box.r) <= threshold * self.height
|
|
310
|
+
else:
|
|
311
|
+
cond_right = True
|
|
312
|
+
return cond_left and cond_right
|
|
313
|
+
|
|
314
|
+
def to_bottom_left_origin(self, page_height) -> "BoundingBox":
|
|
315
|
+
"""to_bottom_left_origin.
|
|
316
|
+
|
|
317
|
+
:param page_height:
|
|
318
|
+
|
|
319
|
+
"""
|
|
320
|
+
if self.coord_origin == "BOTTOMLEFT":
|
|
321
|
+
return self
|
|
322
|
+
elif self.coord_origin == "TOPLEFT":
|
|
323
|
+
return BoundingBox(
|
|
324
|
+
l=self.l,
|
|
325
|
+
r=self.r,
|
|
326
|
+
t=page_height - self.t,
|
|
327
|
+
b=page_height - self.b,
|
|
328
|
+
coord_origin="BOTTOMLEFT",
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
def to_top_left_origin(self, page_height):
|
|
332
|
+
"""to_top_left_origin.
|
|
333
|
+
|
|
334
|
+
:param page_height:
|
|
335
|
+
|
|
336
|
+
"""
|
|
337
|
+
if self.coord_origin == "TOPLEFT":
|
|
338
|
+
return self
|
|
339
|
+
elif self.coord_origin == "BOTTOMLEFT":
|
|
340
|
+
return BoundingBox(
|
|
341
|
+
l=self.l,
|
|
342
|
+
r=self.r,
|
|
343
|
+
t=page_height - self.t, # self.b
|
|
344
|
+
b=page_height - self.b, # self.t
|
|
345
|
+
coord_origin="TOPLEFT",
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
def plot(
|
|
349
|
+
self,
|
|
350
|
+
image: Image.Image,
|
|
351
|
+
color: tuple | None = (255, 0, 0),
|
|
352
|
+
width: int = WIDTH_BOXES,
|
|
353
|
+
):
|
|
354
|
+
box = self.to_top_left_origin(image.size[1])
|
|
355
|
+
if self.relative:
|
|
356
|
+
box_absolute = box.to_absolute(page_size=self.reference, inplace=False)
|
|
357
|
+
else:
|
|
358
|
+
box_absolute = box
|
|
359
|
+
if color is None:
|
|
360
|
+
color = tuple(random.sample(range(0, 255, 1), 3))
|
|
361
|
+
draw = ImageDraw.Draw(image)
|
|
362
|
+
|
|
363
|
+
draw.rectangle(
|
|
364
|
+
box_absolute.as_tuple(),
|
|
365
|
+
outline=color,
|
|
366
|
+
width=width,
|
|
367
|
+
)
|
|
368
|
+
return image
|
|
369
|
+
|
|
370
|
+
@staticmethod
|
|
371
|
+
def merge_boxes(boxes: list["BoundingBox"]) -> "BoundingBox":
|
|
372
|
+
list_x = [box.l for box in boxes] + [box.r for box in boxes]
|
|
373
|
+
list_y = [box.t for box in boxes] + [box.b for box in boxes]
|
|
374
|
+
for b in boxes:
|
|
375
|
+
assert b.coord_origin == boxes[0].coord_origin
|
|
376
|
+
assert b.relative == boxes[0].relative
|
|
377
|
+
assert b.reference == boxes[0].reference
|
|
378
|
+
return BoundingBox(
|
|
379
|
+
coord_origin=boxes[0].coord_origin,
|
|
380
|
+
relative=boxes[0].relative,
|
|
381
|
+
reference=boxes[0].reference,
|
|
382
|
+
l=min(list_x),
|
|
383
|
+
r=max(list_x),
|
|
384
|
+
t=min(list_y),
|
|
385
|
+
b=max(list_y),
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
def rotate(self, angle: Literal[90, 180, 270], size: Size) -> "BoundingBox":
|
|
389
|
+
original_relative = self.relative
|
|
390
|
+
if self.coord_origin != "TOPLEFT":
|
|
391
|
+
raise ValueError("rotate assumes TOPLEFT coordinate origin")
|
|
392
|
+
|
|
393
|
+
# convert to absolute coordinates if needed
|
|
394
|
+
if original_relative:
|
|
395
|
+
l = self.l * size.width
|
|
396
|
+
t = self.t * size.height
|
|
397
|
+
r = self.r * size.width
|
|
398
|
+
b = self.b * size.height
|
|
399
|
+
else:
|
|
400
|
+
l, t, r, b = self.l, self.t, self.r, self.b
|
|
401
|
+
|
|
402
|
+
width, height = size.width, size.height
|
|
403
|
+
|
|
404
|
+
if angle == 270:
|
|
405
|
+
dx, dy = height, 0
|
|
406
|
+
elif angle == 180:
|
|
407
|
+
dx, dy = width, height
|
|
408
|
+
else: # 90
|
|
409
|
+
dx, dy = 0, width
|
|
410
|
+
|
|
411
|
+
corners = [
|
|
412
|
+
(l, t),
|
|
413
|
+
(r, t),
|
|
414
|
+
(r, b),
|
|
415
|
+
(l, b),
|
|
416
|
+
]
|
|
417
|
+
|
|
418
|
+
def _rotate_point(x: float, y: float):
|
|
419
|
+
if angle == 270:
|
|
420
|
+
return -y, x
|
|
421
|
+
elif angle == 180:
|
|
422
|
+
return -x, -y
|
|
423
|
+
else: # 90
|
|
424
|
+
return y, -x
|
|
425
|
+
|
|
426
|
+
rotated = [_rotate_point(x, y) for x, y in corners]
|
|
427
|
+
xs = [dx + p[0] for p in rotated]
|
|
428
|
+
ys = [dy + p[1] for p in rotated]
|
|
429
|
+
|
|
430
|
+
l_new, r_new = min(xs), max(xs)
|
|
431
|
+
t_new, b_new = min(ys), max(ys)
|
|
432
|
+
|
|
433
|
+
if angle in (90, 270):
|
|
434
|
+
new_width, new_height = height, width
|
|
435
|
+
else:
|
|
436
|
+
new_width, new_height = width, height
|
|
437
|
+
|
|
438
|
+
if original_relative:
|
|
439
|
+
return BoundingBox(
|
|
440
|
+
l=l_new / new_width,
|
|
441
|
+
t=t_new / new_height,
|
|
442
|
+
r=r_new / new_width,
|
|
443
|
+
b=b_new / new_height,
|
|
444
|
+
coord_origin="TOPLEFT",
|
|
445
|
+
relative=True,
|
|
446
|
+
reference=Size(width=new_width, height=new_height),
|
|
447
|
+
)
|
|
448
|
+
else:
|
|
449
|
+
return BoundingBox(
|
|
450
|
+
l=l_new,
|
|
451
|
+
t=t_new,
|
|
452
|
+
r=r_new,
|
|
453
|
+
b=b_new,
|
|
454
|
+
coord_origin="TOPLEFT",
|
|
455
|
+
relative=False,
|
|
456
|
+
reference=None,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def get_width_height_text(text, font, font_size):
|
|
461
|
+
size_text = font.getmask(text).getbbox()
|
|
462
|
+
if size_text is None:
|
|
463
|
+
(text_width, text_height) = (0, font_size)
|
|
464
|
+
else:
|
|
465
|
+
(text_width, text_height) = size_text[2::]
|
|
466
|
+
return text_width, text_height + 5
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def draw_highlighted_text(
|
|
470
|
+
image,
|
|
471
|
+
rectangle_coords,
|
|
472
|
+
text,
|
|
473
|
+
text_color=(0, 0, 0),
|
|
474
|
+
bg_color=(255, 255, 255),
|
|
475
|
+
opacity=0.8,
|
|
476
|
+
font_size: int = 20,
|
|
477
|
+
font_name: str = "DejaVuSans.ttf",
|
|
478
|
+
):
|
|
479
|
+
"""
|
|
480
|
+
Dessine un texte avec un rectangle semi-transparent en surbrillance.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
image (PIL.Image.Image): L'image de base.
|
|
484
|
+
rectangle_coords (tuple): Les coordonnées du rectangle (x1, y1, x2, y2).
|
|
485
|
+
text (str): Le texte à écrire.
|
|
486
|
+
text_color (tuple): Couleur du texte (R, G, B).
|
|
487
|
+
bg_color (tuple): Couleur de fond du rectangle (R, G, B).
|
|
488
|
+
opacity (float): Opacité du rectangle (entre 0 et 1).
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
PIL.Image.Image: L'image modifiée.
|
|
492
|
+
"""
|
|
493
|
+
font = ImageFont.truetype(font_name, font_size)
|
|
494
|
+
# Créer une image semi-transparente pour le rectangle
|
|
495
|
+
overlay = Image.new("RGBA", image.size, (0, 0, 0, 0))
|
|
496
|
+
draw_overlay = ImageDraw.Draw(overlay)
|
|
497
|
+
|
|
498
|
+
# Extraire les coordonnées
|
|
499
|
+
x1, y1, x2, y2 = rectangle_coords
|
|
500
|
+
|
|
501
|
+
# Dessiner le rectangle semi-transparent
|
|
502
|
+
bg_color_with_opacity = (
|
|
503
|
+
*bg_color,
|
|
504
|
+
int(255 * opacity),
|
|
505
|
+
) # Ajouter l'opacité à la couleur de fond
|
|
506
|
+
draw_overlay.rectangle(
|
|
507
|
+
rectangle_coords, fill=bg_color_with_opacity, width=WIDTH_BOXES
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
# Superposer l'overlay sur l'image de base
|
|
511
|
+
combined = Image.alpha_composite(image.convert("RGBA"), overlay)
|
|
512
|
+
|
|
513
|
+
# Créer un objet ImageDraw sur l'image combinée
|
|
514
|
+
draw_combined = ImageDraw.Draw(combined)
|
|
515
|
+
|
|
516
|
+
# Calculer la position centrée du texte
|
|
517
|
+
|
|
518
|
+
(text_width, text_height) = get_width_height_text(text, font, font_size)
|
|
519
|
+
text_x = x1 + (x2 - x1 - text_width) // 2
|
|
520
|
+
text_y = y1 + (y2 - y1 - text_height) // 2
|
|
521
|
+
|
|
522
|
+
# Dessiner le texte par-dessus le rectangle
|
|
523
|
+
draw_combined.text((text_x, text_y), text, fill=text_color, font=font)
|
|
524
|
+
|
|
525
|
+
return combined.convert("RGB") # Convertir en mode RGB si besoin
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def draw_text_of_box(
|
|
529
|
+
image,
|
|
530
|
+
left,
|
|
531
|
+
top,
|
|
532
|
+
text,
|
|
533
|
+
font_size: int = 20,
|
|
534
|
+
font_name: str = "DejaVuSans.ttf",
|
|
535
|
+
text_inside: bool = True,
|
|
536
|
+
**kwargs,
|
|
537
|
+
):
|
|
538
|
+
font = ImageFont.truetype(font_name, font_size)
|
|
539
|
+
(text_width, text_height) = get_width_height_text(text, font, font_size)
|
|
540
|
+
if text_inside:
|
|
541
|
+
rectangle_coords = (left, top, left + text_width, top + text_height)
|
|
542
|
+
else:
|
|
543
|
+
rectangle_coords = (left, top - text_height, left + text_width, top)
|
|
544
|
+
return draw_highlighted_text(
|
|
545
|
+
image=image,
|
|
546
|
+
rectangle_coords=rectangle_coords,
|
|
547
|
+
text=text,
|
|
548
|
+
font_name=font_name,
|
|
549
|
+
font_size=font_size,
|
|
550
|
+
**kwargs,
|
|
551
|
+
)
|