From 4a6a60ad5b00cb6c0d3a466b1f537ed93d573fa2 Mon Sep 17 00:00:00 2001 From: Artur Barseghyan Date: Sun, 13 Oct 2024 22:42:58 +0200 Subject: [PATCH 1/3] Enable workflow trigger (#208) --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e74374f..ed113d9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,6 +4,7 @@ on: push: pull_request: types: [review_requested, ready_for_review] + workflow_dispatch: jobs: # ************************************* From 64988c15f5901156e592aff036d9323b309149fc Mon Sep 17 00:00:00 2001 From: Artur Barseghyan Date: Wed, 16 Oct 2024 23:43:57 +0200 Subject: [PATCH 2/3] Add JPG support (#210) --- CHANGELOG.rst | 6 + Makefile | 2 +- README.rst | 6 +- docs/creating_images.rst | 5 +- fake.py | 362 ++++++++++++++++++++++++++++++++++++++- pyproject.toml | 2 +- 6 files changed, 373 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bd1e942..731f65a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,12 @@ are used for versioning (schema follows below): 0.3.4 to 0.4). - All backwards incompatible changes are mentioned in this document. +0.10.3 +------ +2024-10-16 + +- Add `JPG` file support through ``jpg`` and ``jpg_file`` providers. + 0.10.2 ------ 2024-10-07 diff --git a/Makefile b/Makefile index 64b5d8f..e6d6771 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # Update version ONLY here -VERSION := 0.10.2 +VERSION := 0.10.3 SHELL := /bin/bash # Makefile for project VENV := ~/.virtualenvs/fake.py/bin/activate diff --git a/README.rst b/README.rst index c1cec20..270a45d 100644 --- a/README.rst +++ b/README.rst @@ -75,7 +75,7 @@ types (such as `uuid`, `str`, `int`, `float`, `bool`), GEO data such as city, country, geo-location, country code, latitude, longitude and locales, IBANs and ISBNs, as well as byte content for multiple file formats including `PDF`, `DOCX`, `ODT`, `PNG`, `SVG`, `BMP`, `GIF`, `TIF`, `PPM`, -`WAV`, `ZIP`, `TAR` and `EML`. +`JPG`, `WAV`, `ZIP`, `TAR` and `EML`. The package also supports file creation on the filesystem and includes factories (dynamic fixtures) compatible with `Django`_, `TortoiseORM`_, @@ -86,8 +86,8 @@ Features - Generation of random texts, (person) names, emails, URLs, dates, IPs, and primitive Python data types. - Support for various file formats (`PDF`, `DOCX`, `ODT`, `TXT`, `PNG`, `SVG`, - `BMP`, `GIF`, `TIF`, `PPM`, `WAV`, `ZIP`, `TAR`, `EML`) and file creation - on the filesystem. + `BMP`, `GIF`, `TIF`, `PPM`, `JPG`, `WAV`, `ZIP`, `TAR`, `EML`) and file + creation on the filesystem. - Basic factories for integration with `Django`_, `Pydantic`_, `TortoiseORM`_ and `SQLAlchemy`_. - `CLI`_ for generating data from command line. diff --git a/docs/creating_images.rst b/docs/creating_images.rst index d8123e5..e49cb33 100644 --- a/docs/creating_images.rst +++ b/docs/creating_images.rst @@ -21,6 +21,7 @@ Currently, 6 image formats are supported: - ``GIF`` - ``TIF`` - ``PPM`` +- ``JPG`` Generating images as bytes -------------------------- @@ -81,7 +82,7 @@ With ``size`` and ``color`` tweaks: ---- -All other formats (``SVG``, ``BMP``, ``GIF``, ``TIF`` and ``PPM``) work in -exact same way. +All other formats (``SVG``, ``BMP``, ``GIF``, ``TIF``, ``PPM`` and ``JPG``) +work in exact same way. ---- diff --git a/fake.py b/fake.py index db4d423..d17b9cc 100644 --- a/fake.py +++ b/fake.py @@ -16,6 +16,7 @@ import re import secrets import string +import struct import subprocess import tarfile import unittest @@ -66,7 +67,7 @@ from uuid import UUID __title__ = "fake.py" -__version__ = "0.10.2" +__version__ = "0.10.3" __author__ = "Artur Barseghyan " __copyright__ = "2023-2024 Artur Barseghyan" __license__ = "MIT" @@ -102,13 +103,31 @@ "SubFactory", "TextPdfGenerator", "TortoiseModelFactory", + "create_inner_bmp_file", + "create_inner_docx_file", + "create_inner_eml_file", + "create_inner_gif_file", + "create_inner_jpg_file", + "create_inner_odt_file", + "create_inner_pdf_file", + "create_inner_png_file", + "create_inner_ppm_file", + "create_inner_svg_file", + "create_inner_tar_file", + "create_inner_text_pdf_file", + "create_inner_tif_file", + "create_inner_txt_file", + "create_inner_wav_file", + "create_inner_zip_file", "fill_dataclass", "fill_pydantic_model", "format_type_hint", + "fuzzy_choice_create_inner_file", "get_argparse_type", "get_provider_args", "get_provider_defaults", "is_optional_type", + "list_create_inner_file", "main", "organize_providers", "post_save", @@ -1359,6 +1378,258 @@ def create( return odt_bytes.getvalue() +class JpgGenerator: + GRAY_1_PX_JPG = ( + b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00" + b"\xff\xdb\x00C\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xc0\x00\x0b\x08\x00\x01\x00\x01\x01\x01\x11\x00\xff\xc4\x00\x14" + b"\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\xff\xda\x00\x08\x01\x01\x00\x00?\x00?\xff\xd9" + ) + GRAY_1_PX_JPG_BYTEARRAY = bytearray(GRAY_1_PX_JPG) + + RED_1_PX_JPG = ( + b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00" + b"\xff\xdb\x00C\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xdb\x00C\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b'\xff\xc0\x00\x11\x08\x00\x01\x00\x01\x03\x01"\x00\x02\x11\x01\x03\x11' + b"\x01\xff\xc4\x00\x15\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x02\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xc4\x00\x15\x01" + b"\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01" + b"\x03\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11" + b"\x00?\x00\x90\x02\x8f\xff\xd9" + ) + RED_1_PX_JPG_BYTEARRAY = bytearray(RED_1_PX_JPG) + + YELLOW_1_PX_JPG = ( + b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00" + b"\xff\xdb\x00C\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xdb\x00C\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b'\xff\xc0\x00\x11\x08\x00\x01\x00\x01\x03\x01"\x00\x02\x11\x01\x03\x11' + b"\x01\xff\xc4\x00\x15\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x02\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xc4\x00\x15\x01" + b"\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01" + b"\x03\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11" + b"\x00?\x00\xb0\x13/\xff\xd9" + ) + YELLOW_1_PX_JPG_BYTEARRAY = bytearray(YELLOW_1_PX_JPG) + + BLUE_1_PX_JPG = ( + b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00" + b"\xff\xdb\x00C\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xdb\x00C\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b'\xff\xc0\x00\x11\x08\x00\x01\x00\x01\x03\x01"\x00\x02\x11\x01\x03\x11' + b"\x01\xff\xc4\x00\x15\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x02\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xc4\x00\x15\x01" + b"\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01" + b"\x03\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11" + b"\x00?\x00\x80\x14\x0f\xff\xd9" + ) + BLUE_1_PX_JPG_BYTEARRAY = bytearray(BLUE_1_PX_JPG) + + GREEN_1_PX_JPG = ( + b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00" + b"\xff\xdb\x00C\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xdb\x00C\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b'\xff\xc0\x00\x11\x08\x00\x01\x00\x01\x03\x01"\x00\x02\x11\x01\x03\x11' + b"\x01\xff\xc4\x00\x15\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x01\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xc4\x00\x14\x01" + b"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02" + b"\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00?" + b"\x00\xa0\x00?\xff\xd9" + ) + GREEN_1_PX_JPG_BYTEARRAY = bytearray(GREEN_1_PX_JPG) + + BLACK_1_PX_JPG = ( + b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00" + b"\xff\xdb\x00C\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xdb\x00C\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b'\xff\xc0\x00\x11\x08\x00\x01\x00\x01\x03\x01"\x00\x02\x11\x01\x03\x11' + b"\x01\xff\xc4\x00\x15\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x03\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xc4\x00\x14\x01" + b"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00?" + b"\x00\x98\x00\xff\xd9" + ) + BLACK_1_PX_JPG_BYTEARRAY = bytearray(BLACK_1_PX_JPG) + + COLORS = { + (0, 255, 0): GREEN_1_PX_JPG_BYTEARRAY, + (255, 255, 0): YELLOW_1_PX_JPG_BYTEARRAY, + (255, 0, 0): RED_1_PX_JPG_BYTEARRAY, + (0, 0, 255): BLUE_1_PX_JPG_BYTEARRAY, + (0, 0, 0): BLACK_1_PX_JPG_BYTEARRAY, + (128, 128, 128): GRAY_1_PX_JPG_BYTEARRAY, + } + + @classmethod + def euclidean_distance(cls, color1, color2): + return math.sqrt(sum((a - b) ** 2 for a, b in zip(color1, color2))) + + @classmethod + def detect_closest_color(cls, color: Tuple[int, int, int]) -> bytearray: + closest = None + min_distance = float("inf") + + for ref_color, color_name in cls.COLORS.items(): + distance = cls.euclidean_distance(color, ref_color) + if distance < min_distance: + min_distance = distance + closest = color_name + + return closest + + @classmethod + def find_marker(cls, jpeg_bytes, marker) -> int: + """Helper function to find a marker.""" + marker_bytes = marker + index = jpeg_bytes.find(marker_bytes) + if index == -1: + raise ValueError(f"Marker {marker} not found.") + return index + + @classmethod + def generate( + cls, + size: Tuple[int, int] = (100, 100), + color: Tuple[int, int, int] = (128, 128, 128), + ) -> bytes: + """Create a JPG image of a specified size and color. + + :param size: Tuple of width and height of the image in pixels. + :param color: Color of the image in RGB format (tuple of three + integers). + :return: Byte content of the JPG image. + :rtype: bytes + """ + original = cls.detect_closest_color(color) + width, height = size + jpeg = original.copy() + + # Step 1: Update the SOF0 marker with new dimensions + SOF0 = b"\xFF\xC0" + sof0_index = cls.find_marker(jpeg, SOF0) + + # SOF0 structure: + # [Marker] + # [Length] + # [Precision] + # [Height] + # [Width] + # [Components] + # [Component Spec...] + # Length is 2 bytes, Precision is 1 byte, Height and Width are 2 bytes + # each. + # Components: 1 byte ID, 1 byte Sampling factors, 1 byte Quantization + # table number. + + # Extract current height and width (for 1x1 image) + # Height is at sof0_index + 5 and sof0_index + 6 + # Width is at sof0_index + 7 and sof0_index + 8 + # We need to modify these to new_height and new_width + + # Pack new height and width as big-endian unsigned shorts + new_height_bytes = struct.pack(">H", height) + new_width_bytes = struct.pack(">H", width) + + # Replace height and width in the JPEG bytes + jpeg[sof0_index + 5] = new_height_bytes[0] + jpeg[sof0_index + 6] = new_height_bytes[1] + jpeg[sof0_index + 7] = new_width_bytes[0] + jpeg[sof0_index + 8] = new_width_bytes[1] + + # Step 2: Locate the Start of Scan (SOS) marker + SOS = b"\xFF\xDA" + sos_index = cls.find_marker(jpeg, SOS) + + # SOS structure: + # [Marker] + # [Length] + # [Components] + # [Component Spec...] + # [Start & End of Spectral Selection] + # [Successive Approximation] + + # Extract the length of the SOS segment to find where image data starts + sos_length = struct.unpack(">H", jpeg[sos_index + 2 : sos_index + 4])[0] + image_data_start = sos_index + 2 + sos_length + + # Locate the End of Image (EOI) marker + EOI = b"\xFF\xD9" + eoi_index = jpeg.find(EOI, image_data_start) + if eoi_index == -1: + eoi_index = 0 + # raise ValueError("EOI marker not found.") + + # Step 3: Extract the original image data (between SOS and EOI) + original_image_data = jpeg[image_data_start:eoi_index] + + # Step 4: Determine the number of 8x8 blocks needed + blocks_per_row = (width + 7) // 8 # Ceiling division + blocks_per_col = (height + 7) // 8 + total_blocks = blocks_per_row * blocks_per_col + + # Step 5: Replicate the image data for each block + # For a constant gray image, each block's data is identical + replicated_image_data = original_image_data * total_blocks + + # Optional: If the image size isn't a multiple of 8, padding might be + # necessary. + # However, JPEG decoders typically ignore the extra padding, so we can + # proceed. + + # Step 6: Assemble the new JPEG bytes + new_jpeg = ( + jpeg[:image_data_start] + replicated_image_data + jpeg[eoi_index:] + ) + + return bytes(new_jpeg) + + class ProviderRegistryItem(str): __slots__ = ("tags",) @@ -2372,6 +2643,22 @@ def ppm( # Complete PPM file return ppm_header + bytes(image_data) + @provider(tags=("Image",)) + def jpg( + self, + size: Tuple[int, int] = (100, 100), + color: Tuple[int, int, int] = (128, 128, 128), + ) -> bytes: + """Create a JPG image of a specified size and color. + + :param size: Tuple of width and height of the image in pixels. + :param color: Color of the image in RGB format, tuple of three + integers: (0-255, 0-255, 0-255). + :return: Byte content of the JPG image. + :rtype: bytes + """ + return JpgGenerator.generate(size=size, color=color) + @provider(tags=("Image",)) def image( self, @@ -2382,12 +2669,21 @@ def image( "gif", "tif", "ppm", + "jpg", ] = "png", size: Tuple[int, int] = (100, 100), color: Tuple[int, int, int] = (0, 0, 255), ) -> bytes: """Create an image of a specified format, size and color.""" - if image_format not in {"png", "svg", "bmp", "gif", "tif", "ppm"}: + if image_format not in { + "png", + "svg", + "bmp", + "gif", + "tif", + "ppm", + "jpg", + }: raise ValueError() image_func = getattr(self, image_format) return image_func(size=size, color=color) @@ -2866,6 +3162,7 @@ def _image_file( "gif", "tif", "ppm", + "jpg", ] = "png", size: Tuple[int, int] = (100, 100), color: Tuple[int, int, int] = (0, 0, 255), @@ -3046,6 +3343,32 @@ def ppm_file( prefix=prefix, ) + @provider( + tags=( + "Image", + "File", + ) + ) + def jpg_file( + self, + size: Tuple[int, int] = (100, 100), + color: Tuple[int, int, int] = (128, 128, 128), + storage: Optional[BaseStorage] = None, + basename: Optional[str] = None, + prefix: Optional[str] = None, + extension: Optional[str] = None, + ) -> StringValue: + """Create a JPG image file of a specified size and color.""" + return self._image_file( + image_format="jpg", + size=size, + color=color, + extension=extension, + storage=storage, + basename=basename, + prefix=prefix, + ) + @provider( tags=( "Audio", @@ -3721,6 +4044,25 @@ def create_inner_ppm_file( ) +def create_inner_jpg_file( + storage: Optional[BaseStorage] = None, + basename: Optional[str] = None, + prefix: Optional[str] = None, + size: Tuple[int, int] = (100, 100), + color: Tuple[int, int, int] = (128, 128, 128), + **kwargs, +) -> StringValue: + """Create inner JPG file.""" + return FAKER.jpg_file( + storage=storage, + basename=basename, + prefix=prefix, + size=size, + color=color, + **kwargs, + ) + + def create_inner_wav_file( storage: Optional[BaseStorage] = None, basename: Optional[str] = None, @@ -5979,8 +6321,13 @@ def test_ppm(self) -> None: self.assertTrue(ppm) self.assertIsInstance(ppm, bytes) + def test_jpg(self) -> None: + jpg = self.faker.jpg() + self.assertTrue(jpg) + self.assertIsInstance(jpg, bytes) + def test_image(self): - for image_format in {"png", "svg", "bmp", "gif", "tif", "ppm"}: + for image_format in {"png", "svg", "bmp", "gif", "tif", "ppm", "jpg"}: with self.subTest(image_format=image_format): image = self.faker.image( image_format=image_format, @@ -6081,6 +6428,10 @@ def test_ppm_file(self) -> None: file = self.faker.ppm_file() self.assertTrue(os.path.exists(file.data["filename"])) + def test_jpg_file(self) -> None: + file = self.faker.jpg_file() + self.assertTrue(os.path.exists(file.data["filename"])) + def test_wav_file(self) -> None: file = self.faker.wav_file() self.assertTrue(os.path.exists(file.data["filename"])) @@ -6173,6 +6524,11 @@ def test_create_inner_ppm_file(self): self.assertTrue(value) self.assertIsInstance(value, StringValue) + def test_create_inner_jpg_file(self): + value = create_inner_jpg_file() + self.assertTrue(value) + self.assertIsInstance(value, StringValue) + def test_create_inner_wav_file(self): value = create_inner_wav_file() self.assertTrue(value) diff --git a/pyproject.toml b/pyproject.toml index f724639..794c120 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "fake.py" description = "Minimalistic, standalone alternative fake data generator with no dependencies." readme = "README.rst" -version = "0.10.2" +version = "0.10.3" dependencies = [] authors = [ {name = "Artur Barseghyan", email = "artur.barseghyan@gmail.com"}, From e240181ba727e2de89cf242926b13230d4ac65bc Mon Sep 17 00:00:00 2001 From: Artur Barseghyan Date: Thu, 17 Oct 2024 21:03:59 +0200 Subject: [PATCH 3/3] Improve slugify function. Improve dovcs (#212) --- CHANGELOG.rst | 7 +++++++ README.rst | 2 ++ docs/creating_images.rst | 45 ++++++++++++++++++++++++++++++++++++++++ fake.py | 4 ++-- 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 731f65a..2214bde 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,13 @@ are used for versioning (schema follows below): 0.3.4 to 0.4). - All backwards incompatible changes are mentioned in this document. +0.10.4 +------ +2024-10-17 + +- Add a ``separator`` argument to the ``slugify`` function. +- Minor documentation improvements. + 0.10.3 ------ 2024-10-16 diff --git a/README.rst b/README.rst index 270a45d..442837f 100644 --- a/README.rst +++ b/README.rst @@ -267,6 +267,7 @@ As bytes FAKER.docx() # bytes FAKER.eml() # bytes FAKER.gif() # bytes + FAKER.jpg() # bytes FAKER.odt() # bytes FAKER.pdf() # bytes FAKER.png() # bytes @@ -288,6 +289,7 @@ As files on the file system FAKER.docx_file() # str FAKER.eml_file() # str FAKER.gif_file() # str + FAKER.jpg_file() # str FAKER.odt_file() # str FAKER.pdf_file() # str FAKER.png_file() # str diff --git a/docs/creating_images.rst b/docs/creating_images.rst index e49cb33..e9d00f4 100644 --- a/docs/creating_images.rst +++ b/docs/creating_images.rst @@ -85,4 +85,49 @@ With ``size`` and ``color`` tweaks: All other formats (``SVG``, ``BMP``, ``GIF``, ``TIF``, ``PPM`` and ``JPG``) work in exact same way. +The only format that slightly deviates from others is ``JPG``. Produced +``JPG`` images are still rectangles, but unlike all others, instead of being +filled with a single solid colour, they are filled with a mixture of colours, +around a picked base colour. Also, colours on the ``JPG`` image are not +precise, but a closest match to the colour given. + +The following code will generate a 10x10 px square filled with solid yellow +colour. + +.. container:: jsphinx-toggle-emphasis + + .. code-block:: python + :name: test_jpg_file_10x10_yellow + :emphasize-lines: 3 + + from fake import FAKER + + FAKER.jpg_file(size=(10, 10), color=(182, 232, 90)) + +While the following code, will generate a 640x480 px square filled with yellow +and other colours. + +.. container:: jsphinx-toggle-emphasis + + .. code-block:: python + :name: test_jpg_file_640x480_yellow_mix + :emphasize-lines: 3 + + from fake import FAKER + + FAKER.jpg_file(size=(640, 480), color=(18, 52, 185)) + +The only colour that always stays solid is the default colour - gray +``(128, 128, 128)``. + +.. container:: jsphinx-toggle-emphasis + + .. code-block:: python + :name: test_jpg_file_300x200_solid_gray + :emphasize-lines: 3 + + from fake import FAKER + + FAKER.jpg_file(size=(720, 540)) + ---- diff --git a/fake.py b/fake.py index d17b9cc..b5d74b5 100644 --- a/fake.py +++ b/fake.py @@ -316,9 +316,9 @@ SLUGIFY_RE = re.compile(r"[^a-zA-Z0-9]") -def slugify(value: str) -> str: +def slugify(value: str, separator: str = "") -> str: """Slugify.""" - return SLUGIFY_RE.sub("", value).lower() + return SLUGIFY_RE.sub(separator, value).lower() class MetaData: