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.
Files changed (44) hide show
  1. vlmparse/benchpdf2md/bench_tests/benchmark_tsts.py +1763 -0
  2. vlmparse/benchpdf2md/bench_tests/utils.py +0 -0
  3. vlmparse/benchpdf2md/create_dataset.py +60 -0
  4. vlmparse/benchpdf2md/olmocrbench/katex/__init__.py +1 -0
  5. vlmparse/benchpdf2md/olmocrbench/katex/render.py +592 -0
  6. vlmparse/benchpdf2md/olmocrbench/repeatdetect.py +175 -0
  7. vlmparse/benchpdf2md/olmocrbench/run_olmocr_bench.py +256 -0
  8. vlmparse/benchpdf2md/olmocrbench/tests.py +1334 -0
  9. vlmparse/benchpdf2md/run_benchmark.py +296 -0
  10. vlmparse/benchpdf2md/st_visu_benchmark/app.py +271 -0
  11. vlmparse/benchpdf2md/st_visu_benchmark/highligh_text.py +117 -0
  12. vlmparse/benchpdf2md/st_visu_benchmark/test_form.py +95 -0
  13. vlmparse/benchpdf2md/st_visu_benchmark/ui_elements.py +20 -0
  14. vlmparse/benchpdf2md/st_visu_benchmark/utils.py +50 -0
  15. vlmparse/benchpdf2md/utils.py +56 -0
  16. vlmparse/clients/chandra.py +323 -0
  17. vlmparse/clients/deepseekocr.py +52 -0
  18. vlmparse/clients/docling.py +146 -0
  19. vlmparse/clients/dotsocr.py +277 -0
  20. vlmparse/clients/granite_docling.py +132 -0
  21. vlmparse/clients/hunyuanocr.py +45 -0
  22. vlmparse/clients/lightonocr.py +43 -0
  23. vlmparse/clients/mineru.py +119 -0
  24. vlmparse/clients/nanonetocr.py +29 -0
  25. vlmparse/clients/olmocr.py +46 -0
  26. vlmparse/clients/openai_converter.py +173 -0
  27. vlmparse/clients/paddleocrvl.py +48 -0
  28. vlmparse/clients/pipe_utils/cleaner.py +74 -0
  29. vlmparse/clients/pipe_utils/html_to_md_conversion.py +136 -0
  30. vlmparse/clients/pipe_utils/utils.py +12 -0
  31. vlmparse/clients/prompts.py +66 -0
  32. vlmparse/data_model/box.py +551 -0
  33. vlmparse/data_model/document.py +148 -0
  34. vlmparse/servers/docker_server.py +199 -0
  35. vlmparse/servers/utils.py +250 -0
  36. vlmparse/st_viewer/fs_nav.py +53 -0
  37. vlmparse/st_viewer/st_viewer.py +80 -0
  38. {vlmparse-0.1.0.dist-info → vlmparse-0.1.3.dist-info}/METADATA +12 -1
  39. vlmparse-0.1.3.dist-info/RECORD +50 -0
  40. vlmparse-0.1.0.dist-info/RECORD +0 -13
  41. {vlmparse-0.1.0.dist-info → vlmparse-0.1.3.dist-info}/WHEEL +0 -0
  42. {vlmparse-0.1.0.dist-info → vlmparse-0.1.3.dist-info}/entry_points.txt +0 -0
  43. {vlmparse-0.1.0.dist-info → vlmparse-0.1.3.dist-info}/licenses/LICENSE +0 -0
  44. {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
+ )