diff --git a/.github/.archive/converter.py b/.github/.archive/converter.py deleted file mode 100644 index 750ff69..0000000 --- a/.github/.archive/converter.py +++ /dev/null @@ -1,149 +0,0 @@ -import os -import logging -import click -import mimetypes -from PIL import Image -import json -import csv -import odf.opendocument -import xml.etree.ElementTree as ET - - -logging.basicConfig(filename='file.log', level=logging.INFO, format='%(asctime)s:%(levelname)s:%(message)s') - - -class FileConverter: - def __init__(self, input_path: str, output_path: str): - """ - Initializes a new instance of the FileConverter class. - - Args: - input_path (str): Path to the input file. - output_path (str): Path to save the converted file. - """ - self.input_path = input_path - self.output_path = output_path - - def convert(self) -> None: - """ - Converts a file to the desired format. - - Raises: - NotImplementedError: If the method is not implemented in the derived class. - ValueError: If the input file format is not supported. - """ - raise NotImplementedError - - @staticmethod - def supported_conversions() -> str: - """ - Returns a string with the supported file formats and conversions. - - Returns: - str: Supported file formats and conversions. - """ - return "Supported file formats and conversions:\n" \ - "JSON: can be converted to CSV or JSON\n" \ - "CSV: can be converted to JSON or CSV (no conversion needed)\n" \ - "ODT: can be converted to plain text\n" \ - "XML: can be converted to JSON" - - -class ImageConverter(FileConverter): - def convert(self) -> None: - """ - Converts an image to the desired format. - - Raises: - ValueError: If the input file format is not supported. - """ - try: - img = Image.open(self.input_path) - img.save(self.output_path) - except Exception as e: - logging.exception(f"Failed to convert image. Input: {self.input_path}, Output: {self.output_path}. Error: {str(e)}") - raise ValueError("Unsupported file format.") - - -class TextConverter(FileConverter): - def convert(self) -> None: - """ - Converts a text document to the desired format. - - Raises: - ValueError: If the input file format is not supported or the conversion is not possible. - """ - input_ext = os.path.splitext(self.input_path)[1].lower() - output_ext = os.path.splitext(self.output_path)[1].lower() - - if input_ext == '.json' and output_ext == '.csv': - with open(self.input_path, 'r') as f: - data = json.load(f) - with open(self.output_path, 'w', newline='') as f: - writer = csv.writer(f) - writer.writerow(data[0].keys()) - for row in data: - writer.writerow(row.values()) - elif input_ext == '.csv' and output_ext == '.json': - with open(self.input_path, 'r') as f: - reader = csv.DictReader(f) - data = [row for row in reader] - with open(self.output_path, 'w') as f: - json.dump(data, f, indent=4) - elif input_ext == '.odt' and output_ext == '.txt': - doc = odf.opendocument.load(self.input_path) - with open(self.output_path, 'w') as f: - f.write(doc.text().replace('\n', ' ')) - elif input_ext == '.xml' and output_ext == '.json': - tree = ET.parse(self.input_path) - root = tree.getroot() - data = [] - for child in root: - data.append(child.attrib) - with open(self.output_path, 'w') as f: - json.dump(data, f, indent=4) - else: - raise ValueError(f"Unsupported file format or conversion. Input: {input_ext}, Output: {output_ext}. " - f"{FileConverter.supported_conversions()}") - - if not os.path.exists(self.output_path): - raise ValueError(f"Conversion failed. Input: {self.input_path}, Output: {self.output_path}. " - f"{FileConverter.supported_conversions()}") - - -@click.command() -@click.argument('input_path', type=click.Path(exists=True)) -@click.argument('output_path', type=click.Path()) -def convert_file(input_path: str, output_path: str) -> None: - """ - Converts a file to the desired format. - - Args: - input_path (str): Path to the input file. - output_path (str): Path to save the converted file. - - Raises: - ValueError: If the input and output file formats are the same or the input file format is not supported. - """ - input_ext = os.path.splitext(input_path)[1].lower() - output_ext = os.path.splitext(output_path)[1].lower() - - if input_ext == output_ext: - raise ValueError("Input and output file formats cannot be the same.") - - file_type, _ = mimetypes.guess_type(input_path) - if file_type is None: - raise ValueError("Unsupported file format.") - - if file_type.startswith('image'): - converter = ImageConverter(input_path, output_path) - elif file_type.startswith('text'): - converter = TextConverter(input_path, output_path) - else: - raise ValueError("Unsupported file format.") - - converter.convert() - - -if __name__ == "__main__": - convert_file() \ No newline at end of file diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml new file mode 100644 index 0000000..c6720bd --- /dev/null +++ b/.github/workflows/black.yml @@ -0,0 +1,16 @@ +name: Format with Black + +on: + pull_request: + branches: + - main + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: psf/black@stable + with: + options: "--color" + src: "." \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0ae652d..80e36d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ - +.pdm-python # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/data/CHAD.png b/data/CHAD.png deleted file mode 100644 index f222adc..0000000 Binary files a/data/CHAD.png and /dev/null differ diff --git a/data/Screen Time Data.csv b/data/Screen Time Data.csv deleted file mode 100644 index 37a6753..0000000 --- a/data/Screen Time Data.csv +++ /dev/null @@ -1,29 +0,0 @@ -index,Date,Week Day,Total Screen Time ,Social Networking,Reading and Reference,Other,Productivity,Health and Fitness,Entertainment,Creativity,Yoga -0,04/17/19,Wednesday,187,89,17,41,22,0,0,0,0 -1,04/18/19,Thursday,123,78,17,8,9,0,0,0,0 -2,04/19/19,Friday,112,52,40,8,4,0,3,0,0 -3,04/20/19,Saturday,101,69,9,38,2,0,3,0,0 -4,04/21/19,Sunday,56,35,2,43,3,0,1,1,0 -5,04/22/19,Monday,189,68,0,9,3,4,0,0,0 -6,04/23/19,Tuesday,158,56,18,41,12,15,0,0,0 -7,04/24/19,Wednesday,135,98,3,33,16,0,0,0,0 -8,04/25/19,Thursday,52,25,7,3,16,0,0,0,0 -9,04/26/19,Friday,198,76,8,29,15,0,32,0,0 -10,04/27/19,Saturday,116,75,10,20,5,0,0,0,0 -11,04/28/19,Sunday,85,42,22,4,2,0,0,0,0 -12,04/29/19,Monday,109,46,8,13,9,15,1,0,1 -13,04/30/19,Tuesday,79,40,2,9,12,0,0,0,1 -14,05/01/19,Wednesday,127,90,0,10,7,0,0,0,1 -15,05/02/19,Thursday,170,60,3,2,11,0,0,0,1 -16,05/03/19,Friday,91,64,2,18,5,1,1,2,1 -17,05/04/19,Saturday,58,34,4,5,3,0,1,0,1 -18,05/05/19,Sunday,133,109,5,1,3,0,0,0,1 -19,05/06/19,Monday,144,81,4,5,3,0,0,0,1 -20,05/07/19,Tuesday,110,70,5,6,15,0,9,0,1 -21,05/08/19,Wednesday,122,53,25,26,15,0,0,0,1 -22,05/09/19,Thursday,96,42,15,16,19,0,0,0,1 -23,05/10/19,Friday,161,93,13,17,16,1,0,0,1 -24,05/11/19,Saturday,58,49,1,2,2,0,0,2,1 -25,05/12/19,Sunday,52,28,1,1,6,0,0,1,1 -26,05/13/19,Monday,61,37,1,0,4,0,0,0,1 -27,05/14/19,Tuesday,88,41,2,7,15,0,0,0,1 diff --git a/data/looking respectfully.jpg b/data/looking respectfully.jpg deleted file mode 100644 index d7a9c73..0000000 Binary files a/data/looking respectfully.jpg and /dev/null differ diff --git a/data/peepoHehe.webp b/data/peepoHehe.webp deleted file mode 100644 index da3a01c..0000000 Binary files a/data/peepoHehe.webp and /dev/null differ diff --git a/main.py b/main.py index ec2c43c..9dd062c 100644 --- a/main.py +++ b/main.py @@ -1,167 +1,78 @@ +""" +Main module for image conversion. Handles both single image and directory processing. +""" + import os +import argparse from typing import List -from loguru import logger -from src.image_converter import convert_image -from src.text_converter import convert_text - - -def setup_logging() -> None: - """ - Sets up logging for the application. - """ - logger.add("converter.log", level="INFO", format="{time}:{level}: {message}") - - -setup_logging() - - -def print_menu() -> None: - """ - Prints the main menu for the file converter program. - """ - print("""\ -╔══════════════════════════════════════════════════════════╗ -║ File Converter ║ -╠══════════════════════════════════════════════════════════╣ -║ 1. Convert PNG to JPG. ║ -║ 2. Convert JPG to PNG. ║ -║ 3. Convert JSON to CSV. ║ -║ 4. Convert CSV to JSON. ║ -║ 5. Convert ODT to plain text. ║ -║ 6. Convert XML to JSON. ║ -║ ║ -║ Enter 'q' to quit. ║ -║ Note: You can enter either a specific file or a directory ║ -║ path when prompted. ║ -║ ║ -║ Hint: ║ -║ - To convert a single file, enter the full path to the ║ -║ file including the file name and extension. ║ -║ - To convert all files in a directory, enter the full path ║ -║ to the directory. ║ -║ - Converted files will be saved in the same directory as ║ -║ the original file with a new extension. ║ -╚══════════════════════════════════════════════════════════╝ - """) - - -def convert_files(paths: List[str], conversion_type: str) -> None: - """ - Converts multiple files of the same type in batch. +from formaverter.image_converter import convert_image +from formaverter.image_collector import collect_images - Args: - paths: A list of file paths to convert. - conversion_type: The type of conversion to perform. - """ - for path in paths: - if os.path.isfile(path): - new_filename = os.path.splitext(path)[0] + convert_extension(conversion_type) - if conversion_type in ["png_to_jpg", "jpg_to_png"]: - convert_image(path, new_filename, conversion_type) - else: - convert_text(path, new_filename) - logger.info(f"Converted {path} to {new_filename}.") - print(f"Conversion of {path} to {new_filename} successful.") - elif os.path.isdir(path): - for file in os.listdir(path): - if file.lower().endswith(convert_extension(conversion_type)): - input_path = os.path.join(path, file) - output_path = os.path.join(path, os.path.splitext(file)[0] + convert_extension(conversion_type)) - if conversion_type in ["png_to_jpg", "jpg_to_png"]: - convert_image(input_path, output_path, conversion_type) - else: - convert_text(input_path, output_path) - logger.info(f"{conversion_type.upper()} conversion in the specified directory is complete.") - print(f"{conversion_type.upper()} conversion in the specified directory is complete.") - else: - print(f"{path} is not a valid file or directory.") - -def convert_extension(conversion_type: str) -> str: +def process_images(input_path: str, output_path: str, output_format: str) -> List[str]: """ - Returns the file extension for the specified conversion type. + Process images based on whether the input path is a single image or a directory. Args: - conversion_type: The type of conversion. + input_path (str): Path to the input image file or directory. + output_path (str): Path to save the converted image file or directory. + output_format (str): Desired output format (e.g., 'jpg', 'png', 'bmp', 'webp'). Returns: - str: The file extension for the specified conversion type. - """ - if conversion_type == "png_to_jpg": - return ".jpg" - elif conversion_type == "jpg_to_png": - return ".png" - elif conversion_type == "json_to_csv": - return ".csv" - elif conversion_type == "csv_to_json": - return ".json" - elif conversion_type == "odt_to_txt": - return ".txt" - elif conversion_type == "xml_to_json": - return ".json" - else: - raise ValueError("Invalid conversion type!") + List[str]: A list of paths to the converted images. - -def main() -> None: - """ - Runs the file converter program. + Raises: + ValueError: If the input path does not exist. """ - print_menu() - choice = input("Enter the number of your choice: ") - - while choice != 'q': - try: - path = input("Enter the path of the file or directory to convert: ") - - if not os.path.exists(path): - print("Invalid path!") - continue - - # Convert relative path to absolute path - path = os.path.abspath(path) - - if choice == "1": - conversion_type = "png_to_jpg" - elif choice == "2": - conversion_type = "jpg_to_png" - elif choice == "3": - conversion_type = "json_to_csv" - elif choice == "4": - conversion_type = "csv_to_json" - elif choice == "5": - conversion_type = "odt_to_txt" - elif choice == "6": - conversion_type = "xml_to_json" - else: - print("Invalid choice!") - continue - - if os.path.isfile(path): - convert_files([path], conversion_type) - elif os.path.isdir(path): - paths = [os.path.join(path, file) for file in os.listdir(path)] - convert_files(paths, conversion_type) - else: - print(f"{path} is not a valid file or directory.") - - # Ask user if they want to continue - while True: - response = input("Do you want to continue? (y/n): ").lower() - if response in ["y", "yes"]: - break - elif response in ["n", "no", "q", "quit", "exit", "\x1b", "\x03"]: - choice = "q" - break - - except ValueError as e: - logger.error(f"An error occurred: {str(e)}") - print(f"An error occurred: {str(e)}") - - if choice != "q": - print_menu() - choice = input("Enter the number of your choice: ") + if not os.path.exists(input_path): + raise ValueError(f"Input path does not exist: {input_path}") + + converted_images = [] + + if os.path.isfile(input_path): + # Single image processing + output_filename = ( + f"{os.path.splitext(os.path.basename(input_path))[0]}.{output_format}" + ) + output_file_path = os.path.join(output_path, output_filename) + convert_image(input_path, output_file_path, output_format) + if os.path.exists(output_file_path): + converted_images.append(output_file_path) + elif os.path.isdir(input_path): + # Directory processing + os.makedirs(output_path, exist_ok=True) + image_files = collect_images(input_path) + + for file_path in image_files: + output_filename = ( + f"{os.path.splitext(os.path.basename(file_path))[0]}.{output_format}" + ) + output_file_path = os.path.join(output_path, output_filename) + try: + convert_image(file_path, output_file_path, output_format) + if os.path.exists(output_file_path): + converted_images.append(output_file_path) + except ValueError as e: + print(f"Error converting {file_path}: {str(e)}") + + return converted_images if __name__ == "__main__": - main() \ No newline at end of file + parser = argparse.ArgumentParser( + description="Convert images to a specified format." + ) + parser.add_argument("input_path", help="Path to input image or directory") + parser.add_argument("output_path", help="Path to output image or directory") + parser.add_argument( + "output_format", help="Desired output format (jpg, png, bmp, webp)" + ) + args = parser.parse_args() + + try: + converted_files = process_images( + args.input_path, args.output_path, args.output_format + ) + print(f"Successfully converted {len(converted_files)} images.") + except ValueError as e: + print(f"Error: {str(e)}") diff --git a/pdm.lock b/pdm.lock new file mode 100644 index 0000000..58801e7 --- /dev/null +++ b/pdm.lock @@ -0,0 +1,135 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "dev"] +strategy = ["inherit_metadata"] +lock_version = "4.5.0" +content_hash = "sha256:3fc38762c6fefc8350d8912cfc824dee10df493a505332a61f280ceaad41b4d7" + +[[metadata.targets]] +requires_python = "~=3.13" + +[[package]] +name = "black" +version = "24.10.0" +requires_python = ">=3.9" +summary = "The uncompromising code formatter." +groups = ["dev"] +dependencies = [ + "click>=8.0.0", + "mypy-extensions>=0.4.3", + "packaging>=22.0", + "pathspec>=0.9.0", + "platformdirs>=2", + "tomli>=1.1.0; python_version < \"3.11\"", + "typing-extensions>=4.0.1; python_version < \"3.11\"", +] +files = [ + {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, + {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, + {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, + {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, + {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, + {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, +] + +[[package]] +name = "click" +version = "8.1.7" +requires_python = ">=3.7" +summary = "Composable command line interface toolkit" +groups = ["dev"] +dependencies = [ + "colorama; platform_system == \"Windows\"", + "importlib-metadata; python_version < \"3.8\"", +] +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +groups = ["dev"] +marker = "platform_system == \"Windows\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +requires_python = ">=3.5" +summary = "Type system extensions for programs checked with the mypy type checker." +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "24.2" +requires_python = ">=3.8" +summary = "Core utilities for Python packages" +groups = ["dev"] +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +requires_python = ">=3.8" +summary = "Utility library for gitignore style pattern matching of file paths." +groups = ["dev"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "pillow" +version = "11.0.0" +requires_python = ">=3.9" +summary = "Python Imaging Library (Fork)" +groups = ["default"] +files = [ + {file = "pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699"}, + {file = "pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa"}, + {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f"}, + {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb"}, + {file = "pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798"}, + {file = "pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de"}, + {file = "pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84"}, + {file = "pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b"}, + {file = "pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003"}, + {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2"}, + {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a"}, + {file = "pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8"}, + {file = "pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8"}, + {file = "pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904"}, + {file = "pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3"}, + {file = "pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +requires_python = ">=3.8" +summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +groups = ["dev"] +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..52f6d9c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,28 @@ +[project] +name = "Formaverter" +version = "2.0.0" +description = "Converts images from one format to another using Pillow. Currently supports JPG, PNG, BMP, and WebP." +authors = [ + {name = "Daethyra", email = "109057945+Daethyra@users.noreply.github.com"}, +] +dependencies = [ + "pillow<12.0.0,>=11.0.0", +] +requires-python = "<4.0,>=3.13" +readme = "README.md" +license = {text = "GNU Aferro GPL"} + +[tool.pdm] +distribution = true +package-dir = "src" + +[tool.pdm.build] +includes = [] +[build-system] +requires = ["pdm-backend"] +build-backend = "pdm.backend" + +[dependency-groups] +dev = [ + "black>=24.10.0", +] diff --git a/readme.md b/readme.md index 38163e5..7b8c53d 100644 --- a/readme.md +++ b/readme.md @@ -1,72 +1,128 @@ +# Formaverter -# File Extension Converter +## Description +Formaverter is a Python tool that allows you to convert images between different formats using the Pillow library. It supports conversions between JPG, PNG, BMP, and WebP formats. -## Convert image types, text files, and structured data betweenst their respective formats. +## Features +- Convert images between JPG, PNG, BMP, and WebP formats +- Batch conversion of multiple images +- Simple command-line interface +- Skips conversion if the input and output formats are the same -File Extension Converter is a Python program that allows you to convert files of various types to other formats. It supports the following conversions: +## Installation +To install Formaverter, you need Python 3.13 or later. You can install it using pip: -* PNG to JPG -* JPG to PNG -* JSON to CSV -* CSV to JSON -* ODT to plain text -* XML to JSON +`pip install formaverter` -## Prerequisites: +## Usage -* Python 3.x -* Pillow -* Odfpy +### Using main.py directly as a CLI tool (Recommended) -## Installation: +If you've cloned the repository or downloaded the source code, you can use the `main.py` file directly: -Clone the repository to your local machine. +1. Navigate to the directory containing `main.py` +2. Run the following command: -Install the required dependencies by running + `python main.py ` -`pip install -r requirements.txt` + The arguments are the same as described above. -## How to Use: +Example: -1. Clone this repository or download the source code. -2. Navigate to the directory containing the `main.py` script in your terminal or command prompt. -3. To use the program, run python main.py from the command line. This will display the main menu -4. Enter the number of the conversion you want to perform, followed by the full path to the file or directory you want to convert. The program will then convert the file(s) and save them in the same directory as the original file(s) with a new extension. +`python main.py ./input_image.jpg ./output_image.png png` -## Error Handling: +For batch conversion: -The tool provides basic error handling to ensure the validity of the input paths. If any errors occur during the conversion, they are displayed on the terminal. +`python main.py ./input_directory ./output_directory png` -## Reasoning and Intentions Behind the Project +Note: When using `main.py` directly, make sure you have all the required dependencies installed in your Python environment. -This project aims to be a one-stop solution for various file conversion needs. With a focus on modularity, each conversion type is implemented as a pluggable component, allowing for easy extension and integration into other projects. The goal is to empower developers and users alike to build faster and smarter. +### Using the Formaverter package -Future plans include adding automated webhook file pushing via Discord and potentially other services to make the tool even more versatile. +You can use the Formaverter package directly from the command line: -## Detailed Usage Guide +`python -m image_converter ` -### Setup +- ``: Path to the input image file or directory +- ``: Path to save the converted image(s) +- ``: Desired output format (jpg, png, bmp, or webp) -1. Clone the repository. -2. Install the required Python packages. -3. Drop your files in to the 'data/' folder. +Example: -### Running the Program +`python -m image_converter ./input_image.jpg ./output_image.png png` -Execute `main.py` to start the application: +For batch conversion, provide a directory as the input path: -```bash +`python -m image_converter ./input_directory ./output_directory png` -python main.py +### Using Formaverter in your own projects +You can also import and use Formaverter in your own Python projects. Here's how: + +1. First, make sure you've installed Formaverter: + + `pip install formaverter` + +2. In your Python script, import the necessary functions: + + ```python + from formaverter import convert_image, ImageConverter + ``` + +To convert a single image using the straightforward `convert_image` function: + +```python +convert_image('path/to/input/image.jpg', 'path/to/output/image.png', 'png') ``` -You will be presented with a menu listing the available file conversion options. Enter the corresponding number for the conversion you wish to perform. +To use the ImageConverter class directly: + +```python +converter = ImageConverter('path/to/input/image.jpg', 'path/to/output/image.png', 'png') +converter.convert() +``` + +For batch conversion, you can use the collect_images function and loop through the results: + +```python +from formaverter import collect_images, convert_image +``` + +```python +input_directory = 'path/to/input/directory' +output_directory = 'path/to/output/directory' +output_format = 'png' + +image_files = collect_images(input_directory) + +for input_path in image_files: + filename = os.path.basename(input_path) + name, _ = os.path.splitext(filename) + output_path = os.path.join(output_directory, f"{name}.{output_format}") + convert_image(input_path, output_path, output_format) +``` + +This allows you to integrate Formaverter's functionality into your own Python scripts or larger projects, giving you more control over the conversion process. + +## Dependencies +- Pillow >= 11.0.0 + +## Development +To set up the development environment: + +1. Clone the repository +2. Install PDM if you haven't already: `pip install pdm` +3. Install dependencies: `pdm install` +4. Run tests: ex. `python -m unittest .\tests\test_image_converter.py` -You can either specify a single file or a directory for batch conversion. For a single file, provide the full path, including the file name and extension. For a directory, provide the full directory path. +## Contributing +Contributions are welcome! Please feel free to submit a Pull Request. -Converted files will be saved in the same directory as the original files but with new extensions. +## License +This project is licensed under the GNU Affero General Public License. See the LICENSE file for details. -### Logging +## Author +Daethyra <109057945+Daethyra@users.noreply.github.com> -Logs are saved in a file named `converter.log`. The log level is set to `INFO`. +## Version +2.0.0 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index d8edcd5..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -loguru -pytest -Pillow -odfpy diff --git a/src/formaverter/__init__.py b/src/formaverter/__init__.py new file mode 100644 index 0000000..e89240c --- /dev/null +++ b/src/formaverter/__init__.py @@ -0,0 +1,2 @@ +from .image_converter import convert_image, ImageConverter +from .image_collector import collect_images diff --git a/src/formaverter/image_collector.py b/src/formaverter/image_collector.py new file mode 100644 index 0000000..5190a20 --- /dev/null +++ b/src/formaverter/image_collector.py @@ -0,0 +1,39 @@ +""" +This module provides functionality to collect images from a directory. +""" + +import os +from typing import List +from .image_converter import ImageConverter + + +def collect_images(input_dir: str) -> List[str]: + """ + Collects all supported image files from the input directory. + + Args: + input_dir (str): Path to the input directory containing images. + + Returns: + List[str]: A list of paths to the supported image files. + + Raises: + ValueError: If the input directory does not exist. + """ + if not os.path.isdir(input_dir): + raise ValueError(f"Input directory does not exist: {input_dir}") + + supported_extensions = set( + ImageConverter.SUPPORTED_CONVERSIONS.keys() + ) # {'.jpg', '.jpeg', '.png', '.bmp', '.webp'} + image_files = [] + + for filename in os.listdir(input_dir): + file_path = os.path.join(input_dir, filename) + if ( + os.path.isfile(file_path) + and os.path.splitext(filename)[1].lower() in supported_extensions + ): + image_files.append(file_path) + + return image_files diff --git a/src/formaverter/image_converter.py b/src/formaverter/image_converter.py new file mode 100644 index 0000000..d368840 --- /dev/null +++ b/src/formaverter/image_converter.py @@ -0,0 +1,108 @@ +""" +This module provides a class for converting images to different formats using Pillow. It also has a function to simplify implementing the conversion logic. +""" + +import os +from PIL import Image + + +class ImageConverter: + SUPPORTED_CONVERSIONS = { + ".jpg": {".jpg", ".png", ".bmp", ".webp"}, + ".png": {".jpg", ".png", ".bmp", ".webp"}, + ".bmp": {".jpg", ".png", ".bmp", ".webp"}, + ".webp": {".jpg", ".png", ".bmp", ".webp"}, + } + + def __init__(self, input_path: str, output_path: str, output_format: str): + """ + Initializes a new instance of the ImageConverter class. + + Args: + input_path (str): Path to the input image file. + output_path (str): Path to save the converted image file. + output_format (str): Desired output format (e.g., 'png', 'jpg', 'bmp', 'webp'). + """ + self.input_path = input_path + self.output_path = output_path + self.output_format = output_format.lower() + + def convert(self) -> None: + """ + Converts an image to the desired format. + + Raises: + ValueError: If the input file format is not supported or the conversion is not possible. + """ + input_ext = os.path.splitext(self.input_path)[1].lower() + output_ext = os.path.splitext(self.output_path)[1].lower() + + if input_ext not in self.SUPPORTED_CONVERSIONS: + raise ValueError( + f"Unsupported input file format: {input_ext}. {self.supported_conversions()}" + ) + + if output_ext not in self.SUPPORTED_CONVERSIONS[input_ext]: + raise ValueError( + f"Unsupported conversion: {input_ext} to {output_ext}. {self.supported_conversions()}" + ) + + if input_ext == output_ext: + # If the input and output formats are the same, move the file instead of re-saving it + ## self._move_or_error() + print( + f"Skipping conversion for {self.input_path} (already in {self.output_format} format)" + ) + return + else: + self._convert_image(input_ext, output_ext) + + def _convert_image(self, input_ext: str, output_ext: str) -> None: + """ + Converts the input image to the specified output format using Pillow. + """ + img = Image.open(self.input_path) + img.save(self.output_path) + + def _move_or_error(self) -> None: + """ + If the input and output paths are the same, move the file instead of re-saving it. + """ + if self.input_path != self.output_path: + os.replace(self.input_path, self.output_path) + else: + raise ValueError( + f"Input and output paths are the same: {self.input_path}. Please provide a different output path." + ) + + @staticmethod + def supported_conversions() -> str: + """ + Returns a string with the supported file formats and conversions. + + Returns: + str: A string with the supported file formats and conversions. + """ + return ( + "Supported file formats and conversions:\n" + "JPEG: can be converted to JPEG, PNG, BMP, WebP\n" + "PNG: can be converted to JPEG, PNG, BMP, WebP\n" + "BMP: can be converted to JPEG, PNG, BMP, WebP\n" + "WebP: can be converted to JPEG, PNG, BMP, WebP" + ) + + +def convert_image(input_path: str, output_path: str, output_format: str) -> None: + """ + Converts an image to the desired format. + + Args: + input_path (str): Path to the input image file. + output_path (str): Path to save the converted image file. + output_format (str): Desired output format (e.g., 'png', 'jpg', 'bmp', 'webp'). + + Raises: + ValueError: If the input file format is not supported or the conversion is not possible. + """ + converter = ImageConverter(input_path, output_path, output_format) + converter.convert() diff --git a/src/image_converter.py b/src/image_converter.py deleted file mode 100644 index 2da6fd3..0000000 --- a/src/image_converter.py +++ /dev/null @@ -1,86 +0,0 @@ -import os -from PIL import Image - -class ImageConverter: - SUPPORTED_CONVERSIONS = { - '.jpg': {'.jpg'}, - '.png': {'.png'}, - '.bmp': {'.jpg', '.png'} - } - - def __init__(self, input_path: str, output_path: str): - """ - Initializes a new instance of the ImageConverter class. - - Args: - input_path (str): Path to the input image file. - output_path (str): Path to save the converted image file. - """ - self.input_path = input_path - self.output_path = output_path - - def convert(self) -> None: - """ - Converts an image to the desired format. - - Raises: - ValueError: If the input file format is not supported or the conversion is not possible. - """ - input_ext = os.path.splitext(self.input_path)[1].lower() - output_ext = os.path.splitext(self.output_path)[1].lower() - - if input_ext not in self.SUPPORTED_CONVERSIONS: - raise ValueError(f"Unsupported input file format: {input_ext}. " - f"Supported file formats and conversions: {self.supported_conversions()}") - - if output_ext not in self.SUPPORTED_CONVERSIONS[input_ext]: - raise ValueError(f"Unsupported output file format: {output_ext}. " - f"Supported file formats and conversions: {self.supported_conversions()}") - - if input_ext == '.jpg' and output_ext == '.jpg': - if self.input_path != self.output_path: - os.replace(self.input_path, self.output_path) - else: - raise ValueError(f"Input and output paths are the same: {self.input_path}. Please provide a different output path.") - elif input_ext == '.png' and output_ext == '.png': - if self.input_path != self.output_path: - os.replace(self.input_path, self.output_path) - else: - raise ValueError(f"Input and output paths are the same: {self.input_path}. Please provide a different output path.") - elif input_ext == '.bmp' and (output_ext == '.jpg' or output_ext == '.png'): - self._bmp_to_image(output_ext) - else: - raise ValueError(f"Conversion failed. Input: {self.input_path}, Output: {self.output_path}. " - f"{self.supported_conversions()}") - - def _bmp_to_image(self, output_ext: str) -> None: - img = Image.open(self.input_path) - img.save(self.output_path, output_ext.upper()) - - @staticmethod - def supported_conversions() -> str: - """ - Returns a string with the supported file formats and conversions. - - Returns: - str: A string with the supported file formats and conversions. - """ - return "Supported file formats and conversions:\n" \ - "JPEG: can be converted to JPEG (no conversion needed)\n" \ - "PNG: can be converted to PNG (no conversion needed)\n" \ - "BMP: can be converted to JPEG or PNG" - - -def convert_image(input_path: str, output_path: str) -> None: - """ - Converts an image to the desired format. - - Args: - input_path (str): Path to the input image file. - output_path (str): Path to save the converted image file. - - Raises: - ValueError: If the input file format is not supported or the conversion is not possible. - """ - converter = ImageConverter(input_path, output_path) - converter.convert() \ No newline at end of file diff --git a/src/requirements.txt b/src/requirements.txt deleted file mode 100644 index 5fe18d5..0000000 --- a/src/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -click -pillow -odfpy \ No newline at end of file diff --git a/src/text_converter.py b/src/text_converter.py deleted file mode 100644 index a791f93..0000000 --- a/src/text_converter.py +++ /dev/null @@ -1,113 +0,0 @@ -import os -import json -import csv -import xml.etree.ElementTree as ET -from odf import opendocument - -class TextConverter: - SUPPORTED_CONVERSIONS = { - '.json': {'.csv', '.json'}, - '.csv': {'.json', '.csv'}, - '.odt': {'.txt'}, - '.xml': {'.json'} - } - - def __init__(self, input_path: str, output_path: str): - """ - Initializes a new instance of the TextConverter class. - - Args: - input_path (str): Path to the input text file. - output_path (str): Path to save the converted text file. - """ - self.input_path = input_path - self.output_path = output_path - - def convert(self) -> None: - """ - Converts a text document to the desired format. - - Raises: - ValueError: If the input file format is not supported or the conversion is not possible. - """ - input_ext = os.path.splitext(self.input_path)[1].lower() - output_ext = os.path.splitext(self.output_path)[1].lower() - - if input_ext not in self.SUPPORTED_CONVERSIONS: - raise ValueError(f"Unsupported input file format: {input_ext}. " - f"Supported file formats and conversions: {self.supported_conversions()}") - - if output_ext not in self.SUPPORTED_CONVERSIONS[input_ext]: - raise ValueError(f"Unsupported output file format: {output_ext}. " - f"Supported file formats and conversions: {self.supported_conversions()}") - - if input_ext == '.json' and output_ext == '.csv': - self._json_to_csv() - elif input_ext == '.csv' and output_ext == '.json': - self._csv_to_json() - elif input_ext == '.odt' and output_ext == '.txt': - self._odt_to_txt() - elif input_ext == '.xml' and output_ext == '.json': - self._xml_to_json() - else: - raise ValueError(f"Conversion failed. Input: {self.input_path}, Output: {self.output_path}. " - f"{self.supported_conversions()}") - - def _json_to_csv(self) -> None: - with open(self.input_path, 'r') as f: - data = json.load(f) - with open(self.output_path, 'w', newline='') as f: - writer = csv.writer(f) - writer.writerow(data[0].keys()) - for row in data: - writer.writerow(row.values()) - - def _csv_to_json(self) -> None: - with open(self.input_path, 'r') as f: - reader = csv.DictReader(f) - data = [row for row in reader] - with open(self.output_path, 'w') as f: - json.dump(data, f, indent=4) - - def _odt_to_txt(self) -> None: - doc = opendocument.load(self.input_path) - with open(self.output_path, 'w') as f: - f.write(doc.text().replace('\n', ' ')) - - def _xml_to_json(self) -> None: - tree = ET.parse(self.input_path) - root = tree.getroot() - data = [] - for child in root: - data.append(child.attrib) - with open(self.output_path, 'w') as f: - json.dump(data, f, indent=4) - - @staticmethod - def supported_conversions() -> str: - """ - Returns a string with the supported file formats and conversions. - - Returns: - str: A string with the supported file formats and conversions. - """ - return "Supported file formats and conversions:\n" \ - "JSON: can be converted to CSV or JSON\n" \ - "CSV: can be converted to JSON or CSV (no conversion needed)\n" \ - "ODT: can be converted to plain text\n" \ - "XML: can be converted to JSON" - - -def convert_text(input_path: str, output_path: str) -> None: - """ - Converts a text document to the desired format. - - Args: - input_path (str): Path to the input text file. - output_path (str): Path to save the converted text file. - - Raises: - ValueError: If the input file format is not supported or the conversion is not possible. - """ - converter = TextConverter(input_path, output_path) - converter.convert() \ No newline at end of file diff --git a/tests.py b/tests.py deleted file mode 100644 index 48b5fd8..0000000 --- a/tests.py +++ /dev/null @@ -1,51 +0,0 @@ -import unittest -import logging -from pathlib import Path - -# Relative imports of test modules -from .tests.test_image_converter import ImageConverterTests -from .tests.test_text_converter import TextConverterTests -from .tests.test_main import MainModuleTests - -def setup_logging(): - """ - Sets up logging for the test results. - """ - log_file_path = Path(__file__).parent / 'test_results.log' - logging.basicConfig(filename=log_file_path, - level=logging.INFO, - format='%(asctime)s:%(levelname)s:%(message)s') - -def create_test_suite(): - """ - Creates a unified test suite combining all tests from the three modules. - """ - test_suite = unittest.TestSuite() - test_suite.addTest(unittest.makeSuite(ImageConverterTests)) - test_suite.addTest(unittest.makeSuite(TextConverterTests)) - test_suite.addTest(unittest.makeSuite(MainModuleTests)) - - return test_suite - -def configure_test_runner(): - """ - Configures a test runner that executes the test suite. - """ - return unittest.TextTestRunner(verbosity=2) - -def main(): - """ - Main function to run the test suite. - """ - setup_logging() - suite = create_test_suite() - runner = configure_test_runner() - test_result = runner.run(suite) - - if not test_result.wasSuccessful(): - logging.error(f"Number of failed tests: {len(test_result.failures)}") - for test, traceback in test_result.failures: - logging.error(f"{test.id()}: {traceback}") - -if __name__ == '__main__': - main() diff --git a/src/__init__.py b/tests/__init__.py similarity index 100% rename from src/__init__.py rename to tests/__init__.py diff --git a/tests/test_image_collector.py b/tests/test_image_collector.py new file mode 100644 index 0000000..de8416a --- /dev/null +++ b/tests/test_image_collector.py @@ -0,0 +1,63 @@ +import unittest +import sys +import os +import tempfile +import shutil + +sys.path.insert( + 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")) +) + +from formaverter.image_collector import collect_images +from formaverter.image_converter import ImageConverter + + +class TestImageCollector(unittest.TestCase): + + def setUp(self): + # Create a temporary directory + self.test_dir = tempfile.mkdtemp() + + # Create some test files + self.valid_images = ["test1.jpg", "test2.png", "test3.bmp", "test4.webp"] + self.invalid_files = ["test5.txt", "test6.pdf", "test7"] + + for filename in self.valid_images + self.invalid_files: + open(os.path.join(self.test_dir, filename), "a").close() + + def tearDown(self): + # Remove the directory after the test + shutil.rmtree(self.test_dir) + + def test_collect_images(self): + # Test if the function collects only valid image files + collected_images = collect_images(self.test_dir) + + self.assertEqual(len(collected_images), len(self.valid_images)) + + for image in collected_images: + self.assertTrue(os.path.basename(image) in self.valid_images) + + def test_non_existent_directory(self): + # Test if the function raises a ValueError for a non-existent directory + with self.assertRaises(ValueError): + collect_images("/path/to/non/existent/directory") + + def test_empty_directory(self): + # Test if the function returns an empty list for an empty directory + empty_dir = tempfile.mkdtemp() + self.assertEqual(collect_images(empty_dir), []) + shutil.rmtree(empty_dir) + + def test_supported_extensions(self): + # Test if the function only collects files with supported extensions + collected_images = collect_images(self.test_dir) + supported_extensions = set(ImageConverter.SUPPORTED_CONVERSIONS.keys()) + + for image in collected_images: + _, ext = os.path.splitext(image) + self.assertIn(ext.lower(), supported_extensions) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_image_converter.py b/tests/test_image_converter.py index aa709d7..dbebc75 100644 --- a/tests/test_image_converter.py +++ b/tests/test_image_converter.py @@ -1,85 +1,80 @@ -import pytest +import sys import os + +sys.path.insert( + 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")) +) + +import unittest +import tempfile +import shutil from PIL import Image -from ..src.image_converter import ImageConverter, convert_image - -# Test data setup -@pytest.fixture -def setup_images(tmp_path): - """ - Setup fixture to create dummy images in different formats. - """ - bmp_path = tmp_path / "test.bmp" - jpg_path = tmp_path / "test.jpg" - png_path = tmp_path / "test.png" - - # Create a simple image in BMP, JPG, PNG formats - img = Image.new("RGB", (100, 100), color="red") - img.save(bmp_path, "BMP") - img.save(jpg_path, "JPEG") - img.save(png_path, "PNG") - - return bmp_path, jpg_path, png_path - -# Test Cases for Successful Conversions -def test_convert_bmp_to_jpg(setup_images, tmp_path): - bmp_path, _, _ = setup_images - output_path = tmp_path / "output.jpg" - convert_image(str(bmp_path), str(output_path)) - assert os.path.exists(output_path) - -def test_convert_bmp_to_png(setup_images, tmp_path): - bmp_path, _, _ = setup_images - output_path = tmp_path / "output.png" - convert_image(str(bmp_path), str(output_path)) - assert os.path.exists(output_path) - -def test_copy_jpg(setup_images, tmp_path): - _, jpg_path, _ = setup_images - output_path = tmp_path / "copy.jpg" - convert_image(str(jpg_path), str(output_path)) - assert os.path.exists(output_path) - -def test_copy_png(setup_images, tmp_path): - _, _, png_path = setup_images - output_path = tmp_path / "copy.png" - convert_image(str(png_path), str(output_path)) - assert os.path.exists(output_path) - -# Test Cases for Failure Cases -def test_unsupported_input_format(setup_images, tmp_path): - _, _, _ = setup_images - output_path = tmp_path / "output.unknown" - with pytest.raises(ValueError): - convert_image("unsupported.format", str(output_path)) - -def test_unsupported_output_format(setup_images, tmp_path): - bmp_path, _, _ = setup_images - output_path = tmp_path / "output.unknown" - with pytest.raises(ValueError): - convert_image(str(bmp_path), str(output_path)) - -def test_same_input_output_path(setup_images): - bmp_path, _, _ = setup_images - with pytest.raises(ValueError): - convert_image(str(bmp_path), str(bmp_path)) - -# Test Cases for Edge Cases -def test_nonexistent_input_file(tmp_path): - nonexistent_path = tmp_path / "nonexistent.bmp" - output_path = tmp_path / "output.jpg" - with pytest.raises(FileNotFoundError): - convert_image(str(nonexistent_path), str(output_path)) - -def test_invalid_file_path(): - invalid_path = "/invalid/path/to/file.bmp" - with pytest.raises(ValueError): - convert_image(invalid_path, "output.jpg") - -# Utility Function Tests -def test_supported_conversions(): - expected_output = "Supported file formats and conversions:\n" \ - "JPEG: can be converted to JPEG (no conversion needed)\n" \ - "PNG: can be converted to PNG (no conversion needed)\n" \ - "BMP: can be converted to JPEG or PNG" - assert ImageConverter.supported_conversions() == expected_output +from formaverter.image_converter import ImageConverter, convert_image + + +class TestImageConverter(unittest.TestCase): + + def setUp(self): + self.test_dir = tempfile.mkdtemp() + self.input_jpg = os.path.join(self.test_dir, "test.jpg") + self.output_png = os.path.join(self.test_dir, "test.png") + self.output_jpg = os.path.join(self.test_dir, "test_output.jpg") + + # Create a test JPEG image + with Image.new("RGB", (100, 100), color="red") as img: + img.save(self.input_jpg) + + def tearDown(self): + shutil.rmtree(self.test_dir) + + def test_convert_jpg_to_png(self): + converter = ImageConverter(self.input_jpg, self.output_png, "png") + converter.convert() + self.assertTrue(os.path.exists(self.output_png)) + with Image.open(self.output_png) as img: + self.assertEqual(img.format, "PNG") + + def test_convert_jpg_to_jpg(self): + converter = ImageConverter(self.input_jpg, self.output_jpg, "jpg") + converter.convert() + self.assertFalse( + os.path.exists(self.output_jpg) + ) # Should not create a new file + + def test_unsupported_input_format(self): + invalid_input = os.path.join(self.test_dir, "test.txt") + with open(invalid_input, "w") as f: + f.write("This is not an image file") + with self.assertRaises(ValueError): + converter = ImageConverter(invalid_input, self.output_png, "png") + converter.convert() + + def test_unsupported_conversion(self): + invalid_output = os.path.join(self.test_dir, "test.gif") + with self.assertRaises(ValueError): + converter = ImageConverter(self.input_jpg, invalid_output, "gif") + converter.convert() + + def test_same_input_output_format(self): + output_jpg = os.path.join(self.test_dir, "test_same.jpg") + converter = ImageConverter(self.input_jpg, output_jpg, "jpg") + converter.convert() + self.assertFalse(os.path.exists(output_jpg)) # Should not create a new file + + def test_convert_image_function(self): + convert_image(self.input_jpg, self.output_png, "png") + self.assertTrue(os.path.exists(self.output_png)) + with Image.open(self.output_png) as img: + self.assertEqual(img.format, "PNG") + + def test_supported_conversions_string(self): + supported_str = ImageConverter.supported_conversions() + self.assertIsInstance(supported_str, str) + self.assertIn("JPEG", supported_str) + self.assertIn("PNG", supported_str) + self.assertIn("BMP", supported_str) + self.assertIn("WebP", supported_str) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_main.py b/tests/test_main.py deleted file mode 100644 index c91a215..0000000 --- a/tests/test_main.py +++ /dev/null @@ -1,54 +0,0 @@ -import pytest -from unittest.mock import patch, MagicMock -from ..main import * -import os - -# Test Menu Display -def test_print_menu(capsys): - main.print_menu() - captured = capsys.readouterr() - assert "File Converter" in captured.out - -# File Conversion Tests -@patch('main.input', side_effect=['1', '/path/to/pngfile.png', 'n']) -@patch('main.convert_files') -def test_convert_single_file(mock_convert_files, mock_input): - with patch('os.path.exists', return_value=True), \ - patch('os.path.isfile', return_value=True): - main.main() - mock_convert_files.assert_called_once() - -@patch('main.input', side_effect=['1', '/path/to/directory', 'n']) -@patch('main.convert_files') -def test_convert_batch_files(mock_convert_files, mock_input): - with patch('os.listdir', return_value=['file1.png', 'file2.png']), \ - patch('os.path.exists', return_value=True), \ - patch('os.path.isdir', return_value=True): - main.main() - assert mock_convert_files.call_count == 1 - -# User Input Handling -@patch('main.input', side_effect=['7', 'q']) -def test_invalid_choice(mock_input, capsys): - main.main() - captured = capsys.readouterr() - assert "Invalid choice!" in captured.out - -@patch('main.input', side_effect=['1', '/invalid/path.png', 'n']) -def test_invalid_file_path(mock_input, capsys): - with patch('os.path.exists', return_value=False): - main.main() - captured = capsys.readouterr() - assert "Invalid path!" in captured.out - -# Error Handling Tests -def test_unsupported_conversion_type(): - with pytest.raises(ValueError): - main.convert_extension("unsupported_type") - -@patch('main.input', side_effect=['1', '/nonexistent/path.png', 'n']) -def test_nonexistent_path_handling(mock_input, capsys): - with patch('os.path.exists', return_value=False): - main.main() - captured = capsys.readouterr() - assert "Invalid path!" in captured.out diff --git a/tests/test_text_converter.py b/tests/test_text_converter.py deleted file mode 100644 index daad6ec..0000000 --- a/tests/test_text_converter.py +++ /dev/null @@ -1,109 +0,0 @@ -import pytest -import os -import json -import csv -import xml.etree.ElementTree as ET -from odf.opendocument import load as load_odt -from ..src.text_converter import TextConverter, convert_text -from io import StringIO - -# Helper Functions for Test Data Creation -def create_json_file(path, data): - with open(path, 'w') as file: - json.dump(data, file) - -def create_csv_file(path, data): - with open(path, 'w', newline='') as file: - writer = csv.writer(file) - writer.writerow(data[0].keys()) - for row in data: - writer.writerow(row.values()) - -def create_xml_file(path, data): - root = ET.Element("root") - for item in data: - child = ET.SubElement(root, "child", attrib=item) - tree = ET.ElementTree(root) - tree.write(path) - -def create_odt_file(path, text): - doc = opendocument.Text() - para = P(text) - doc.text.addElement(para) - doc.save(path) - -# Test Cases for Successful Conversions -@pytest.fixture -def setup_files(tmp_path): - # Create dummy files for each format - json_path = tmp_path / "test.json" - csv_path = tmp_path / "test.csv" - xml_path = tmp_path / "test.xml" - odt_path = tmp_path / "test.odt" - - # Dummy data - data = [{"name": "John", "age": 30}, {"name": "Jane", "age": 25}] - create_json_file(json_path, data) - create_csv_file(csv_path, data) - create_xml_file(xml_path, [{"name": "John"}, {"name": "Jane"}]) - create_odt_file(odt_path, "Sample text") - - return json_path, csv_path, xml_path, odt_path - -def test_convert_json_to_csv(setup_files, tmp_path): - json_path, _, _, _ = setup_files - output_path = tmp_path / "output.csv" - convert_text(str(json_path), str(output_path)) - assert os.path.exists(output_path) - -def test_convert_csv_to_json(setup_files, tmp_path): - _, csv_path, _, _ = setup_files - output_path = tmp_path / "output.json" - convert_text(str(csv_path), str(output_path)) - assert os.path.exists(output_path) - -def test_convert_odt_to_txt(setup_files, tmp_path): - _, _, _, odt_path = setup_files - output_path = tmp_path / "output.txt" - convert_text(str(odt_path), str(output_path)) - assert os.path.exists(output_path) - -def test_convert_xml_to_json(setup_files, tmp_path): - _, _, xml_path, _ = setup_files - output_path = tmp_path / "output.json" - convert_text(str(xml_path), str(output_path)) - assert os.path.exists(output_path) - -# Test Cases for Failure Cases -def test_unsupported_input_format(tmp_path): - unsupported_path = tmp_path / "unsupported.format" - output_path = tmp_path / "output.csv" - with pytest.raises(ValueError): - convert_text(str(unsupported_path), str(output_path)) - -def test_unsupported_output_format(setup_files, tmp_path): - json_path, _, _, _ = setup_files - output_path = tmp_path / "unsupported.format" - with pytest.raises(ValueError): - convert_text(str(json_path), str(output_path)) - -# Test Cases for Edge Cases -def test_nonexistent_input_file(tmp_path): - nonexistent_path = tmp_path / "nonexistent.json" - output_path = tmp_path / "output.csv" - with pytest.raises(FileNotFoundError): - convert_text(str(nonexistent_path), str(output_path)) - -def test_invalid_file_path(): - invalid_path = "/invalid/path/to/file.json" - with pytest.raises(ValueError): - convert_text(invalid_path, "output.csv") - -# Utility Function Tests -def test_supported_conversions(): - expected_output = "Supported file formats and conversions:\n" \ - "JSON: can be converted to CSV or JSON\n" \ - "CSV: can be converted to JSON or CSV (no conversion needed)\n" \ - "ODT: can be converted to plain text\n" \ - "XML: can be converted to JSON" - assert TextConverter.supported_conversions() == expected_output