diff --git a/.secrets.baseline b/.secrets.baseline index 967416a7..cf73d772 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -172,7 +172,7 @@ "filename": "src/faker_file/tests/test_sftp_server.py", "hashed_secret": "e8662cfb96bd9c7fe84c31d76819ec3a92c80e63", "is_verified": true, - "line_number": 83 + "line_number": 84 } ], "src/faker_file/tests/test_sftp_storage.py": [ @@ -194,5 +194,5 @@ } ] }, - "generated_at": "2023-08-23T21:49:15Z" + "generated_at": "2023-09-20T19:26:51Z" } diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 908055e1..1527811e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,7 @@ Release history and notes .. _imgkit: https://pypi.org/project/imgkit/ .. _pdf2image: https://pypi.org/project/pdf2image/ .. _pdfkit: https://pypi.org/project/pdfkit/ +.. _Pillow: https://pypi.org/project/pillow/ .. _reportlab: https://pypi.org/project/reportlab/ `Sequence based identifiers @@ -25,6 +26,25 @@ are used for versioning (schema follows below): 0.3.4 to 0.4). - All backwards incompatible changes are mentioned in this document. +0.17.8 +------ +2023-09-21 + +.. note:: + + This release is dedicated to the victims of the war in Artsakh + (Nagorno-Karabakh), a land now lost to its native inhabitants (Armenians). + Following a grueling nine-month blockade, Azerbaijan initiated another + military onslaught on September 19, 2023. The already weakened and + outnumbered forces of Artsakh could no longer mount an effective + resistance. + +- Added support for ``DynamicTemplate`` to all non-graphic image providers. + That means, that you can produce images with text, tables, various + headings and other images. Correspondent snippets are implemented for all + supported image generators; namely `reportlab`_, `WeasyPrint`_ and + `Pillow`_. + 0.17.7 ------ 2023-09-12 diff --git a/README.rst b/README.rst index 97312be9..136dca46 100644 --- a/README.rst +++ b/README.rst @@ -36,6 +36,7 @@ faker-file .. _Creating PDF: https://faker-file.readthedocs.io/en/latest/creating_pdf.html .. _Creating DOCX: https://faker-file.readthedocs.io/en/latest/creating_docx.html .. _Creating ODT: https://faker-file.readthedocs.io/en/latest/creating_odt.html +.. _Creating images: https://faker-file.readthedocs.io/en/latest/creating_images.html .. _CLI: https://faker-file.readthedocs.io/en/latest/cli.html .. _Methodology: https://faker-file.readthedocs.io/en/latest/methodology.html .. _Contributor guidelines: https://faker-file.readthedocs.io/en/latest/contributor_guidelines.html @@ -108,15 +109,13 @@ All licenses are mentioned below between the brackets. been tested with ``Django`` starting from version 2.2 to 4.2 (although only maintained versions of Django are currently being tested against). - ``BMP``, ``GIF`` and ``TIFF`` file support requires either just - `Pillow`_ (`HPND`) for very basic functionality, or a combination of - `WeasyPrint`_ (`BSD`), `pdf2image`_ (`MIT`), `Pillow`_ (`HPND`) and - `poppler`_ (`GPLv2`) for advanced functionality. + `Pillow`_ (`HPND`), or a combination of `WeasyPrint`_ (`BSD`), + `pdf2image`_ (`MIT`), `Pillow`_ (`HPND`) and `poppler`_ (`GPLv2`). - ``DOCX`` file support requires `python-docx`_ (`MIT`). - ``EPUB`` file support requires `xml2epub`_ (`MIT`) and `Jinja2`_ (`BSD`). - ``ICO``, ``JPEG``, ``PNG``, ``SVG`` and ``WEBP`` files support - requires either just `Pillow`_ (`HPND`) for very basic functionality, or a - combination of `imgkit`_ (`MIT`) and `wkhtmltopdf`_ (`LGPLv3`) for advanced - functionality. + requires either just `Pillow`_ (`HPND`), or a combination of + `imgkit`_ (`MIT`) and `wkhtmltopdf`_ (`LGPLv3`). - ``MP3`` file support requires `gTTS`_ (`MIT`) or `edge-tts`_ (`GPLv3`). - ``PDF`` file support requires either combination of `pdfkit`_ (`MIT`) and `wkhtmltopdf`_ (`LGPLv3`), or `reportlab`_ (`BSD`). @@ -144,6 +143,7 @@ Documentation - For tips on ``PDF`` creation see `Creating PDF`_. - For tips on ``DOCX`` creation see `Creating DOCX`_. - For tips on ``ODT`` creation see `Creating ODT`_. +- For tips on images creation see `Creating images`_. - For CLI options see the `CLI`_. - Read the `Methodology`_. - For guidelines on contributing check the `Contributor guidelines`_. @@ -293,8 +293,8 @@ Supported file types - ``ZIP`` For all image formats (``BMP``, ``ICO``, ``GIF``, ``JPEG``, ``PNG``, ``SVG``, -``TIFF`` and ``WEBP``) and ``PDF``, there are both graphic and text-to-image -file providers. +``TIFF`` and ``WEBP``) and ``PDF``, there are both graphic-only and +mixed-content file providers (that also have text-to-image capabilities). Additional providers -------------------- diff --git a/SECURITY.rst b/SECURITY.rst index e6ad062a..df1ca3e0 100644 --- a/SECURITY.rst +++ b/SECURITY.rst @@ -19,12 +19,14 @@ of ``faker-file`` 0.17.x, support will be provided for ``faker-file`` 0.16.x. Upon the release of ``faker-file`` 0.18.x, security support for ``faker-file`` 0.16.x will end. -+-----------+-----------+ -| Version | Supported | -+===========+===========+ -| 0.17.x | Yes | -+-----------+-----------+ -| 0.16.x | Yes | -+-----------+-----------+ -| < 0.16 | No | -+-----------+-----------+ +.. code-block:: text + + ┌─────────────────┬────────────────┐ + │ Version │ Supported │ + ├─────────────────┼────────────────┤ + │ 0.17.x │ Yes │ + ├─────────────────┼────────────────┤ + │ 0.16.x │ Yes │ + ├─────────────────┼────────────────┤ + │ < 0.16 │ No │ + └─────────────────┴────────────────┘ diff --git a/docs/creating_images.rst b/docs/creating_images.rst new file mode 100644 index 00000000..a3a958eb --- /dev/null +++ b/docs/creating_images.rst @@ -0,0 +1,399 @@ +Creating images +=============== +.. Internal references + +.. _faker-file: https://pypi.org/project/faker-file/ + +.. External references + +.. _imgkit: https://pypi.org/project/imgkit/ +.. _Pillow: https://pillow.readthedocs.io/ +.. _WeasyPrint: https://pypi.org/project/weasyprint/ +.. _wkhtmltopdf: https://wkhtmltopdf.org/ + +Creating images could be a challenging task. System dependencies on one +side, large variety of many image formats on another. + +Underlying creation of image files has been delegated to an abstraction layer +of image generators. If you don't like how image files are generated or format +you need isn't supported, you can create your own layer, using your favourite +library. + +Generally speaking, in `faker-file`_ each file provider represents a certain +file type (with only a few exceptions). For generating a file in PNG format +you should use `PngFileProvider`. For JPEG you would use `JpegFileProvider`. + +Image providers +--------------- +Currently, there are 3 types of image providers implemented: + +- Graphic-only image providers. +- Mixed-content image providers. +- Image augmentation providers. + +The graphic-only image providers are only capable of producing random +graphics. + +The mixed-content image providers can produce an image consisting of +both text and graphics. Moreover, text comes in variety of different +headings (such as h1, h2, h3, etc), paragraphs and tables. + +Image augmentation providers simply augment existing images in a various, +declaratively random, ways, such as: flip, resize, lighten, darken, +grayscale and others. + +Image generators +---------------- +The following image generators are available. + +- ``PilImageGenerator``, built on top of the `Pillow`_. It's the generator + that will likely won't ask for any system dependencies that you don't + yet have installed. +- ``ImgkitImageGenerator`` (default), built on top of the `imgkit`_ + and `wkhtmltopdf`_. Extremely easy to work with. Supports many formats. +- ``WeasyPrintImageGenerator``, built on top of the `WeasyPrint`_. + Easy to work with. Supports formats that `imgkit`_ does not. + +Building mixed-content images using `imgkit`_ +--------------------------------------------- +While `imgkit`_ generator is heavier and has `wkhtmltopdf`_ as a system +dependency, it produces better quality images and has no issues with fonts +or unicode characters. + +See the following full functional snippet for generating images using `imgkit`_. + +.. code-block:: python + :name: test_building_images_using_imgkit + + from faker import Faker + from faker_file.providers.png_file import PngFileProvider + from faker_file.providers.image.imgkit_generator import ( + ImgkitImageGenerator, + ) + + FAKER = Faker() # Initialize Faker + FAKER.add_provider(PngFileProvider) # Register PngFileProvider + + # Generate PNG file using `imgkit` + pdf_file = FAKER.png_file(image_generator_cls=ImgkitImageGenerator) + +The generated PNG image will have 10,000 characters of text. The generated image +will be as wide as needed to fit those 10,000 characters, but newlines are +respected. + +If you want image to be less wide, set value of ``wrap_chars_after`` to 80 +characters (or any other number that fits your needs). See the example below: + +.. code-block:: python + + # Generate an image file, wrapping each line after 80 characters + png_file = FAKER.png_file( + image_generator_cls=ImgkitImageGenerator, wrap_chars_after=80 + ) + +To have a longer text, increase the value of ``max_nb_chars`` accordingly. +See the example below: + +.. code-block:: python + + # Generate an image file of 20,000 characters + png_file = FAKER.png_file( + image_generator_cls=ImgkitImageGenerator, max_nb_chars=20_000 + ) + +As mentioned above, it's possible to diversify the generated context with +images, paragraphs, tables and pretty much everything that you could think of, +although currently only images, paragraphs and tables are supported out of +the box. In order to customise the blocks image file is built from, +the ``DynamicTemplate`` class is used. See the example below for usage +examples: + +.. code-block:: python + + # Additional imports + from faker_file.base import DynamicTemplate + from faker_file.contrib.image.imgkit_snippets import ( + add_paragraph, + add_picture, + add_table, + ) + + # Create an image file with a paragraph, a picture and a table. + # The ``DynamicTemplate`` simply accepts a list of callables (such + # as ``add_paragraph``, ``add_picture``) and dictionary to be later on + # fed to the callables as keyword arguments for customising the default + # values. + png_file = FAKER.png_file( + image_generator_cls=ImgkitImageGenerator, + content=DynamicTemplate( + [ + (add_paragraph, {}), # Add paragraph + (add_picture, {}), # Add picture + (add_table, {}), # Add table + ] + ) + ) + + # You could make the list as long as you like or simply multiply for + # easier repetition as follows: + png_file = FAKER.png_file( + image_generator_cls=ImgkitImageGenerator, + content=DynamicTemplate( + [ + (add_paragraph, {}), # Add paragraph + (add_picture, {}), # Add picture + (add_table, {}), # Add table + ] * 100 # Will repeat your config 100 times + ) + ) + +Building mixed-content images using `WeasyPrint`_ +------------------------------------------------- +While `WeasyPrint`_ generator isn't better or faster than the `imgkit`_, it +supports formats that `imgkit`_ doesn't (and vice-versa) and therefore is a +good alternative to. + +See the following snippet for generating images using `WeasyPrint`_. + +.. code-block:: python + :name: test_building_images_using_weasyprint + + from faker import Faker + from faker_file.providers.png_file import PngFileProvider + from faker_file.providers.image.weasyprint_generator import ( + WeasyPrintImageGenerator, + ) + + FAKER = Faker() # Initialize Faker + FAKER.add_provider(PngFileProvider) # Register provider + + # Generate image file using `WeasyPrint` + png_file = FAKER.png_file(image_generator_cls=WeasyPrintImageGenerator) + +All examples shown for `imgkit`_ apply for `WeasyPrint`_ generator, however +when building images files from blocks (paragraphs, images and tables), the +imports shall be adjusted: + +As mentioned above, it's possible to diversify the generated context with +images, paragraphs, tables and pretty much everything else that you could +think of, although currently only images, paragraphs and tables are supported. +In order to customise the blocks image file is built from, the +``DynamicTemplate`` class is used. See the example below for usage examples: + +.. code-block:: python + + # Additional imports + from faker_file.base import DynamicTemplate + from faker_file.contrib.image.weasyprint_snippets import ( + add_paragraph, + add_picture, + add_table, + ) + + # Create an image file with paragraph, picture and table. + # The ``DynamicTemplate`` simply accepts a list of callables (such + # as ``add_paragraph``, ``add_picture``) and dictionary to be later on + # fed to the callables as keyword arguments for customising the default + # values. + png_file = FAKER.png_file( + image_generator_cls=WeasyPrintImageGenerator, + content=DynamicTemplate( + [ + (add_paragraph, {}), # Add paragraph + (add_picture, {}), # Add picture + (add_table, {}), # Add table + ] + ) + ) + + # You could make the list as long as you like or simply multiply for + # easier repetition as follows: + png_file = FAKER.png_file( + image_generator_cls=WeasyPrintImageGenerator, + content=DynamicTemplate( + [ + (add_paragraph, {}), # Add paragraph + (add_picture, {}), # Add picture + (add_table, {}), # Add table + ] * 100 + ) + ) + +Building mixed-content images using `Pillow`_ +--------------------------------------------- +Usage example: + +.. code-block:: python + :name: test_building_images_using_pillow + + from faker import Faker + from faker_file.providers.png_file import PngFileProvider + from faker_file.providers.image.pil_generator import PilImageGenerator + + FAKER = Faker() + FAKER.add_provider(PngFileProvider) + + png_file = FAKER.png_file(image_generator_cls=PilImageGenerator) + +With options: + +.. code-block:: python + + png_file = FAKER.png_file( + image_generator_cls=PilImageGenerator, + image_generator_kwargs={ + "encoding": "utf8", + "font_size": 14, + "page_width": 800, + "page_height": 1200, + "line_height": 16, + "spacing": 5, + }, + wrap_chars_after=100, + ) + +All examples shown for `imgkit`_ and `WeasyPrint`_ apply to `Pillow`_ generator, +however when building image files from blocks (paragraphs, images and tables), +the imports shall be adjusted. See the example below: + +.. code-block:: python + + # Additional imports + from faker_file.base import DynamicTemplate + from faker_file.contrib.png_file.pil_snippets import ( + add_paragraph, + add_picture, + add_table, + ) + + # Create an image file with paragraph, picture and table. + # The ``DynamicTemplate`` simply accepts a list of callables (such as + # ``add_paragraph``, ``add_picture``) and dictionary to be later on fed + # to the callables as keyword arguments for customising the default + # values. + png_file = FAKER.png_file( + image_generator_cls=PilImageGenerator, + content=DynamicTemplate( + [ + (add_paragraph, {}), # Add paragraph + (add_picture, {}), # Add picture + (add_table, {}), # Add table + ] + ) + ) + + # You could make the list as long as you like or simply multiply for + # easier repetition as follows: + png_file = FAKER.png_file( + image_generator_cls=PilImageGenerator, + content=DynamicTemplate( + [ + (add_paragraph, {}), # Add paragraph + (add_picture, {}), # Add picture + (add_table, {}), # Add table + ] * 100 + ) + ) + +Creating graphics-only images using `Pillow`_ +--------------------------------------------- +There are so called ``graphic`` image file providers available. Produced image +files would not contain text, so don't use it when you need text based content. +However, sometimes you just need a valid image file, without caring much about +the content. That's where graphic image providers comes to rescue: + +.. code-block:: python + :name: test_building_images_with_graphics_using_pillow + + from faker import Faker + from faker_file.providers.png_file import GraphicPngFileProvider + + FAKER = Faker() # Initialize Faker + FAKER.add_provider(GraphicPngFileProvider) # Register provider + + png_file = FAKER.graphic_png_file() + +The generated file will contain a random graphic (consisting of lines and +shapes of different colours). One of the most useful arguments supported is +``size``. + +.. code-block:: python + + png_file = FAKER.graphic_png_file( + size=(800, 800), + ) + +Augment existing images +----------------------- +Augment the input image with a series of random augmentation methods. + +.. code-block:: python + :name: test_augment_images_using_pillow + + from faker import Faker + from faker_file.base import DynamicTemplate + from faker_file.contrib.pdf_file.pil_snippets import * + from faker_file.providers.image.augment import ( + flip_horizontal, + flip_vertical, + decrease_contrast, + add_brightness, + resize_width, + resize_height, + ) + from faker_file.providers.image.pil_generator import PilImageGenerator + from faker_file.providers.png_file import ( + GraphicPngFileProvider, + PngFileProvider, + ) + from faker_file.providers.augment_image_from_path import ( + AugmentImageFromPathProvider + ) + from faker_file.providers.augment_random_image_from_dir import ( + AugmentRandomImageFromDirProvider + ) + + FAKER = Faker() + FAKER.add_provider(PngFileProvider) + FAKER.add_provider(GraphicPngFileProvider) + FAKER.add_provider(AugmentImageFromPathProvider) + FAKER.add_provider(AugmentRandomImageFromDirProvider) + + # Create a couple of graphic images to augment later on. + FAKER.graphic_png_file(basename="01") # One named 01.png + # And 5 more with random names. + for __ in range(5): + FAKER.graphic_png_file() + + # We could have also assumed that images directory exists and contains + # image files, amount which 01.png. Augmentations will be applied + # sequentially, one by one until all fulfilled. If you wish to apply only + # a random number of augmentations, but not all, pass the `num_steps` + # argument, with value less than the number of `augmentations` provided. + augmented_image_file = FAKER.augment_image_from_path( + path="/tmp/tmp/01.png", + augmentations=[ + (flip_horizontal, {}), + (flip_vertical, {}), + (decrease_contrast, {}), + (add_brightness, {}), + (resize_width, {"lower": 0.9, "upper": 1.1}), + (resize_height, {"lower": 0.9, "upper": 1.1}), + ], + prefix="augmented_image_01_", + # num_steps=3, + ) + + augmented_random_image_file = FAKER.augment_random_image_from_dir( + source_dir_path="/tmp/tmp/", + augmentations=[ + (flip_horizontal, {}), + (flip_vertical, {}), + (decrease_contrast, {}), + (add_brightness, {}), + (resize_width, {"lower": 0.9, "upper": 1.1}), + (resize_height, {"lower": 0.9, "upper": 1.1}), + ], + prefix="augmented_random_image_", + # num_steps=3, + ) diff --git a/docs/creating_pdf.rst b/docs/creating_pdf.rst index f9ff587c..07176466 100644 --- a/docs/creating_pdf.rst +++ b/docs/creating_pdf.rst @@ -19,11 +19,12 @@ Currently, there are three PDF generators implemented: - ``PdfkitPdfGenerator`` (default), built on top of the `pdfkit`_ and `wkhtmltopdf`_. -- ``PilPdfGenerator``, build on top of the `Pillow`_. Currently, the most - basic generator in terms of features, but on the same time, the - generator that will likely won't ask for any system dependencies that - you don't yet have installed. - ``ReportlabPdfGenerator``, build on top of the famous `reportlab`_. +- ``PilPdfGenerator``, build on top of the `Pillow`_. Produced PDFs would + contain images only (even texts are stored as images), unlike `pdfkit`_ or + `reportlab`_ based solutions, where PDFs would simply contain selectable + text. However, it's the generator that will likely won't ask for any + system dependencies that you don't yet have installed. Building PDF with text using `pdfkit`_ -------------------------------------- diff --git a/pytest.ini b/pytest.ini index 4842906b..a09b7ab6 100644 --- a/pytest.ini +++ b/pytest.ini @@ -31,3 +31,4 @@ addopts= --capture=no markers = optional: mark a test as a optional. + documentation: mark a test as a documentation test. diff --git a/setup.py b/setup.py index fb323455..2e878b10 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import find_packages, setup -version = "0.17.7" +version = "0.17.8" try: readme = open(os.path.join(os.path.dirname(__file__), "README.rst")).read() diff --git a/src/faker_file/__init__.py b/src/faker_file/__init__.py index 194bb50f..6bdc6e11 100644 --- a/src/faker_file/__init__.py +++ b/src/faker_file/__init__.py @@ -1,5 +1,5 @@ __title__ = "faker_file" -__version__ = "0.17.7" +__version__ = "0.17.8" __author__ = "Artur Barseghyan " __copyright__ = "2022-2023 Artur Barseghyan" __license__ = "MIT" diff --git a/src/faker_file/contrib/image/__init__.py b/src/faker_file/contrib/image/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/faker_file/contrib/image/imgkit_snippets.py b/src/faker_file/contrib/image/imgkit_snippets.py new file mode 100644 index 00000000..da04f7ee --- /dev/null +++ b/src/faker_file/contrib/image/imgkit_snippets.py @@ -0,0 +1,5 @@ +from ..pdf_file.pdfkit_snippets import * # noqa + +__author__ = "Artur Barseghyan " +__copyright__ = "2022-2023 Artur Barseghyan" +__license__ = "MIT" diff --git a/src/faker_file/contrib/image/pil_snippets.py b/src/faker_file/contrib/image/pil_snippets.py new file mode 100644 index 00000000..881f18f5 --- /dev/null +++ b/src/faker_file/contrib/image/pil_snippets.py @@ -0,0 +1,5 @@ +from ..pdf_file.pil_snippets import * # noqa + +__author__ = "Artur Barseghyan " +__copyright__ = "2022-2023 Artur Barseghyan" +__license__ = "MIT" diff --git a/src/faker_file/contrib/image/weasyprint_snippets.py b/src/faker_file/contrib/image/weasyprint_snippets.py new file mode 100644 index 00000000..ac15a0f4 --- /dev/null +++ b/src/faker_file/contrib/image/weasyprint_snippets.py @@ -0,0 +1,195 @@ +import base64 + +from pdf2image import convert_from_bytes +from weasyprint import HTML + +from ...base import DEFAULT_FORMAT_FUNC + +__author__ = "Artur Barseghyan " +__copyright__ = "2022-2023 Artur Barseghyan" +__license__ = "MIT" + + +def create_data_url(image_bytes: bytes, image_format: str) -> str: + """Create data URL.""" + encoded_image = base64.b64encode(image_bytes).decode("utf-8") + return f"data:image/{image_format};base64,{encoded_image}" + + +def add_table( + provider, + generator, + document, + data, + counter, + **kwargs, +): + """Callable responsible for the table generation using pdfkit.""" + rows = kwargs.get("rows", 3) + cols = kwargs.get("cols", 4) + + # Begin the HTML table + table_html = "" + + for row_num in range(rows): + table_html += "" + + for col_num in range(cols): + text = provider.generator.paragraph() + table_html += f"" + + # Meta-data + data.setdefault("content_modifiers", {}) + data["content_modifiers"].setdefault("add_table", {}) + data["content_modifiers"]["add_table"].setdefault(counter, []) + data["content_modifiers"]["add_table"][counter].append(text) + + table_html += "" + + # End the HTML table + table_html += "
{text}
" + + pdf_bytes = HTML(string=generator.wrap(table_html)).write_pdf() + generator.pages.extend(convert_from_bytes(pdf_bytes)) + + +def add_picture( + provider, + generator, + document, + data, + counter, + **kwargs, +): + """Callable responsible for the picture generation using pdfkit.""" + image = kwargs.get("image", provider.generator.image()) + data_url = create_data_url(image, "png") + image_html = f"Inline Image" + + pdf_bytes = HTML(string=generator.wrap(image_html)).write_pdf() + generator.pages.extend(convert_from_bytes(pdf_bytes)) + + +def add_page_break( + provider, + generator, + document, + data, + counter, + **kwargs, +): + """Callable responsible for the page break insertion using pdfkit.""" + page_break_html = "
" + pdf_bytes = HTML(string=generator.wrap(page_break_html)).write_pdf() + generator.pages.extend(convert_from_bytes(pdf_bytes)) + + +def add_paragraph( + provider, + generator, + document, + data, + counter, + **kwargs, +): + """Callable responsible for paragraph generation using pdfkit.""" + content = kwargs.get("content", None) + max_nb_chars = kwargs.get("content", 5_000) + wrap_chars_after = kwargs.get("wrap_chars_after", None) + format_func = kwargs.get("format_func", DEFAULT_FORMAT_FUNC) + + _content = provider._generate_text_content( + max_nb_chars=max_nb_chars, + wrap_chars_after=wrap_chars_after, + content=content, + format_func=format_func, + ) + + paragraph_html = f"

{_content}

" + pdf_bytes = HTML(string=generator.wrap(paragraph_html)).write_pdf() + generator.pages.extend(convert_from_bytes(pdf_bytes)) + + # Meta-data + data.setdefault("content_modifiers", {}) + data["content_modifiers"].setdefault("add_paragraph", {}) + data["content_modifiers"]["add_paragraph"].setdefault(counter, []) + data["content_modifiers"]["add_paragraph"][counter].append(_content) + data["content"] += "\r\n" + _content + + +def add_heading( + provider, + generator, + document, + data, + counter, + **kwargs, +): + """Callable responsible for heading generation using pdfkit.""" + content = kwargs.get("content", None) + max_nb_chars = kwargs.get("max_nb_chars", 30) + wrap_chars_after = kwargs.get("wrap_chars_after", None) + format_func = kwargs.get("format_func", DEFAULT_FORMAT_FUNC) + level = kwargs.get("level", 1) + if level < 1 or level > 6: + level = 1 + + _content = provider._generate_text_content( + max_nb_chars=max_nb_chars, + wrap_chars_after=wrap_chars_after, + content=content, + format_func=format_func, + ) + + heading_html = f"
{_content}
" + pdf_bytes = HTML(string=generator.wrap(heading_html)).write_pdf() + generator.pages.extend(convert_from_bytes(pdf_bytes)) + + # Meta-data + data.setdefault("content_modifiers", {}) + data["content_modifiers"].setdefault("add_heading", {}) + data["content_modifiers"]["add_heading"].setdefault(counter, []) + data["content_modifiers"]["add_heading"][counter].append(_content) + data["content"] += "\r\n" + _content + + +def add_h1_heading(provider, generator, document, data, counter, **kwargs): + """Callable responsible for the h1 heading generation.""" + return add_heading( + provider, generator, document, data, counter, level=1, **kwargs + ) + + +def add_h2_heading(provider, generator, document, data, counter, **kwargs): + """Callable responsible for the h2 heading generation.""" + return add_heading( + provider, generator, document, data, counter, level=2, **kwargs + ) + + +def add_h3_heading(provider, generator, document, data, counter, **kwargs): + """Callable responsible for the h3 heading generation.""" + return add_heading( + provider, generator, document, data, counter, level=3, **kwargs + ) + + +def add_h4_heading(provider, generator, document, data, counter, **kwargs): + """Callable responsible for the h4 heading generation.""" + return add_heading( + provider, generator, document, data, counter, level=4, **kwargs + ) + + +def add_h5_heading(provider, generator, document, data, counter, **kwargs): + """Callable responsible for the h5 heading generation.""" + return add_heading( + provider, generator, document, data, counter, level=5, **kwargs + ) + + +def add_h6_heading(provider, generator, document, data, counter, **kwargs): + """Callable responsible for the h6 heading generation.""" + return add_heading( + provider, generator, document, data, counter, level=6, **kwargs + ) diff --git a/src/faker_file/contrib/pdf_file/pil_snippets.py b/src/faker_file/contrib/pdf_file/pil_snippets.py index f8ac5633..3bcfa556 100644 --- a/src/faker_file/contrib/pdf_file/pil_snippets.py +++ b/src/faker_file/contrib/pdf_file/pil_snippets.py @@ -264,7 +264,7 @@ def add_paragraph( font = ImageFont.truetype(generator.font, generator.font_size) y_text = position[1] - LOGGER.error(f"position: {position}") + # LOGGER.debug(f"position: {position}") for counter, line in enumerate(lines): text_width, text_height = generator.draw.textsize( line, font=font, spacing=generator.spacing diff --git a/src/faker_file/providers/file_from_url.py b/src/faker_file/providers/file_from_url.py new file mode 100644 index 00000000..e69de29b diff --git a/src/faker_file/providers/image/augment.py b/src/faker_file/providers/image/augment.py index bd7e1503..a493185d 100644 --- a/src/faker_file/providers/image/augment.py +++ b/src/faker_file/providers/image/augment.py @@ -338,7 +338,7 @@ def augment_image( func_and_kwargs = pop_func(_augmentations) if func_and_kwargs: func, kwargs = func_and_kwargs - LOGGER.info(f"Applying {func} to {id(image_bytes)}") + # LOGGER.debug(f"Applying {func} to {id(image_bytes)}") try: image = func(image, **kwargs) except Exception as err: diff --git a/src/faker_file/providers/image/imgkit_generator.py b/src/faker_file/providers/image/imgkit_generator.py index e99db4d3..30f80d2c 100644 --- a/src/faker_file/providers/image/imgkit_generator.py +++ b/src/faker_file/providers/image/imgkit_generator.py @@ -8,6 +8,7 @@ from faker.generator import Generator from faker.providers.python import Provider +from ...base import DynamicTemplate, StringList from ...constants import DEFAULT_FILE_ENCODING from ..base.image_generator import BaseImageGenerator @@ -28,13 +29,57 @@ class ImgkitImageGenerator(BaseImageGenerator): from faker import Faker from faker_file.providers.png_file import PngFileProvider - from faker_file.providers.images.generators import imgkit_generator + from faker_file.providers.image.imgkit_generator import ( + ImgkitImageGenerator + ) FAKER = Faker() FAKER.add_provider(PngFileProvider) file = FAKER.png_file( - img_generator_cls=imgkit_generator.ImgkitImageGenerator + img_generator_cls=ImgkitImageGenerator + ) + + + Using `DynamicTemplate`: + + .. code-block:: python + + from faker_file.base import DynamicTemplate + from faker_file.contrib.image.imgkit_snippets import ( + add_h1_heading, + add_h2_heading, + add_h3_heading, + add_h4_heading, + add_h5_heading, + add_h6_heading, + add_heading, + add_page_break, + add_paragraph, + add_picture, + add_table, + ) + + # Create a file with lots of elements + file = FAKER.png_file( + image_generator_cls=ImgkitImageGenerator, + content=DynamicTemplate( + [ + (add_h1_heading, {}), + (add_paragraph, {}), + (add_h2_heading, {}), + (add_h3_heading, {}), + (add_h4_heading, {}), + (add_h5_heading, {}), + (add_h6_heading, {}), + (add_paragraph, {}), + (add_picture, {}), + (add_page_break, {}), + (add_h6_heading, {}), + (add_table, {}), + (add_paragraph, {}), + ] + ) ) """ @@ -52,8 +97,26 @@ def generate( ) -> bytes: """Generate image.""" with contextlib.redirect_stdout(io.StringIO()): + if isinstance(content, DynamicTemplate): + _content = StringList() + for counter, (ct_modifier, ct_modifier_kwargs) in enumerate( + content.content_modifiers + ): + ct_modifier( + provider, + self, + _content, + data, + counter, + **ct_modifier_kwargs, + ) + else: + _content = ( + f"
{content}
" + ) + return imgkit.from_string( - f"
{content}
", + str(_content), False, options={"format": provider.extension}, ) diff --git a/src/faker_file/providers/image/pil_generator.py b/src/faker_file/providers/image/pil_generator.py index 606ad2e7..7658c422 100644 --- a/src/faker_file/providers/image/pil_generator.py +++ b/src/faker_file/providers/image/pil_generator.py @@ -1,13 +1,15 @@ import logging +import textwrap from io import BytesIO from pathlib import Path -from typing import Any, Dict, Union +from typing import Any, Dict, List, Type, Union from faker import Faker from faker.generator import Generator from faker.providers.python import Provider from PIL import Image, ImageDraw, ImageFont +from ...base import DynamicTemplate from ...constants import DEFAULT_FILE_ENCODING from ..base.image_generator import BaseImageGenerator @@ -48,6 +50,94 @@ class PilImageGenerator(BaseImageGenerator): }, wrap_chars_after=119, ) + + With dynamic content: + + .. code-block:: python + + from faker import Faker + from faker_file.base import DynamicTemplate + from faker_file.contrib.image.pil_snippets import * + from faker_file.providers.image.pil_generator import PilImageGenerator + from faker_file.providers.png_file import PngFileProvider + + FAKER = Faker() + FAKER.add_provider(PngFileProvider) + + file = FAKER.png_file( + image_generator_cls=PilImageGenerator, + content=DynamicTemplate( + [ + (add_h1_heading, {}), + (add_paragraph, {"max_nb_chars": 500}), + (add_paragraph, {"max_nb_chars": 500}), + (add_paragraph, {"max_nb_chars": 500}), + (add_paragraph, {"max_nb_chars": 500}), + ] + ) + ) + + file = FAKER.png_file( + image_generator_cls=PilImageGenerator, + content=DynamicTemplate( + [ + (add_h1_heading, {}), + (add_paragraph, {}), + (add_picture, {}), + (add_paragraph, {}), + (add_picture, {}), + (add_paragraph, {}), + (add_picture, {}), + (add_paragraph, {}), + ] + ) + ) + + file = FAKER.png_file( + image_generator_cls=PilImageGenerator, + content=DynamicTemplate( + [ + (add_h1_heading, {}), + (add_picture, {}), + (add_paragraph, {"max_nb_chars": 500}), + (add_picture, {}), + (add_paragraph, {"max_nb_chars": 500}), + (add_picture, {}), + (add_paragraph, {"max_nb_chars": 500}), + (add_picture, {}), + (add_paragraph, {"max_nb_chars": 500}), + ] + ) + ) + + file = FAKER.png_file( + image_generator_cls=PilImageGenerator, + content=DynamicTemplate( + [ + (add_h1_heading, {}), + (add_picture, {}), + (add_paragraph, {"max_nb_chars": 500}), + (add_table, {"rows": 5, "cols": 4}), + ] + ) + ) + + file = FAKER.png_file( + image_generator_cls=PilImageGenerator, + content=DynamicTemplate( + [ + (add_h1_heading, {"margin": (2, 2)}), + (add_picture, {"margin": (2, 2)}), + (add_paragraph, {"max_nb_chars": 500, "margin": (2, 2)}), + (add_picture, {"margin": (2, 2)}), + (add_paragraph, {"max_nb_chars": 500, "margin": (2, 2)}), + (add_picture, {"margin": (2, 2)}), + (add_paragraph, {"max_nb_chars": 500, "margin": (2, 2)}), + (add_picture, {"margin": (2, 2)}), + (add_paragraph, {"max_nb_chars": 500, "margin": (2, 2)}), + ] + ) + ) """ encoding: str = DEFAULT_FILE_ENCODING @@ -58,6 +148,47 @@ class PilImageGenerator(BaseImageGenerator): line_height: int = 14 spacing: int = 6 + def __init__(self: "PilImageGenerator", **kwargs) -> None: + super().__init__(**kwargs) + self.pages = [] + self.img = None + self.draw = None + self.image_mode = "RGB" + + @classmethod + def find_max_fit_for_multi_line_text( + cls: Type["PilImageGenerator"], + draw: ImageDraw, + lines: List[str], + font: ImageFont, + max_width: int, + ): + # Find the longest line + longest_line = str(max(lines, key=len)) + return cls.find_max_fit_for_single_line_text( + draw, longest_line, font, max_width + ) + + @classmethod + def find_max_fit_for_single_line_text( + cls: Type["PilImageGenerator"], + draw: "ImageDraw", + text: str, + font: ImageFont, + max_width: int, + ) -> int: + low, high = 0, len(text) + while low < high: + mid = (high + low) // 2 + text_width, _ = draw.textsize(text[:mid], font=font) + + if text_width > max_width: + high = mid + else: + low = mid + 1 + + return low - 1 + def handle_kwargs(self: "PilImageGenerator", **kwargs) -> None: """Handle kwargs.""" if "encoding" in kwargs: @@ -72,6 +203,39 @@ def handle_kwargs(self: "PilImageGenerator", **kwargs) -> None: self.line_height = kwargs["line_height"] if "spacing" in kwargs: self.spacing = kwargs["spacing"] + if "image_mode" in kwargs: + self.image_mode = kwargs["image_mode"] + + def create_image_instance(self, height: Union[int, None] = None) -> Image: + return Image.new( + self.image_mode, + (self.page_width, height or self.page_height), + (255, 255, 255), + ) + + def start_new_page(self): + self.img = self.create_image_instance() + self.draw = ImageDraw.Draw(self.img) + + def save_and_start_new_page(self): + self.pages.append(self.img.copy()) + self.start_new_page() + + def combine_images_vertically(self): + # Calculate total width and height + total_width = max(image.width for image in self.pages) + total_height = sum(image.height for image in self.pages) + + # Create a new, white canvas to paste images onto + new_image = Image.new("RGB", (total_width, total_height), "white") + + # Paste each image + y_offset = 0 + for image in self.pages: + new_image.paste(image, (0, y_offset)) + y_offset += image.height + + return new_image def generate( self: "PilImageGenerator", @@ -81,21 +245,75 @@ def generate( **kwargs, ) -> bytes: """Generate image.""" - lines = content.split("\n") - height = len(lines) * self.font_size - img = Image.new( - "RGB", - (self.page_width, height or self.page_height), - (255, 255, 255), - ) - draw = ImageDraw.Draw(img) - font = ImageFont.truetype(self.font, self.font_size) - y_text = 0 - for line in lines: - draw.text((0, y_text), line, fill=(0, 0, 0), spacing=6, font=font) - y_text += self.line_height + position = (0, 0) + if isinstance(content, DynamicTemplate): + self.start_new_page() + for counter, (ct_modifier, ct_modifier_kwargs) in enumerate( + content.content_modifiers + ): + ct_modifier_kwargs["position"] = position + # LOGGER.debug(f"ct_modifier_kwargs: {ct_modifier_kwargs}") + add_page, position = ct_modifier( + provider, + self, + data, + counter, + **ct_modifier_kwargs, + ) + + self.pages.append(self.img.copy()) # Add as a new page + else: + self.img = self.create_image_instance() + self.draw = ImageDraw.Draw(self.img) + font = ImageFont.truetype(self.font, self.font_size) + + # The `content_specs` is a dictionary that holds two keys: + # `max_nb_chars` and `wrap_chars_after`. Those are the same values + # passed to the `PdfFileProvider`. + content_specs = kwargs.get("content_specs", {}) + lines = content.split("\n") + line_max_num_chars = self.find_max_fit_for_multi_line_text( + self.draw, + lines, + font, + self.page_width, + ) + wrap_chars_after = content_specs.get("wrap_chars_after") + if ( + not wrap_chars_after + or wrap_chars_after + and (wrap_chars_after > line_max_num_chars) + ): + lines = textwrap.wrap(content, line_max_num_chars) + + y_text = 0 + for counter, line in enumerate(lines): + text_width, text_height = self.draw.textsize( + line, font=font, spacing=self.spacing + ) + if y_text + text_height > self.page_height: + self.save_and_start_new_page() + y_text = 0 + + self.draw.text( + (0, y_text), + line, + fill=(0, 0, 0), + spacing=self.spacing, + font=font, + ) + y_text += text_height + self.line_height + + self.pages.append(self.img.copy()) # Add as a new page buffer = BytesIO() - img.save(buffer, format=provider.image_format) + # Combine images together vertically + combined_image = self.combine_images_vertically() + combined_image.save( + buffer, + resolution=100.0, + quality=95, + format=provider.image_format, + ) return buffer.getvalue() diff --git a/src/faker_file/providers/image/weasyprint_generator.py b/src/faker_file/providers/image/weasyprint_generator.py index 6b840544..f09b4e08 100644 --- a/src/faker_file/providers/image/weasyprint_generator.py +++ b/src/faker_file/providers/image/weasyprint_generator.py @@ -9,6 +9,7 @@ from PIL import Image from weasyprint import HTML +from ...base import DynamicTemplate, StringList from ...constants import DEFAULT_FILE_ENCODING from ..base.image_generator import BaseImageGenerator @@ -39,12 +40,133 @@ class WeasyPrintImageGenerator(BaseImageGenerator): file = FAKER.png_file( img_generator_cls=WeasyPrintImageGenerator ) + + With dynamic content: + + .. code-block:: python + + from faker import Faker + from faker_file.base import DynamicTemplate + from faker_file.contrib.image.weasyprint_snippets import * + from faker_file.providers.image.weasyprint_generator import ( + WeasyPrintImageGenerator + ) + from faker_file.providers.png_file import PngFileProvider + + FAKER = Faker() + FAKER.add_provider(PngFileProvider) + + file = FAKER.png_file( + image_generator_cls=WeasyPrintImageGenerator, + content=DynamicTemplate( + [ + (add_h1_heading, {}), + (add_paragraph, {"max_nb_chars": 500}), + (add_paragraph, {"max_nb_chars": 500}), + (add_paragraph, {"max_nb_chars": 500}), + (add_paragraph, {"max_nb_chars": 500}), + ] + ) + ) + + file = FAKER.png_file( + image_generator_cls=WeasyPrintImageGenerator, + content=DynamicTemplate( + [ + (add_h1_heading, {}), + (add_paragraph, {}), + (add_picture, {}), + (add_paragraph, {}), + (add_picture, {}), + (add_paragraph, {}), + (add_picture, {}), + (add_paragraph, {}), + ] + ) + ) + + file = FAKER.png_file( + image_generator_cls=WeasyPrintImageGenerator, + content=DynamicTemplate( + [ + (add_h1_heading, {}), + (add_picture, {}), + (add_paragraph, {"max_nb_chars": 500}), + (add_picture, {}), + (add_paragraph, {"max_nb_chars": 500}), + (add_picture, {}), + (add_paragraph, {"max_nb_chars": 500}), + (add_picture, {}), + (add_paragraph, {"max_nb_chars": 500}), + ] + ) + ) + + file = FAKER.png_file( + image_generator_cls=WeasyPrintImageGenerator, + content=DynamicTemplate( + [ + (add_h1_heading, {}), + (add_picture, {}), + (add_paragraph, {"max_nb_chars": 500}), + (add_table, {"rows": 5, "cols": 4}), + ] + ) + ) + + file = FAKER.png_file( + image_generator_cls=WeasyPrintImageGenerator, + content=DynamicTemplate( + [ + (add_h1_heading, {"margin": (2, 2)}), + (add_picture, {"margin": (2, 2)}), + (add_paragraph, {"max_nb_chars": 500, "margin": (2, 2)}), + (add_picture, {"margin": (2, 2)}), + (add_paragraph, {"max_nb_chars": 500, "margin": (2, 2)}), + (add_picture, {"margin": (2, 2)}), + (add_paragraph, {"max_nb_chars": 500, "margin": (2, 2)}), + (add_picture, {"margin": (2, 2)}), + (add_paragraph, {"max_nb_chars": 500, "margin": (2, 2)}), + ] + ) + ) """ encoding: str = DEFAULT_FILE_ENCODING + page_width: int = 794 # A4 size at 96 DPI + page_height: int = 1123 # A4 size at 96 DPI + wrapper_tag: str = "div" + + def __init__(self: "WeasyPrintImageGenerator", **kwargs): + super().__init__(**kwargs) + self.pages = [] + self.image_mode = "RGB" def handle_kwargs(self: "WeasyPrintImageGenerator", **kwargs) -> None: """Handle kwargs.""" + if "page_width" in kwargs: + self.page_width = kwargs["page_width"] + if "page_height" in kwargs: + self.page_height = kwargs["page_height"] + if "image_mode" in kwargs: + self.image_mode = kwargs["image_mode"] + if "wrapper_tag" in kwargs: + self.wrapper_tag = kwargs["wrapper_tag"] + + def create_image_instance( + self: "WeasyPrintImageGenerator", + width: Union[int, None] = None, + height: Union[int, None] = None, + ) -> Image: + return Image.new( + self.image_mode, + (width or self.page_width, height or self.page_height), + # (width, height), + (255, 255, 255), + ) + + def wrap(self: "WeasyPrintImageGenerator", content: str) -> str: + return f"<{self.wrapper_tag}>" f"{content}" f"" def generate( self: "WeasyPrintImageGenerator", @@ -54,30 +176,46 @@ def generate( **kwargs, ) -> bytes: """Generate image.""" - # Convert the HTML to a PDF and store in memory - pdf_bytes = HTML(string=f"
{content}
").write_pdf() + if isinstance(content, DynamicTemplate): + _content = StringList() + for counter, (ct_modifier, ct_modifier_kwargs) in enumerate( + content.content_modifiers + ): + ct_modifier( + provider, + self, + _content, + data, + counter, + **ct_modifier_kwargs, + ) + else: + # Convert the HTML to a PDF and store in memory + pdf_bytes = HTML( + string=f"<{self.wrapper_tag}>{content}" + ).write_pdf() - # Convert the in-memory PDF to images - images = convert_from_bytes(pdf_bytes) + # Convert the in-memory PDF to images + self.pages = convert_from_bytes(pdf_bytes) # If the PDF has multiple pages, stitch them together - if len(images) > 1: + if len(self.pages) > 1: # Get dimensions of combined image - width, height = images[0].size[0], sum( - [img.size[1] for img in images] + width, height = self.pages[0].size[0], sum( + [img.size[1] for img in self.pages] ) # Create a new image with white background - result = Image.new("RGB", (width, height), (255, 255, 255)) + result = self.create_image_instance(width, height) # Iterate over images and paste them onto the new image y_offset = 0 - for img in images: + for img in self.pages: result.paste(img, (0, y_offset)) y_offset += img.size[1] else: # If there's only one page, use it directly - result = images[0] + result = self.pages[0] # Save the result to a BytesIO object output = io.BytesIO() diff --git a/src/faker_file/providers/mixins/image_mixin.py b/src/faker_file/providers/mixins/image_mixin.py index 7e29bed7..4abdc566 100644 --- a/src/faker_file/providers/mixins/image_mixin.py +++ b/src/faker_file/providers/mixins/image_mixin.py @@ -4,7 +4,13 @@ from faker.generator import Generator from faker.providers.python import Provider -from ...base import DEFAULT_FORMAT_FUNC, BytesValue, FileMixin, StringValue +from ...base import ( + DEFAULT_FORMAT_FUNC, + BytesValue, + DynamicTemplate, + FileMixin, + StringValue, +) from ...constants import DEFAULT_IMAGE_MAX_NB_CHARS from ...helpers import load_class_from_path from ...registry import FILE_REGISTRY @@ -23,9 +29,6 @@ "WEASYPRINT_IMAGE_GENERATOR", ) -DEFAULT_IMAGE_GENERATOR = ( - "faker_file.providers.image.imgkit_generator.ImgkitImageGenerator" -) IMAGEKIT_IMAGE_GENERATOR = ( "faker_file.providers.image.imgkit_generator.ImgkitImageGenerator" ) @@ -36,6 +39,8 @@ "faker_file.providers.image.weasyprint_generator.WeasyPrintImageGenerator" ) +DEFAULT_IMAGE_GENERATOR = IMAGEKIT_IMAGE_GENERATOR + class ImageMixin(FileMixin): """Image mixin.""" @@ -127,15 +132,6 @@ def _image_file( basename=basename, ) - content = self._generate_text_content( - max_nb_chars=max_nb_chars, - wrap_chars_after=wrap_chars_after, - content=content, - format_func=format_func, - ) - - data = {"content": content, "filename": filename, "storage": storage} - if image_generator_cls is None: image_generator_cls = DEFAULT_IMAGE_GENERATOR @@ -144,13 +140,30 @@ def _image_file( if not image_generator_kwargs: image_generator_kwargs = {} + + image_generator_kwargs["content_specs"] = { + "max_nb_chars": max_nb_chars, + "wrap_chars_after": wrap_chars_after, + } + image_generator = image_generator_cls( generator=self.generator, **image_generator_kwargs, ) + data = {"content": "", "filename": filename, "storage": storage} + if isinstance(content, DynamicTemplate): + _content = content + else: + _content = self._generate_text_content( + max_nb_chars=max_nb_chars, + wrap_chars_after=wrap_chars_after, + content=content, + format_func=format_func, + ) + data["content"] = _content _raw_content = image_generator.generate( - content=content, + content=_content, data=data, provider=self, ) diff --git a/src/faker_file/providers/pdf_file/__init__.py b/src/faker_file/providers/pdf_file/__init__.py index aa66695a..eed8767b 100644 --- a/src/faker_file/providers/pdf_file/__init__.py +++ b/src/faker_file/providers/pdf_file/__init__.py @@ -49,11 +49,11 @@ PIL_PDF_GENERATOR = ( "faker_file.providers.pdf_file.generators.pil_generator.PilPdfGenerator" ) -DEFAULT_PDF_GENERATOR = PDFKIT_PDF_GENERATOR REPORTLAB_PDF_GENERATOR = ( "faker_file.providers.pdf_file.generators.reportlab_generator" ".ReportlabPdfGenerator" ) +DEFAULT_PDF_GENERATOR = PDFKIT_PDF_GENERATOR class PdfFileProvider(BaseProvider, FileMixin): diff --git a/src/faker_file/providers/pdf_file/generators/pdfkit_generator.py b/src/faker_file/providers/pdf_file/generators/pdfkit_generator.py index 177f2b7d..070b2df1 100644 --- a/src/faker_file/providers/pdf_file/generators/pdfkit_generator.py +++ b/src/faker_file/providers/pdf_file/generators/pdfkit_generator.py @@ -27,14 +27,14 @@ class PdfkitPdfGenerator(BasePdfGenerator): from faker import Faker from faker_file.providers.pdf_file import PdfFileProvider - from faker_file.providers.pdf_file.generators import pdfkit_generator + from faker_file.providers.pdf_file.generators.pdfkit_generator import ( + PdfkitPdfGenerator + ) FAKER = Faker() FAKER.add_provider(PdfFileProvider) - file = FAKER.pdf_file( - pdf_generator_cls=pdfkit_generator.PdfkitPdfGenerator - ) + file = FAKER.pdf_file(pdf_generator_cls=PdfkitPdfGenerator) Using `DynamicTemplate`: @@ -57,7 +57,7 @@ class PdfkitPdfGenerator(BasePdfGenerator): # Create a file with lots of elements file = FAKER.pdf_file( - pdf_generator_cls=pdfkit_generator.PdfkitPdfGenerator, + pdf_generator_cls=PdfkitPdfGenerator, content=DynamicTemplate( [ (add_h1_heading, {}), diff --git a/src/faker_file/providers/pdf_file/generators/pil_generator.py b/src/faker_file/providers/pdf_file/generators/pil_generator.py index 32f94875..3f73d354 100644 --- a/src/faker_file/providers/pdf_file/generators/pil_generator.py +++ b/src/faker_file/providers/pdf_file/generators/pil_generator.py @@ -190,7 +190,7 @@ def generate( y_text = 0 for counter, line in enumerate(lines): text_width, text_height = self.draw.textsize( - line, font=font, spacing=6 + line, font=font, spacing=self.spacing ) # if counter % max_lines_per_page == 0: if y_text + text_height > self.page_height: diff --git a/src/faker_file/tests/_conftest.py b/src/faker_file/tests/_conftest.py index ec3643c9..a3ce3c69 100644 --- a/src/faker_file/tests/_conftest.py +++ b/src/faker_file/tests/_conftest.py @@ -4,6 +4,8 @@ running documentation tests. Therefore, this hook, which simply calls the `clean_up` method of the `FILE_REGISTRY` instance. """ +import pytest + from faker_file.registry import FILE_REGISTRY __author__ = "Artur Barseghyan " @@ -15,6 +17,19 @@ ) +def pytest_collection_modifyitems(config, items): + """Modify test items during collection.""" + for item in items: + try: + from pytest_rst import RSTTestItem + + if isinstance(item, RSTTestItem): + # Dynamically add marker to RSTTestItem tests + item.add_marker(pytest.mark.documentation) + except ImportError: + pass + + def pytest_runtest_setup(item): """Setup before test runs.""" try: diff --git a/src/faker_file/tests/test_cli.py b/src/faker_file/tests/test_cli.py index 1f0a8565..fb91a7ff 100644 --- a/src/faker_file/tests/test_cli.py +++ b/src/faker_file/tests/test_cli.py @@ -259,13 +259,13 @@ def test_cli(self: "TestCLI", method_name: str, kwargs: dict) -> None: # Merge the base command with the generated arguments cmd = ["faker-file", method_name] + args - LOGGER.error(f"cmd: {cmd}") + # LOGGER.debug(f"cmd: {cmd}") # Execute the command with the provided arguments res = subprocess.check_output(cmd).strip() # Extract the filename to verify existence and clean-up filename = extract_filename(res.decode()) - LOGGER.error(f"filename: {filename}") + # LOGGER.debug(f"filename: {filename}") self.assertTrue(filename) self.assertTrue(FS_STORAGE.exists(filename)) FS_STORAGE.unlink(filename) diff --git a/src/faker_file/tests/test_providers.py b/src/faker_file/tests/test_providers.py index 13745da1..87189a7e 100644 --- a/src/faker_file/tests/test_providers.py +++ b/src/faker_file/tests/test_providers.py @@ -35,6 +35,43 @@ add_table as docx_add_table, add_title_heading as docx_add_title_heading, ) +from ..contrib.image.imgkit_snippets import ( + add_h1_heading as image_imgkit_add_h1_heading, + add_h2_heading as image_imgkit_add_h2_heading, + add_h3_heading as image_imgkit_add_h3_heading, + add_h4_heading as image_imgkit_add_h4_heading, + add_h5_heading as image_imgkit_add_h5_heading, + add_h6_heading as image_imgkit_add_h6_heading, + add_page_break as image_imgkit_add_page_break, + add_paragraph as image_imgkit_add_paragraph, + add_picture as image_imgkit_add_picture, + add_table as image_imgkit_add_table, +) +from ..contrib.image.pil_snippets import ( + add_h1_heading as image_pil_add_h1_heading, + add_h2_heading as image_pil_add_h2_heading, + add_h3_heading as image_pil_add_h3_heading, + add_h4_heading as image_pil_add_h4_heading, + add_h5_heading as image_pil_add_h5_heading, + add_h6_heading as image_pil_add_h6_heading, + add_page_break as image_pil_add_page_break, + add_paragraph as image_pil_add_paragraph, + add_picture as image_pil_add_picture, + add_table as image_pil_add_table, +) +from ..contrib.image.weasyprint_snippets import ( + add_h1_heading as image_weasyprint_add_h1_heading, + add_h2_heading as image_weasyprint_add_h2_heading, + add_h3_heading as image_weasyprint_add_h3_heading, + add_h4_heading as image_weasyprint_add_h4_heading, + add_h5_heading as image_weasyprint_add_h5_heading, + add_h6_heading as image_weasyprint_add_h6_heading, + add_heading as image_weasyprint_add_heading, + add_page_break as image_weasyprint_add_page_break, + add_paragraph as image_weasyprint_add_paragraph, + add_picture as image_weasyprint_add_picture, + add_table as image_weasyprint_add_table, +) from ..contrib.odt_file import ( add_h1_heading as odt_add_h1_heading, add_h2_heading as odt_add_h2_heading, @@ -280,6 +317,9 @@ pdf_reportlab_add_non_existing_heading = partial( pdf_reportlab_add_heading, level=0 ) +image_weasyprint_add_non_existing_heading = partial( + image_weasyprint_add_heading, level=0 +) logging.getLogger("fontTools").setLevel(logging.WARNING) @@ -1244,6 +1284,104 @@ class ProvidersTestCase(unittest.TestCase): {"size": (100, 100), "image_generator_cls": None}, None, ), + ( + FAKER, + PngFileProvider, + "png_file", + { + "image_generator_cls": IMAGEKIT_IMAGE_GENERATOR, + "content": DynamicTemplate( + [ + (image_imgkit_add_h1_heading, {}), # Add h1 + (image_imgkit_add_h2_heading, {}), # Add h2 + (image_imgkit_add_h3_heading, {}), # Add h3 + (image_imgkit_add_h4_heading, {}), # Add h4 + (image_imgkit_add_h5_heading, {}), # Add h5 + (image_imgkit_add_h6_heading, {}), # Add h6 + (image_imgkit_add_paragraph, {}), # Add paragraph + (image_imgkit_add_picture, {}), # Add picture + (image_imgkit_add_table, {}), # Add table + (image_imgkit_add_page_break, {}), # Add page break + ] + ), + }, + None, + ), + ( + FAKER, + PngFileProvider, + "png_file", + { + "image_generator_cls": PIL_IMAGE_GENERATOR, + "image_generator_kwargs": { + "image_mode": "RGB", + }, + "content": DynamicTemplate( + [ + (image_pil_add_h1_heading, {}), # Add h1 + (image_pil_add_h2_heading, {}), # Add h2 + (image_pil_add_h3_heading, {}), # Add h3 + (image_pil_add_h4_heading, {}), # Add h4 + (image_pil_add_h5_heading, {}), # Add h5 + (image_pil_add_h6_heading, {}), # Add h6 + (image_pil_add_paragraph, {}), # Add paragraph + (image_pil_add_picture, {}), # Add picture + (image_pil_add_table, {}), # Add table + (image_pil_add_page_break, {}), # Add page break + ] + ), + }, + None, + ), + # Testing one page + ( + FAKER, + PngFileProvider, + "png_file", + { + "image_generator_cls": WEASYPRINT_IMAGE_GENERATOR, + "content": DynamicTemplate( + [ + (image_weasyprint_add_h1_heading, {}), # Add h1 + ] + ), + }, + None, + ), + ( + FAKER, + PngFileProvider, + "png_file", + { + "image_generator_cls": WEASYPRINT_IMAGE_GENERATOR, + "image_generator_kwargs": { + "page_width": 800, + "page_height": 1200, + "image_mode": "RGB", + "wrapper_tag": "div", + }, + "content": DynamicTemplate( + [ + (image_weasyprint_add_h1_heading, {}), # Add h1 + (image_weasyprint_add_h2_heading, {}), # Add h2 + (image_weasyprint_add_h3_heading, {}), # Add h3 + (image_weasyprint_add_h4_heading, {}), # Add h4 + (image_weasyprint_add_h5_heading, {}), # Add h5 + (image_weasyprint_add_h6_heading, {}), # Add h6 + (image_weasyprint_add_non_existing_heading, {}), + (image_weasyprint_add_paragraph, {}), # Add paragraph + ( + image_weasyprint_add_paragraph, + {"max_nb_chars": 1_000}, + ), # Add paragraph + (image_weasyprint_add_picture, {}), # Add picture + (image_weasyprint_add_table, {}), # Add table + (image_weasyprint_add_page_break, {}), # Add page break + ] + ), + }, + None, + ), # PPTX (FAKER, PptxFileProvider, "pptx_file", {}, None), (FAKER_HY, PptxFileProvider, "pptx_file", {}, None), diff --git a/src/faker_file/tests/test_sftp_server.py b/src/faker_file/tests/test_sftp_server.py index a417b3b1..91992e9c 100644 --- a/src/faker_file/tests/test_sftp_server.py +++ b/src/faker_file/tests/test_sftp_server.py @@ -11,6 +11,7 @@ from faker import Faker from ..providers.txt_file import TxtFileProvider +from ..registry import FILE_REGISTRY from .sftp_server import SFTPServerManager, start_server, start_server_async from .utils import AutoFreePortInt @@ -106,6 +107,7 @@ async def test_file_upload(self: "__TestSFTPServerMixin") -> None: uploaded_contents = await uploaded_file.read() self.assertEqual(test_file.data["content"], uploaded_contents) + FILE_REGISTRY.clean_up() async def test_file_delete(self: "__TestSFTPServerMixin") -> None: async with asyncssh.connect( @@ -127,6 +129,7 @@ async def test_file_delete(self: "__TestSFTPServerMixin") -> None: # Delete the file and ensure it's gone await sftp.remove("/testfile_delete.txt") self.assertFalse(await sftp.exists("/testfile_delete.txt")) + FILE_REGISTRY.clean_up() class TestSFTPServerWithStartServerAsync(