• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

barseghyanartur / faker-file / 10821350919

11 Sep 2024 11:39PM CUT coverage: 99.969%. Remained the same
10821350919

push

github

web-flow
Up docs (#74)

3250 of 3251 relevant lines covered (99.97%)

1.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.39
/src/faker_file/contrib/pdf_file/pil_snippets.py
1
"""
2
.. code-block:: python
3

4
    from faker import Faker
5
    from faker_file.base import DynamicTemplate
6
    from faker_file.contrib.pdf_file.pil_snippets import *
7
    from faker_file.providers.pdf_file import PdfFileProvider
8
    from faker_file.providers.pdf_file.generators.pil_generator import (
9
        PilPdfGenerator
10
    )
11

12
    FAKER = Faker()
13
    FAKER.add_provider(PdfFileProvider)
14

15
    file = FAKER.pdf_file(
16
        pdf_generator_cls=PilPdfGenerator,
17
        content=DynamicTemplate(
18
            [
19
                (add_h1_heading, {}),
20
                (add_paragraph, {"max_nb_chars": 500}),
21
                (add_paragraph, {"max_nb_chars": 500}),
22
                (add_paragraph, {"max_nb_chars": 500}),
23
                (add_paragraph, {"max_nb_chars": 500}),
24
            ]
25
        )
26
    )
27

28
    file = FAKER.pdf_file(
29
        pdf_generator_cls=PilPdfGenerator,
30
        content=DynamicTemplate(
31
            [
32
                (add_h1_heading, {}),
33
                (add_paragraph, {}),
34
                (add_picture, {}),
35
                (add_paragraph, {}),
36
                (add_picture, {}),
37
                (add_paragraph, {}),
38
                (add_picture, {}),
39
                (add_paragraph, {}),
40
            ]
41
        )
42
    )
43

44
    file = FAKER.pdf_file(
45
        pdf_generator_cls=PilPdfGenerator,
46
        content=DynamicTemplate(
47
            [
48
                (add_h1_heading, {}),
49
                (add_picture, {}),
50
                (add_paragraph, {"max_nb_chars": 500}),
51
                (add_picture, {}),
52
                (add_paragraph, {"max_nb_chars": 500}),
53
                (add_picture, {}),
54
                (add_paragraph, {"max_nb_chars": 500}),
55
                (add_picture, {}),
56
                (add_paragraph, {"max_nb_chars": 500}),
57
            ]
58
        )
59
    )
60

61
    file = FAKER.pdf_file(
62
        pdf_generator_cls=PilPdfGenerator,
63
        content=DynamicTemplate(
64
            [
65
                (add_h1_heading, {}),
66
                (add_picture, {}),
67
                (add_paragraph, {"max_nb_chars": 500}),
68
                (add_table, {"rows": 5, "cols": 4}),
69
            ]
70
        )
71
    )
72

73
    file = FAKER.pdf_file(
74
        pdf_generator_cls=PilPdfGenerator,
75
        content=DynamicTemplate(
76
            [
77
                (add_h1_heading, {"margin": (2, 2)}),
78
                (add_picture, {"margin": (2, 2)}),
79
                (add_paragraph, {"max_nb_chars": 500, "margin": (2, 2)}),
80
                (add_picture, {"margin": (2, 2)}),
81
                (add_paragraph, {"max_nb_chars": 500, "margin": (2, 2)}),
82
                (add_picture, {"margin": (2, 2)}),
83
                (add_paragraph, {"max_nb_chars": 500, "margin": (2, 2)}),
84
                (add_picture, {"margin": (2, 2)}),
85
                (add_paragraph, {"max_nb_chars": 500, "margin": (2, 2)}),
86
            ]
87
        )
88
    )
89
"""
90

91
import logging
1✔
92
import textwrap
1✔
93
from collections import namedtuple
1✔
94
from io import BytesIO
1✔
95
from typing import Tuple, Union
1✔
96

97
from PIL import Image, ImageFont
1✔
98

99
from ...base import DEFAULT_FORMAT_FUNC
1✔
100

101
__author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
1✔
102
__copyright__ = "2022-2023 Artur Barseghyan"
1✔
103
__license__ = "MIT"
1✔
104
__all__ = (
1✔
105
    "add_h1_heading",
106
    "add_h2_heading",
107
    "add_h3_heading",
108
    "add_h4_heading",
109
    "add_h5_heading",
110
    "add_h6_heading",
111
    "add_heading",
112
    "add_page_break",
113
    "add_paragraph",
114
    "add_picture",
115
    "add_table",
116
)
117

118
LOGGER = logging.getLogger(__name__)
1✔
119

120

121
def expand_margin(
1✔
122
    margin: Union[Tuple[int, int], Tuple[int, int, int, int]]
123
) -> Tuple[int, int, int, int]:
124
    """Utility function to expand the margin tuple."""
125
    if len(margin) == 2:
1✔
126
        top, right = margin
1✔
127
        return top, right, top, right
1✔
128
    elif len(margin) == 4:
1✔
129
        return margin
1✔
130

131

132
def add_picture(
1✔
133
    provider,
134
    generator,
135
    data,
136
    counter,
137
    **kwargs,
138
) -> Tuple[bool, Tuple[int, int]]:
139
    """Callable responsible for picture generation using PIL."""
140
    # Extract margin values
141
    margin = kwargs.get("margin", (0, 0))
1✔
142
    top_margin, right_margin, bottom_margin, left_margin = expand_margin(margin)
1✔
143

144
    image_bytes = kwargs.get(
1✔
145
        "image_bytes", provider.generator.image()
146
    )  # Assuming image() returns bytes
147
    # Create a BytesIO object and load the image data
148
    with BytesIO(image_bytes) as input_stream:
1✔
149
        pil_image = Image.open(input_stream)
1✔
150

151
        # Resize the image
152
        new_width = kwargs.get("image_width", 200)
1✔
153
        new_height = kwargs.get("image_height", 200)
1✔
154
        if new_width > generator.page_width:
1✔
155
            new_width = generator.page_width
1✔
156
        if new_height > generator.page_height:
1✔
157
            new_height = generator.page_height
1✔
158
        pil_image = pil_image.resize((new_width, new_height))
1✔
159

160
        # Create a BytesIO object outside the 'with' statement
161
        output_stream = BytesIO()
1✔
162
        pil_image.save(output_stream, format="PNG")
1✔
163
        output_stream.seek(0)  # Move to the start of the stream
1✔
164

165
    # X, Y coordinates where the text will be placed
166
    # position = kwargs.get("position", (0, 0))
167
    # Adjust the position with margin for the left and top
168
    position = (
1✔
169
        kwargs.get("position", (0, 0))[0] + left_margin,
170
        kwargs.get("position", (0, 0))[1] + top_margin,
171
    )
172

173
    # Calculate the remaining space on the current page
174
    remaining_space = generator.page_height - position[1]
1✔
175

176
    # Create a PIL Image object from bytes
177
    image_to_paste = Image.open(output_stream)
1✔
178

179
    # Check if the image will fit on the current page
180
    # LOGGER.debug(f"image_to_paste.height: {image_to_paste.height}")
181
    # LOGGER.debug(f"remaining_space: {remaining_space}")
182
    if remaining_space < image_to_paste.height:
1✔
183
        # Image won't fit; add the current page to the list and create a new one
184
        generator.save_and_start_new_page()
1✔
185

186
        # Reset position to start of new page
187
        position = (0, 0)
1✔
188

189
    # Ensure that the document and the image to paste have the same mode
190
    if generator.img.mode != image_to_paste.mode:
1✔
191
        image_to_paste = image_to_paste.convert(generator.img.mode)
1✔
192

193
    # Create a mask if the image has an alpha channel
194
    mask = None
1✔
195
    if "A" in image_to_paste.getbands():
1✔
196
        mask = image_to_paste.split()[3]
×
197

198
    # Paste the image into the document
199
    generator.img.paste(image_to_paste, position, mask)
1✔
200

201
    # If you want to keep track of the last position to place
202
    # another element, you can.
203
    # last_position = (
204
    #     position[0] + image.width, position[1] + image_to_paste.height
205
    # )
206
    last_position = (0, position[1] + image_to_paste.height + bottom_margin)
1✔
207

208
    # Meta-data (optional)
209
    data.setdefault("content_modifiers", {})
1✔
210
    data["content_modifiers"].setdefault("add_picture", {})
1✔
211
    data["content_modifiers"]["add_picture"].setdefault(counter, [])
1✔
212
    data["content_modifiers"]["add_picture"][counter].append("Image added")
1✔
213

214
    return False, last_position
1✔
215

216

217
def add_paragraph(
1✔
218
    provider,
219
    generator,
220
    data,
221
    counter,
222
    **kwargs,
223
) -> Tuple[bool, Tuple[int, int]]:
224
    """Callable responsible for paragraph generation using PIL."""
225
    # Extract margin values
226
    margin = kwargs.get("margin", (0, 0))
1✔
227
    top_margin, right_margin, bottom_margin, left_margin = expand_margin(margin)
1✔
228
    content = kwargs.get("content", None)
1✔
229
    max_nb_chars = kwargs.get("max_nb_chars", 5_000)
1✔
230
    wrap_chars_after = kwargs.get("wrap_chars_after", None)
1✔
231
    # X, Y coordinates where the text will be placed
232
    # position = kwargs.get("position", (0, 0))
233
    # position = kwargs.get("position", (0, 0))
234
    # Adjust the position with margin for the left and top
235
    position = (
1✔
236
        kwargs.get("position", (0, 0))[0] + left_margin,
237
        kwargs.get("position", (0, 0))[1] + top_margin,
238
    )
239
    content_specs = kwargs.get("content_specs", {})
1✔
240
    format_func = kwargs.get("format_func", DEFAULT_FORMAT_FUNC)
1✔
241

242
    _content = provider._generate_text_content(
1✔
243
        max_nb_chars=max_nb_chars,
244
        wrap_chars_after=wrap_chars_after,
245
        content=content,
246
        format_func=format_func,
247
    )
248
    font = ImageFont.truetype(generator.font, generator.font_size)
1✔
249
    lines = _content.split("\n")
1✔
250
    line_max_num_chars = generator.find_max_fit_for_multi_line_text(
1✔
251
        generator.draw,
252
        lines,
253
        font,
254
        generator.page_width,
255
    )
256
    wrap_chars_after = content_specs.get("wrap_chars_after")
1✔
257
    if (
1✔
258
        not wrap_chars_after
259
        or wrap_chars_after
260
        and (wrap_chars_after > line_max_num_chars)
261
    ):
262
        lines = textwrap.wrap(_content, line_max_num_chars)
1✔
263

264
    # Load a truetype or opentype font file, and create a font object.
265
    font = ImageFont.truetype(generator.font, generator.font_size)
1✔
266

267
    y_text = position[1]
1✔
268
    # LOGGER.debug(f"position: {position}")
269
    for counter, line in enumerate(lines):
1✔
270
        text_width, text_height = generator.draw.textsize(
1✔
271
            line, font=font, spacing=generator.spacing
272
        )
273
        if y_text + text_height > generator.page_height:
1✔
274
            generator.save_and_start_new_page()
1✔
275
            y_text = 0
1✔
276

277
        generator.draw.text(
1✔
278
            (position[0], y_text),
279
            line,
280
            fill=(0, 0, 0),
281
            spacing=generator.spacing,
282
            font=font,
283
        )
284
        # Move down for next line
285
        y_text += text_height + generator.line_height
1✔
286

287
    # If you want to keep track of the last position to place another
288
    # element, you can.
289
    # last_position = (position[0], y_text)
290
    # last_position = (0, y_text)
291
    last_position = (0, y_text + bottom_margin)
1✔
292

293
    # Add meta-data, assuming data is a dictionary for tracking
294
    data.setdefault("content_modifiers", {})
1✔
295
    data["content_modifiers"].setdefault("add_paragraph", {})
1✔
296
    data["content_modifiers"]["add_paragraph"].setdefault(counter, [])
1✔
297
    data["content_modifiers"]["add_paragraph"][counter].append(_content)
1✔
298
    data.setdefault("content", "")
1✔
299
    data["content"] += "\r\n" + _content
1✔
300

301
    return False, last_position
1✔
302

303

304
def add_page_break(
1✔
305
    provider,
306
    generator,
307
    data,
308
    counter,
309
    **kwargs,
310
) -> Tuple[bool, Tuple[int, int]]:
311
    """Callable responsible for paragraph generation using PIL."""
312
    generator.save_and_start_new_page()
1✔
313

314
    # If you want to keep track of the last position to place another
315
    # element, you can.
316
    # last_position = (position[0], 0)
317
    last_position = (0, 0)
1✔
318

319
    return False, last_position
1✔
320

321

322
def get_heading_font_size(base_size: int, heading_level: int) -> int:
1✔
323
    return base_size * (8 - heading_level) // 2
1✔
324

325

326
def add_heading(
1✔
327
    provider,
328
    generator,
329
    data,
330
    counter,
331
    **kwargs,
332
) -> Tuple[bool, Tuple[int, int]]:
333
    """Callable responsible for H1 heading generation using PIL."""
334
    # Extract margin values
335
    margin = kwargs.get("margin", (0, 0))
1✔
336
    top_margin, right_margin, bottom_margin, left_margin = expand_margin(margin)
1✔
337
    content = kwargs.get("content", None)
1✔
338
    max_nb_chars = kwargs.get("max_nb_chars", 30)
1✔
339
    wrap_chars_after = kwargs.get("wrap_chars_after", None)
1✔
340
    format_func = kwargs.get("format_func", DEFAULT_FORMAT_FUNC)
1✔
341
    # X, Y coordinates where the text will be placed
342
    # position = kwargs.get("position", (0, 0))
343
    # Adjust the position with margin for the left and top
344
    position = (
1✔
345
        kwargs.get("position", (0, 0))[0] + left_margin,
346
        kwargs.get("position", (0, 0))[1] + top_margin,
347
    )
348
    level = kwargs.get("level", 1)
1✔
349
    if level < 1 or level > 6:
1✔
350
        level = 1
1✔
351

352
    font_size = get_heading_font_size(generator.font_size, level)
1✔
353

354
    _content = provider._generate_text_content(
1✔
355
        max_nb_chars=max_nb_chars,
356
        wrap_chars_after=wrap_chars_after,
357
        content=content,
358
        format_func=format_func,
359
    )
360

361
    # Here, you'll specify a different font size for heading
362
    font = ImageFont.truetype(generator.font, font_size)
1✔
363

364
    y = position[1]
1✔
365
    generator.draw.text(
1✔
366
        (position[0], y),
367
        _content,
368
        fill=(0, 0, 0),
369
        font=font,
370
    )
371

372
    text_width, text_height = generator.draw.textsize(_content, font=font)
1✔
373
    y += text_height
1✔
374

375
    # If you want to keep track of the last position to place another
376
    # element, you can.
377
    # last_position = (position[0], y)
378
    # last_position = (0, y)
379
    last_position = (0, y + bottom_margin)
1✔
380

381
    # Add meta-data, assuming data is a dictionary for tracking
382
    data.setdefault("content_modifiers", {})
1✔
383
    data["content_modifiers"].setdefault("add_heading", {})
1✔
384
    data["content_modifiers"]["add_heading"].setdefault(counter, [])
1✔
385
    data["content_modifiers"]["add_heading"][counter].append(_content)
1✔
386
    data.setdefault("content", "")
1✔
387
    data["content"] += "\r\n" + _content
1✔
388

389
    return False, last_position
1✔
390

391

392
def add_h1_heading(
1✔
393
    provider, generator, data, counter, **kwargs
394
) -> Tuple[bool, Tuple[int, int]]:
395
    """Callable responsible for the h1 heading generation."""
396
    return add_heading(provider, generator, data, counter, level=1, **kwargs)
1✔
397

398

399
def add_h2_heading(
1✔
400
    provider, generator, data, counter, **kwargs
401
) -> Tuple[bool, Tuple[int, int]]:
402
    """Callable responsible for the h2 heading generation."""
403
    return add_heading(provider, generator, data, counter, level=2, **kwargs)
1✔
404

405

406
def add_h3_heading(
1✔
407
    provider, generator, data, counter, **kwargs
408
) -> Tuple[bool, Tuple[int, int]]:
409
    """Callable responsible for the h3 heading generation."""
410
    return add_heading(provider, generator, data, counter, level=3, **kwargs)
1✔
411

412

413
def add_h4_heading(
1✔
414
    provider, generator, data, counter, **kwargs
415
) -> Tuple[bool, Tuple[int, int]]:
416
    """Callable responsible for the h4 heading generation."""
417
    return add_heading(provider, generator, data, counter, level=4, **kwargs)
1✔
418

419

420
def add_h5_heading(
1✔
421
    provider, generator, data, counter, **kwargs
422
) -> Tuple[bool, Tuple[int, int]]:
423
    """Callable responsible for the h5 heading generation."""
424
    return add_heading(provider, generator, data, counter, level=5, **kwargs)
1✔
425

426

427
def add_h6_heading(
1✔
428
    provider, generator, data, counter, **kwargs
429
) -> Tuple[bool, Tuple[int, int]]:
430
    """Callable responsible for the h6 heading generation."""
431
    return add_heading(provider, generator, data, counter, level=6, **kwargs)
1✔
432

433

434
# This is a simple placeholder for your table object
435
Table = namedtuple("Table", ["data"])
1✔
436

437

438
def draw_table_cell(document, cell_content, position, cell_size, font):
1✔
439
    """Draw a table cell."""
440
    x, y = position
1✔
441
    width, height = cell_size
1✔
442
    border_color = (0, 0, 0)
1✔
443
    fill_color = (255, 255, 255)
1✔
444

445
    # Draw the rectangle
446
    document.rectangle(
1✔
447
        [position, (x + width, y + height)],
448
        fill=fill_color,
449
        outline=border_color,
450
    )
451

452
    # Draw text in the rectangle
453
    text_width, text_height = document.textsize(cell_content, font=font)
1✔
454
    text_position = (
1✔
455
        x + (width - text_width) // 2,
456
        y + (height - text_height) // 2,
457
    )
458
    document.text(text_position, cell_content, fill=(0, 0, 0), font=font)
1✔
459

460

461
def add_table(
1✔
462
    provider,
463
    generator,
464
    data,
465
    counter,
466
    **kwargs,
467
) -> Tuple[bool, Tuple[int, int]]:
468
    """Callable responsible for table generation using PIL."""
469
    # X, Y coordinates where the table will be placed
470
    position = kwargs.get("position", (0, 0))
1✔
471

472
    # Default cell dimensions
473
    cell_width = kwargs.get("cell_width", 100)
1✔
474
    cell_height = kwargs.get("cell_height", 30)
1✔
475

476
    # Font for the table cells
477
    font = ImageFont.truetype(generator.font, generator.font_size)
1✔
478

479
    # Extract or generate table data
480
    rows = kwargs.get("rows", 3)
1✔
481
    cols = kwargs.get("cols", 4)
1✔
482
    headers = [f"Header {i + 1}" for i in range(cols)]
1✔
483
    table_data = [
1✔
484
        [provider.generator.word() for _ in range(cols)] for _ in range(rows)
485
    ]
486
    table_data.insert(0, headers)
1✔
487
    table = Table(table_data)
1✔
488

489
    y = position[1]
1✔
490
    for row in table.data:
1✔
491
        x = position[0]
1✔
492
        for cell_content in row:
1✔
493
            cell_position = (x, y)
1✔
494
            draw_table_cell(
1✔
495
                generator.draw,
496
                cell_content,
497
                cell_position,
498
                (cell_width, cell_height),
499
                font,
500
            )
501
            x += cell_width
1✔
502
        y += cell_height
1✔
503

504
    last_position = (0, y)
1✔
505

506
    # Add meta-data, assuming data is a dictionary for tracking
507
    data.setdefault("content_modifiers", {})
1✔
508
    data["content_modifiers"].setdefault("add_table", {})
1✔
509
    data["content_modifiers"]["add_table"].setdefault(counter, [])
1✔
510
    data["content_modifiers"]["add_table"][counter].append("Table added")
1✔
511

512
    return False, last_position
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc