From 2eeeb923d321a136630f5adcf00117fe963304fe Mon Sep 17 00:00:00 2001 From: Daethyra <109057945+Daethyra@users.noreply.github.com> Date: Sat, 14 Dec 2024 17:52:36 -0800 Subject: [PATCH 01/20] deleted unused files and archive directory --- .github/.archive/converter.py | 149 ---------------------------------- data/Screen Time Data.csv | 29 ------- src/requirements.txt | 3 - src/text_converter.py | 113 -------------------------- tests.py | 51 ------------ tests/test_image_converter.py | 85 ------------------- tests/test_main.py | 54 ------------ tests/test_text_converter.py | 109 ------------------------- 8 files changed, 593 deletions(-) delete mode 100644 .github/.archive/converter.py delete mode 100644 data/Screen Time Data.csv delete mode 100644 src/requirements.txt delete mode 100644 src/text_converter.py delete mode 100644 tests.py delete mode 100644 tests/test_image_converter.py delete mode 100644 tests/test_main.py delete mode 100644 tests/test_text_converter.py 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/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/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/tests/test_image_converter.py b/tests/test_image_converter.py deleted file mode 100644 index aa709d7..0000000 --- a/tests/test_image_converter.py +++ /dev/null @@ -1,85 +0,0 @@ -import pytest -import os -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 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 From 7b8966370d40d215d38fa138ef79a5680990806e Mon Sep 17 00:00:00 2001 From: Daethyra <109057945+Daethyra@users.noreply.github.com> Date: Sat, 14 Dec 2024 17:59:10 -0800 Subject: [PATCH 02/20] feature(image_converter): add WebP support and refactor conversion logic Added support for converting images to and from the WebP format, expanding the range of supported conversions. Refactored the conversion logic to streamline the process by consolidating similar operations and removing redundant checks. Introduced a new method `_convert_image` to handle all image format conversions using Pillow, and `_move_or_error` to manage file moves when input and output formats are identical. Updated the `supported_conversions` method to reflect these changes. This enhances functionality and improves code maintainability. --- src/image_converter.py | 59 +++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/src/image_converter.py b/src/image_converter.py index 2da6fd3..1f54073 100644 --- a/src/image_converter.py +++ b/src/image_converter.py @@ -1,11 +1,16 @@ +""" +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': {'.png'}, - '.bmp': {'.jpg', '.png'} + '.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): @@ -30,33 +35,32 @@ def convert(self) -> None: 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()}") + 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 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) + raise ValueError(f"Unsupported conversion: {input_ext} to {output_ext}. {self.supported_conversions()}") + + if input_ext == output_ext: + self._move_or_error() else: - raise ValueError(f"Conversion failed. Input: {self.input_path}, Output: {self.output_path}. " - f"{self.supported_conversions()}") + self._convert_image(input_ext, output_ext) - def _bmp_to_image(self, output_ext: str) -> None: + 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, output_ext.upper()) + 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: """ @@ -66,9 +70,10 @@ def supported_conversions() -> str: 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" + "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) -> None: @@ -83,4 +88,4 @@ def convert_image(input_path: str, output_path: str) -> None: 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 + converter.convert() From e7776ed921ca36554832dfa6dbcba30b31cfa238 Mon Sep 17 00:00:00 2001 From: Daethyra <109057945+Daethyra@users.noreply.github.com> Date: Sat, 14 Dec 2024 18:08:31 -0800 Subject: [PATCH 03/20] Add placeholder pyproject.toml --- pyproject.toml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d3ac33d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "image-type-converter" +version = "0.3.0" +description = "Converts images from one format to another using Pillow." +authors = ["Daethyra <109057945+Daethyra@users.noreply.github.com>"] +license = "GNU Aferro GPL" +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.13" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" From c3964bcf9dd7e96e81d9f0237f97473c4b3ebdd3 Mon Sep 17 00:00:00 2001 From: Daethyra <109057945+Daethyra@users.noreply.github.com> Date: Sat, 14 Dec 2024 18:27:26 -0800 Subject: [PATCH 04/20] Added Pillow, created `poetry.lock` file --- poetry.lock | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + 2 files changed, 99 insertions(+) create mode 100644 poetry.lock diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..a200750 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,98 @@ +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. + +[[package]] +name = "pillow" +version = "11.0.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pillow-11.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947"}, + {file = "pillow-11.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f"}, + {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb"}, + {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97"}, + {file = "pillow-11.0.0-cp310-cp310-win32.whl", hash = "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50"}, + {file = "pillow-11.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c"}, + {file = "pillow-11.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1"}, + {file = "pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc"}, + {file = "pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa"}, + {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306"}, + {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9"}, + {file = "pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5"}, + {file = "pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291"}, + {file = "pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9"}, + {file = "pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923"}, + {file = "pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7"}, + {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6"}, + {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc"}, + {file = "pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6"}, + {file = "pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47"}, + {file = "pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25"}, + {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-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba"}, + {file = "pillow-11.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e"}, + {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f"}, + {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae"}, + {file = "pillow-11.0.0-cp39-cp39-win32.whl", hash = "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4"}, + {file = "pillow-11.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd"}, + {file = "pillow-11.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944"}, + {file = "pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.13" +content-hash = "9dd06d93b71f8db5159cc6afa9e229be38b7bcee1707e571f1c81145cc58ef8b" diff --git a/pyproject.toml b/pyproject.toml index d3ac33d..2e970f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.13" +pillow = "^11.0.0" [build-system] From 4c9767b1bab427ca4f177b64a85d6df042b57bc4 Mon Sep 17 00:00:00 2001 From: Daethyra <109057945+Daethyra@users.noreply.github.com> Date: Sat, 14 Dec 2024 18:27:41 -0800 Subject: [PATCH 05/20] feature(image-collection): add image collector module Introduced a new module `image_collector.py` to facilitate the collection of image files from a specified directory. The function `collect_images` retrieves paths of supported image formats, leveraging extensions defined in `ImageConverter.SUPPORTED_CONVERSIONS`. It raises a `ValueError` if the input directory is invalid. This enhancement aids in organizing and processing images efficiently within the application. --- src/image_collector.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/image_collector.py diff --git a/src/image_collector.py b/src/image_collector.py new file mode 100644 index 0000000..7757668 --- /dev/null +++ b/src/image_collector.py @@ -0,0 +1,33 @@ +""" +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 \ No newline at end of file From 3dfd47454b5e53a9fc592007959f58e441e5f419 Mon Sep 17 00:00:00 2001 From: Daethyra <109057945+Daethyra@users.noreply.github.com> Date: Sat, 14 Dec 2024 18:28:00 -0800 Subject: [PATCH 06/20] Add file --- src/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/__init__.py b/src/__init__.py index e69de29..264707d 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -0,0 +1,2 @@ +from .image_converter import convert_image, ImageConverter +from .image_collector import collect_images \ No newline at end of file From 5f56d1302eec4f4165839056d592c48ae3789fb1 Mon Sep 17 00:00:00 2001 From: Daethyra <109057945+Daethyra@users.noreply.github.com> Date: Sat, 14 Dec 2024 18:28:17 -0800 Subject: [PATCH 07/20] test(image_collector, image_converter): add unit tests for image collection and conversion Added comprehensive unit tests for the `collect_images` function in `image_collector` and various methods in `image_converter`. These tests cover scenarios such as collecting valid images, handling non-existent directories, and ensuring only supported file extensions are processed. For `image_converter`, tests include converting JPEG to PNG, handling unsupported formats, and verifying that input and output paths differ. This ensures robustness and correctness of image processing functionalities. --- tests/test_image_collector.py | 58 +++++++++++++++++++++++++++++ tests/test_image_converter.py | 69 +++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 tests/test_image_collector.py create mode 100644 tests/test_image_converter.py diff --git a/tests/test_image_collector.py b/tests/test_image_collector.py new file mode 100644 index 0000000..896664b --- /dev/null +++ b/tests/test_image_collector.py @@ -0,0 +1,58 @@ +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 image_collector import collect_images +from 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() \ No newline at end of file diff --git a/tests/test_image_converter.py b/tests/test_image_converter.py new file mode 100644 index 0000000..6a7a5fa --- /dev/null +++ b/tests/test_image_converter.py @@ -0,0 +1,69 @@ +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 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 + Image.new('RGB', (100, 100), color='red').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) + converter.convert() + self.assertTrue(os.path.exists(self.output_png)) + self.assertEqual(Image.open(self.output_png).format, 'PNG') + + def test_convert_jpg_to_jpg(self): + converter = ImageConverter(self.input_jpg, self.output_jpg) + converter.convert() + self.assertTrue(os.path.exists(self.output_jpg)) + self.assertNotEqual(self.input_jpg, self.output_jpg) + + def test_unsupported_input_format(self): + invalid_input = os.path.join(self.test_dir, 'test.txt') + open(invalid_input, 'w').close() + with self.assertRaises(ValueError): + converter = ImageConverter(invalid_input, self.output_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) + converter.convert() + + def test_same_input_output_path(self): + with self.assertRaises(ValueError): + converter = ImageConverter(self.input_jpg, self.input_jpg) + converter.convert() + + def test_convert_image_function(self): + convert_image(self.input_jpg, self.output_png) + self.assertTrue(os.path.exists(self.output_png)) + self.assertEqual(Image.open(self.output_png).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() \ No newline at end of file From 92d4223678570ac9f8ff4ab081a6cfebee2c0244 Mon Sep 17 00:00:00 2001 From: Daethyra <109057945+Daethyra@users.noreply.github.com> Date: Sat, 14 Dec 2024 18:28:38 -0800 Subject: [PATCH 08/20] refactor(main): streamline image conversion process Replaced the interactive menu-driven file conversion system with a command-line interface using argparse for improved usability and automation. Removed logging setup and text conversion functionalities to focus solely on image processing. Introduced `process_images` function to handle both single image and directory conversions, utilizing `convert_image` and `collect_images`. This refactoring simplifies the codebase, enhances maintainability, and aligns the module with modern CLI practices. --- main.py | 210 +++++++++++++++----------------------------------------- 1 file changed, 55 insertions(+), 155 deletions(-) diff --git a/main.py b/main.py index ec2c43c..a7fa8c5 100644 --- a/main.py +++ b/main.py @@ -1,167 +1,67 @@ +""" +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. - - 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.") +from image_converter import convert_image +from image_collector import collect_images - -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) + 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) + + if image_files: # Only create output directory if we have images to convert + os.makedirs(output_path, exist_ok=True) + + 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) + 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)}") \ No newline at end of file From 27009332107a8cdd127353ad708dc1440ab66d71 Mon Sep 17 00:00:00 2001 From: Daethyra <109057945+Daethyra@users.noreply.github.com> Date: Sat, 14 Dec 2024 18:29:29 -0800 Subject: [PATCH 09/20] fix(image_converter): remove redundant format argument in save method The `img.save` method no longer requires the output format to be explicitly specified, as Pillow already infers the format from the file extension of `self.output_path`. This change simplifies the code and reduces potential errors from mismatched extensions and formats. Error was found using the `test_image_converter.py` file :o --- src/image_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/image_converter.py b/src/image_converter.py index 1f54073..3820a11 100644 --- a/src/image_converter.py +++ b/src/image_converter.py @@ -50,7 +50,7 @@ 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, output_ext.upper()) + img.save(self.output_path) def _move_or_error(self) -> None: """ From 7d56caae705d1be3cb7348e1b0c6661d751c20c3 Mon Sep 17 00:00:00 2001 From: Daethyra <109057945+Daethyra@users.noreply.github.com> Date: Sat, 14 Dec 2024 18:40:54 -0800 Subject: [PATCH 10/20] refactor(project structure): reorganize imports and update function signature - Program is non-operational Reorganized the project structure by moving `image_converter` and `image_collector` modules into a `src` directory, updating import paths accordingly. Removed `requirements.txt` as it is no longer needed in the current setup. Modified the `convert_image` function to include an `output_format` parameter, aligning with the updated `ImageConverter` class requirements. These changes improve code organization and functionality, ensuring compatibility with the new module structure and enhancing image conversion capabilities. --- main.py | 4 ++-- requirements.txt | 4 ---- src/image_collector.py | 2 +- src/image_converter.py | 4 ++-- 4 files changed, 5 insertions(+), 9 deletions(-) delete mode 100644 requirements.txt diff --git a/main.py b/main.py index a7fa8c5..6f61b06 100644 --- a/main.py +++ b/main.py @@ -5,8 +5,8 @@ import os import argparse from typing import List -from image_converter import convert_image -from image_collector import collect_images +from src.image_converter import convert_image +from src.image_collector import collect_images def process_images(input_path: str, output_path: str, output_format: str) -> List[str]: """ 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/image_collector.py b/src/image_collector.py index 7757668..bdc9286 100644 --- a/src/image_collector.py +++ b/src/image_collector.py @@ -4,7 +4,7 @@ import os from typing import List -from image_converter import ImageConverter +from src.image_converter import ImageConverter def collect_images(input_dir: str) -> List[str]: """ diff --git a/src/image_converter.py b/src/image_converter.py index 3820a11..53b7837 100644 --- a/src/image_converter.py +++ b/src/image_converter.py @@ -76,7 +76,7 @@ def supported_conversions() -> str: "WebP: can be converted to JPEG, PNG, BMP, WebP" -def convert_image(input_path: str, output_path: str) -> None: +def convert_image(input_path: str, output_path: str, output_format: str) -> None: """ Converts an image to the desired format. @@ -87,5 +87,5 @@ def convert_image(input_path: str, output_path: str) -> None: Raises: ValueError: If the input file format is not supported or the conversion is not possible. """ - converter = ImageConverter(input_path, output_path) + converter = ImageConverter(input_path, output_path, output_format) converter.convert() From 4826d4ba617adbcfcd8a0282174c5723db52714b Mon Sep 17 00:00:00 2001 From: Daethyra <109057945+Daethyra@users.noreply.github.com> Date: Sat, 14 Dec 2024 18:49:02 -0800 Subject: [PATCH 11/20] fix(image processing): ensure file existence before appending to list - Program is operational Added checks to verify the existence of output files before appending them to the converted images list in `process_images` function. This prevents potential issues with non-existent files being recorded as successfully processed. Additionally, updated the `ImageConverter` class to include `output_format` as an initialization parameter and adjusted logic to skip conversion if input and output formats are identical, providing a message instead. These changes enhance robustness and clarity in image conversion handling. --- main.py | 9 ++++----- src/image_converter.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/main.py b/main.py index 6f61b06..fb9cbce 100644 --- a/main.py +++ b/main.py @@ -33,21 +33,20 @@ def process_images(input_path: str, output_path: str, output_format: str) -> Lis 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) - converted_images.append(output_file_path) + 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) - - if image_files: # Only create output directory if we have images to convert - os.makedirs(output_path, exist_ok=True) 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) - converted_images.append(output_file_path) + 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)}") diff --git a/src/image_converter.py b/src/image_converter.py index 53b7837..c51dc0a 100644 --- a/src/image_converter.py +++ b/src/image_converter.py @@ -13,16 +13,18 @@ class ImageConverter: '.webp': {'.jpg', '.png', '.bmp', '.webp'} } - def __init__(self, input_path: str, output_path: str): + 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: """ @@ -41,7 +43,10 @@ def convert(self) -> None: raise ValueError(f"Unsupported conversion: {input_ext} to {output_ext}. {self.supported_conversions()}") if input_ext == output_ext: - self._move_or_error() + # 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) @@ -83,6 +88,7 @@ def convert_image(input_path: str, output_path: str, output_format: str) -> None 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. From 021cdb4b3ab9db6d64d84faab4ccc9c75f76a16a Mon Sep 17 00:00:00 2001 From: Daethyra <109057945+Daethyra@users.noreply.github.com> Date: Sat, 14 Dec 2024 18:56:21 -0800 Subject: [PATCH 12/20] removing test pictures --- data/CHAD.png | Bin 15634 -> 0 bytes data/looking respectfully.jpg | Bin 82919 -> 0 bytes data/peepoHehe.webp | Bin 8744 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 data/CHAD.png delete mode 100644 data/looking respectfully.jpg delete mode 100644 data/peepoHehe.webp diff --git a/data/CHAD.png b/data/CHAD.png deleted file mode 100644 index f222adcd5407bfa72bd0e314af17d9512d692e7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15634 zcmV+tJ?+AYP)+x=*_Cg_(Ji)9!4VO@p)uODuK)Fn}NlP_!tKltjQRlXjUU5Uz5SRkjP@ zA9l$l%Y@{TMY1FlM9G3F5&!`Z5J(n*B|vPBvk@C+W+%K!FW+~=3Ek~K`rP!wgx%Q% zSmrl%Yu*j#obLYm%b|t8Mc~}x0oPqQZnF+zyO6D-;uKf~mx0Z|v_UfeLg8ElsKGr% z4gYpjk=i~?Q6tYIavPB}x4bQiW}bSh=k|I*e%J$83(hT0(kqVO@{c3t2ub}DxYf@) z)BONNhaz3Z^jZ{Y3vIguwvE^}#%4HjjOlN*C|sS|)&mes zLOA`jRQDZuE7##l;71C9&Yk@%t+SsYo5S?xaQPg<3f8vBi!Ro5(JG}d9Zb=NJZB;Z z5o2&R$G8k7&La?yKudrfnWffx@V5|R32;_0rob3Q5H?Y|Mi@+jo+33hB$^_Ow6RChshJ4DkE&pa*i*2&7Z z6oJm2{p{+}E1${pd3xOibng`P&I`oFJR$)i8fQHV6LE+*FaTySQY!|dz=}mV2nXD< zM9c{O5Q|fzA8dj^0qqLJSwa_}t@4_y1t%7R8{Qv_D2FJENEIhrQPB)0o+OX9lWe^O zm+XC7y+ufT%Mj?=&pF#WM!NbcHl3$PTLeXmz%CHb1FT0Jr2--a$|-OmgT)&e?LZ!m zz5#HfZy2M#wP3|lU-@oud*$%d44A$wUAIzx18EfkF-26TNVbs%`*4*TsZHHU6dioi z*EhKQPu>`~^!&$ZAOA|W^e{#D7_N7Uz^oEx4s9*yyi^)2Q23V=AUW1EG)4{aKC%WY zBX-R;T!X+%fO0TSvO}o?)A<%;bH8dIg_oF{h zB_)k(qE`ZaigR{j3RDiyC8P%^3KF17P8}UJ3E~Dsdr|SNNOB07yzxz0>o?`5uKU`F z_R59*rhAg0ca*@s2KF4zE+eK(U=_;LfB-ZY=?`W^n=C;w!a`v^0teLF|9%YxzNrWt znk(Mh6M$2Q)~G0gXa=I2F!~_qo#VCyz0SR0lc8}3ZU&W;-aIIs+ zu7lv!ePxZ;6kWOSN{yk*vo$qXFm=fxUI}6@!2&Nek3Is+4Y4s}4iFoaBgy=;Trv2u zBcY=k#P$6IwHvUq2UWQlnLc#Q*8Q5wK)3X~bFG)j)0eQt%c%T3fh?h&1s5PrVG$G- zCq1mP$bfoOIq_heYQ(WQ5-dYDxdB{4fw>kS&dcrfPxsFi>5#Y{o#ho&x=3v*#bp6* z>6nwr>#hkBuL%O3TbyL|_+s99maO|CEz@ef@>M z90Grc(092xKR<^T!BzpHP*~!)g6Va!`?h@l$R}inIvn3k-dc3paveAkq_6-SjIna zSG6Lxbe!Xr7#EC;$In+=Pa{^qNC3J9QXvSNxa4}&%)LnM?XMdTZ-@fj^0SVGM<9Cz zYfgc^NI<9jR+axQA;U_AWsJ=Y-3YXcPdK~!9Q00Ndb3!W2i+l%o>!&&R3&!vMU+x&gCBvo z$hC%Y^GUYu8;6%9mPNAm56C6NwV!oTyX~r1;dLRgt~gj@!JuhWkE(Sbf9*K-r0jVfj-Rs*_L;Z~12so!x`NW5wUy6ogrZtfY< z)#q{PaUvE8gKS{`h$>~8>#NytMF?0+rDM|=PJW$?x;mltrh^P*YF}utc|QUH29sSN zGJpgSXk7ZLllb~8p43%sZPz<>KfPmjX6Z|q-btibAdn8qWu=j>5fdRcKx`k8mw7}f z{5O65ZHN9EY%n+NAj&hsVu_$Qh0RVtI*UtZKYyjSzpCw#n`&*0kUsOpT+ zbcd#aQ|q+40qfZ4N&Sg!w2uUaTAtmM(A*^`P?a&g_4gfp|3N`=*BTzjsbtQq}E8gO+d*Kt^xd*Z72~=?&Eh(YMfL#t@t;{Mmzo~2{-}(r% z-ajgKa}kMbau2ToD*vPMd7BT#`%vu~g!=o68~*KoTc&07H`%5zvO#6dI=H^a$pSgb zba3V@z0M0%lLinkUS`C=)yTls(|_Pro`mcep{$@#Sff!c!Fxk`rD5yJ!9_Uf&vmny z8vfe8QyND8_VG4xm(XHSC<$hOScb>>MWO&~6pP%>h~Jy4jOUdr;3V+DQm4FUVjc7+HDd3rwE+d#hz<7P3fbK_TkH?U4RdMwdj06v z{m<8pnc=v`=D<+iuZ@_jy-$BmQedS3ZG1|HE0E$Mws(r2ewA1zCtP>&fK)b5nz}3k z&D`_Oc3vRQj$yJh#8NttCBv!=B^jn~?_p44fWmX6lK<<`G?k4WGWc)nkp0c1Uh%&6WvpfLgyt^F^gguAwfC3OIj>N-)tZOBDAFC#mf5w zt@24jqi(IW>~gT1*ORInTfB{64bs;sEo-k;oBZ9se)Kahi$_`SSj^HX7}&Za@^YQ2 zlYnXZ|G|qJy@HdDXYu76#3@1}2q`w0!*ow!qjUQ&G5L)V=-lD~C=QUF`~xXYqU>qJ z%%fC>GD&$^h;@L(=&+6}YGipzT!Yrq0zyb7ZepE9>j&KaMRA{J}SKyLLxtU?naBE!VAG636`ZFWsKN-J>IU`*+&RDn-)>L)ve z+KdgP?UxCH1`j^;9M3*A%lWe{E?&F{K$c}h@eqMg z7%?$fW82IG6H_5uXKK_MimB-;%|^_Y$ts~%1cCOH<{Yk+Uz!Y52V7nz9~GEh7rdu-0O6SnJ3$OMBIj^)#nWUSw%L<@~ut=9W6FWR}%5 z+Zcfpb$0HUAqZolpouk(XPFead|4bCkl z7*`+8y(nn+y0lkU>GpaoFSl5nZ!tSN$NYSk3+GqptisYFv^vl-ep$5k^Y8>PoL>+? zvjK;0ILIA$943xKUV8aOUViyymM$3Heh7Zyqj&LBKXD5rIkdYV;KWb_4oO0S+99aF zA8I#4GI^iKur6am1a@cdcQH?}dhszTW|hFUL1kDJt+r!g+l{QY>OA%I8GiTozs%_~ zhQ0fyx&6*NX?Hq&`6~~z*z(IsTwEmJkDwS>tuPdxY%fBu)x z^UCQE31`R)PerS%EmpghR@)KXq8z9Ibm%j;^VDIim$7|Ws8onj3TJwB zx|TGDkN)%x-1Y7w6t=@t4?WDI-nwU1I^NjrF`j0$j%d)7RqLRC*w1jCuVlgUJ?HCFCXViU-}#7mK|@q>j?V~ z9^h{tc!cMkoh6AvlE|Qf5TyizAuSArwFBcmF97_c-ALlIPlX4iJezYm!Wac)h|4*1 z&Vhh+24&h*X=77Ry3KpueLWB}szc+v>yzI1Yh&(APpp11Cg+Z($N-R+Y9(eF&o_+ph?t0Iy{MfxeMO{C^ z6W@QHuYL11w(j^ozwr0&p}TsPEbR~n4jmX=*1~0Tm}mjlz3>UC%zS=R1m^ktgt!&j zs~3q}Z=~`^flJ6cG0#7HnlJyq-(z7F?)}IIS?U)2@mIga!hDY)tg^7&AxSDI4c%@R zrTyfzcm=P>z*tLRq1j9ZhVdYkum2v0)rP#xsR@D*rNP>QG_|?|UCFo&GLQd@CoJ)QAKuFZYN-L7}{Eq#Fx!!q6Gwqc0q}8Gh*(?&d=ux{Z3Ri^*0-Y{9jZXwi`% z+OhZG4FA<{{6o@-hxz{F$9VPC0ug)$L77M;uC#Q@LK;!Sji?o%p}9>HgdtHB5r&Fd zy-aEnixoI^a*l2a-7cg(FkOfPSeWg=>T7&EH5|EhFQ542UBuBm^+t%LOP+QJV-2ZA zT!!R}NH&K}FXFmqKPwflk&YvnuvV{Uns z#~y!<)8~D5`Q$WIw!^_gRrc(!Gd-!u~Fk;F|VC$?h=$TLfpT2|X_I=z&3 zyG1umF{YrKrL@{@I=uoRCe19pUXL_QSzKDeSxX$pY?+Mt$h~*+-gn(Vy=G~*7Knm= znxFS;4m>oaL7uxVNCZKb9Xq%3-~Yx(*#D(J=b7hzK#?i3La}G}0ovV^Akb866)H(Y zt&xya0+cwSAS8}sv<{i5H;AH$BnpXQp*fL|7d_IVL#x%MwOVlDVw>61=b4*ZV)o2D z7tW{jLO655lFpr^XfN|${`!Y`*Sp?9y?PO?mr>%^IjYpK)`4r$YrRH1eJiNWXMptx zEYcPxyNELv2~`(mQy?l|fKyz5{Xu@|A5?hc$>ZFA|4CN6aMOM`eCu^=-&tkn-YN$U zG&y)^k|;`PghDlpQI!c$btpne1tf-`;_sUkbh9p5meH7Kk`+1KUXLsXr5kiRDNC&- zI-M5I8QPsy5KE)3x%v8SByo>+cZnzlj4$bw0UIr+*mpcjUPdW;C{}1zbN>F%9N`1+ zK1invQ9Mb#HtkuX(&$JNgqoxh5JdrDs0c)(beV+gJW_)IMCT}aNRVJngo<`xQ$@Gu zXty)cp5d`ak1{{kV)pcTUVQN^&p!_@{9u_cefdk=dfTs1udUAoQsU5bh*^QoEXcOO zcLM~v?!|jo=AXhA3rMj6$t)Bal;!B-KVV_LOOa`+wMlfKvBqMpFXL*|>lhr?`efiBtP_MG zacHT8mfW=2u`^`Xt{F1hLa9YWLzEagtE*H4xrFn(pNQ+z0hM&PYKT{(x+6^zp?om<8!=gkBGFy2dKEY^T4w0AJLE>61Cs*c-kyB<`JN)!?X0W}p+ zYbdBI_WtCJ;Dp76WuAZW9QQwXlJES@Ys6LPrHeq~U2cOgodyat1+YrKc#2T%#`Vtq zmLxkqHxTu1=@qwp@ey?X2x0aTaW)4og)qda1m~(gan)8(xGPI;sj58sB{72v5*sB@y7a^`dQF!*I#A|iddSymRMJ6vZ5?v{i zP;mGA6F&MgKTcFt2C z49*Hg;bWIk9G7Ei(skQ7aq2AH&MbG_wjZ%8C^Cei^t9_-gQ|~04xf@lx zaTvX{7anoaK8jiX9=bRQS{t1UO`C@N9q^a1vu;dg#bYk zmOCY)vs{p8Az@hewxG4X$$)c+2!*p`rP*O!={a#Z;&KAXsl>wc)(Nc5$wTAo{0d8; z6oD})n==d~_wzE#7-JYv^c=3J5l(EUSX?GZEKKFJF3i$PLvkCVs1fKGS5h0sdu5u< zNt}~`Hz?2{O6!59ofjFsUYkG(LC`~mD+I~`HLx0E)rf;30N2E)U}lJ9h#Gd;@aw2b zHP&KDaE*Or@r|g7A0wG~j|AYg&;0DegvBz-u0oM|t64(714<9OqVU|gAL$kauH#vg z^8H!F7C`6+ClR@5DzyoEy&k=8L2EguQQt<8G)Xf@n1qC(q8df0AcR0sddV@)VX!!7 z5SOEDN}vsxE?H}VmtH-_e5cJVx81_b%r@rEpJA#Rt&hYHK#_HFunAFIhmNp3uQ_%0 zRdyVJ9S62ib9J1nV_kv}69nZN6nNI)6gY=7-fNTF+|!Bp{DD^5MpTu;IwmSmSi!g! zN(3d!S7VjyDXu`t!dT$iVaw`cTQ0k%Z(|wdFlGhgR=~D#?(ApF?b-b;ldWLWMFLx( zosXE{audy*eDOd0HAi1qx-+LkB)S*ReHS+?M{ncFQwguN(H8-8vMkE-^B;syOW7#o!oUjCE8L; zb`>B8dL@e-M*G!BD&=T6&AEc2NHOUuDrt zB&|S!3kUHpBTZgKJ8pqs8^>Nb#S@R7;N9<9Vb`uYF7J#CZhgEjtkod(Z2k1< zi+u0FMS2BHZo3^9?&F((@fUpk8&7lW1nl0E&|1xKR+nS6sa&x|B`6{^1(ee~X%(!4Wm zFzvp8$WXNC77J)q;1EPcOPhyC9LpgYcxn2`i|Do$^~pWJ1TQ>2%jdrM4gTu;ZQg$5 z0Kfk0{~Zr}{}I0SwXe}jS4qvt=7_x_bJI-+x$%Y@ID76ayLatp_x9^pURHRskx&sse>79abs0Hr1+wY?;8VUN}Pc6|7xCWCbT>PnQ0TE*A|S z-7&UGq?^#L@#Lc?`PaYy6^@>BeDtF~#XtGSzsCDN^kH5;HcM&?=2tA$#<=I+0aM$y zGr4^yQ#*EX-+e#M`|i4j#>52VtNi7+zQjNOU;Y_S{$P;@o_LvkGc(L=ud%$?W_fuL zOqp2KkCqQg)W+vvOEzXvm;fes>>`@j!gs&*m%MTse)?yAg8%xz`5(Fdx|^s>#dMbD z_`To%cf54$HD+heA)*jbIMEO!L`g_y1Ds9>laTk_^I>kg?KZZ|Y~$bmoBy3Z`Lox! zu<%Fx#wS0{E!S@)>lI|J7S(D3u8i`G)kF?is_)3*iN&{8afnSJZ-HGxk@cSJwO7dV z6&ziEv64YEolAmKh>Z}NfT@FNl64w<;opCY-~OMz%#%+$e)$*vTmIMI{x`h;$36r} z0?uIaf&iFqZbw-`S?{qE2M;~)Fu(VI{ULw$XWyhzZxB`k2y&>!-20KA;J%;y8CpH~ z*0&#HZaJq~OAsK-7J)9JJPBF?AAuP0gN`Lo;WbmKfYcoqT4(v*qYpE=3-0;JALpjq z4pZ5_9aKS52?(_#%UUQbA{nZzIPSJPTsXVTcfa#pzWI%ZSY7VWT5OZlr}*Tr|5N_? zzx=n{dgMm_^It6TFaE_Bc;d0w2y_E$3!g0HBC!1|2UU)*0M!H4!-~bIei?e{i*-4s z*CK5%qVn!VT(O99Z34;B19Bb;PjQ>Pu!yszNfBXPmB*g^0bl>tE2O|b{mozFH-Gcf zY}>vY8ckSjG1;s#)to>JEM1r@?g31S)o#P!m(5Odb zc|LZ_I1K5jj7Ajk^g|C(uhr3k5Qdr{2%zWKI<EQRxeTq$bR2~07^!o1?-D+Q!59q;DU%O}ZJ40$i7(_Tdf!tAM8pLA6A z$}@S6$#X)1JcmGKG$ZJ(%=3kR_ix#{WeYk`pdE7;&v4<~ELj`o=L^zqL7z7$OU#v&q5SNm7W{K+s0jh;{9WXi0^ds2}JQ;GST=PLnozI}Q z%5wXWn|aSYPx9zfIgdQ`5Jz8lh{ES6VJ!rKVA_t@f~4Jb)mP#QY67z_zrg?YKYz-b z7NW|8A|cB%=I2)-gBPE9g0rVu9Jwu_TB$*|Q${Z=O6*8+O&@%TH`XD}Ff}znq~X|e z&(N9QL2nN3|He0X`RPZAqR2<*+ik)yWO;d+GiMBO#Mp*T63^bM2_z2YPM_d=^C#$Z zylN0Pd{o}FVaHB*|NF1!;B`C6iv^Nq1&XrC(b&57snMaF*L1ZlGvKVjmyz}m+5{Mb zwFaz3tU+IGDc4BqxT3@KRLswR?48_xLV|l6N{f$nr&SH(F=is`-6+#uTeD-NN9k}ks8D_RO!Dbjt>6$BiW~1|pm<$$1 zVN5|#L^__ELvXC4@nU(uz*I>!a2p zSZgUvF>p%(^m^FT+(%-#^dTHXU>1mT5XG3*7zAo$TNLUbObX=IN;^y4^PQW(6g#boXV{D)IOV zh@;30k|d$m>rqJ}niCV`X^KRerJ_eC?=o4hql%1r#bLT<5#5Gf51&tal|%zR!~}?2 z`ifgV{{njT2`b?t6c$l3U_Y{PLBntt4BV;Z?xwH7`p-1p2iosHwl=qO9MnjCU3t$z z5lH!1=dj{aBVKssMJ`@waqF$O(3}DlcCf5~bUpH%LTtIOja%L8M|b)1lIsBuov+3OX?Oh+<#; zF8qcOUzgxY_9=ssE*}%LWCV>6ldfR$GL@nX*jk?;3Jj^ujm%xGQ$B1zXgGk-dIb8Y zLR2b+k#|-0E3-z|bM$s)NNo(($v}o$m2%}C>Tj@15eqQbaRVfdyr+D4zopjLHJ5+^ z1&;h~4px&>hc2BYC;rhvNXq0 z_#9R{N==>%jIOhVk#50k;6V3JQ2h;s!u2<(3(A{asyyxT5FpAc2G(NpV$f51J@T%= zsSWhU=V7>D{UXWBvW&h>Ihp}Hn%m;QWxioo6a++C`Dm+vSkIbp;cz^n%FvCB?A}o) zg`=9pU|s2Avt!R4WNU9S+_fr#phnt<`R6z>fGJt=xT9pCN^C+b1D!z1d`t21(f(^d zfurNt_-3P;l3nw;UGx2=u6;dlx$7-%U<(f&!~O5ZU|pHm`pJ$eQnEIc{0hJge8{6? z@GKiy!wKHt+FYFs9P6S~g2?dwrnP;qpG8~_`b+KBydS6JD<8+W7W@WaU2Xh`C>hfm z%Ac3If`^gy++;ZYjdkS+TxV?dA0p%L*IhGIWL_^gH>^6k)B27mVdPtb{0PBDHsiV)!8QM{CB^aj`1P=UL%gvy7>Xay zLfxQam3LTJYsfQ4sH?tiGRPnlz+igqlCm5lJ9hj5MFC}vq6&kJN4w!<*Qxb=zTSTo z!_NlajUU4Cdcj%iU1{rln?L$6F!akPh=`_=OriA_q%woayjRF&J+iFt*e^{N8BvmC z_!?b@bu6^>Wp6T?jbw>z_~$tMXi@fC2Nsl15$x9~YBd42$>Wc_#)%Whc-uR-ap1rt zIti!*6-?$&jOj0>DSL3N6JVvi8DL^85l$%|wqTqB$63NAaH**Mz&?CqNL1^zl&ikO z2A{pz6z!wb&KRGP=^~7&arEiq{Kqdoz``6fCc5n1+dxZ5uiYXHMrq2gofV4{@l%%o zYc+}(OiX~;g3{CEQ4LcD4Fxx_f3;hi2gBF{ENm`d0Oi)|iB|?=rp+3ga2z&i2wVPc zkX5juuW>_JamjJ5UyHS7%G&QWrhuS6#mXw2pHuvYKYoBGkHQ^?Vc*_^gGAq?R>P#; z_2``KJGFc+OyBPs1(-Y{R$IVrB|z12x(2$6(=pbh=&~O0(lm4JY?&V#xXb1Iyiri# z1W^fj7Srkq|LIGA#os*Q>q&m`mp;nweF_4DP1{(TuT!d|e;+9GT?0f_Ae;t0jUtHA zK@}bNOyqt$VC`6=m8vzbzk@3RtI$!MZZF{3XJ6x+e|DZq2p|6On|b$p_tV_c06fKs za_csf7?6>}M{MChMG({>m_Q+kKVD7hD5>}?VQWyvp{%B><1^+H&`lJ1GNq#?c@d*j zt-RQlnPPuOX_o^#s_L%+M?g@ShF}Zd`~LGh_|Pgh+yEc@x%=2YqX@zlN*UjMqcFHq z{sCp`VnFC3ba@aLVzI_yRg85NNM=wnHMQh)g6O*EtpK)6V6kgG>wgDV1y13p^2{?Q zdHk`Hv{&G5M{eMz8@G}~U0MsXT&kO+uj*F*RR)!&;0EBnA)0=b(Ux}>Kn8#RpSoy zGLxZCNJ@q1FhNC4Wa9ermn#0O$OPy~3L^Zel?<)_HyZ|BZP&c61q?hzo4nR~kFIE( zzIX?lU~L_1>pnTxMP(ts!a7Z%B2Ju|=i!Hs((S@M??1wgH_u@61uByhKtFwB#EHFO ze&{eXW>t&?6G+?y`T^Z&<|#M-6t;bVOqy8LB0%WN!b^oz-l&pe%an;t?|)fi`*k`P z2H$VAc%Eg+NTkIY-yy1hthtgHq(YI$oSmKJy8Y8Y4@bvm)`cNab&7`{eUcYm$hh?| z+;!JpI!h-BrH3jM5K*f8{;^Jtn5N=YP@VfRDubO zjhwReka`mpnag&hzlM~!8T6BC2d9q>^Jknhq-mGj6sS-UR1$Ph!BNGS2G&e-=2XEK z|Knfr^{@Rk1T`eAk{24M>J+Zd^Dmqv&*AXlt=xRWR+MBTC!aCG3D%fwqfKJVd?G`F zinl>Hg;ViY8M`W~ep-@glE!YFOi);bQe}DaCa)h2SZkk-yxb+e8|10P;1Zu5X6pWL z7vRL=R6){+(UFjuF6~Z>b}uDt&fx4M&pmU2-}~LK@Zf_5=g%#$aP}+&6w2EU{e`-g|7|TDT6#l8Fcl|E%&Z4iYaNrAA(kt@>IgO!8?j|AL zh6<(-+4VVC8;yvXuXPsc`zwtDi7kV#$NR*n>L()0+68!fTyWGP$A&g^)V#xbVU1LIOymow%lc$bz>g0KjKJ^kmc(KFs5=>9RM}J{I zZ-3VT?z{IEs&Sjn;yJ3#8Pc|8c6O14g^ZbVBAod6{XD8bq;=~9X*5*Ft>oSR){egPhS;w6qg^D1Y~_$s7*2jG1lsB`a!Z|5C% z9ilnqNZThd)=-_SK?bdrj8|Sd$?RFU{T*=V=4p~zj7eLdveHRX1r%0is50fyr*#fF zjAykD(E&Q1BB*XBZ0=gtk!PQN25Uga&41;(TRv5=mB4gBRuMOBeQ_4` z9kqIeN-Y_Si-=w?%JVYY(uv!pOu%o(kszI;#OsK0eET`r-@x%CFqRkA3t5 zxXyWy44J4P`KW&oT$Zo7heagtd6K&PQD2U@vP+K>Z)n>pt1ZXkT#wl^i_9;~bL`kj zPMti%?CAwM9avcQO?)PruxBrP03Q6i)hwo0RTL7nNxd3L`NlY?8}1s?9w} zGKRa25h&5jQ?7HIpm~5=o>DA#2w5#p;Jm~sEUyP#KyE9%@bVm|&cheJ_+7s9z_Ub2 zfz=(dte`PbAFxO(P_I|UZV`kbf!4l#a}Z+8c)?+-wMx6)WofZPXQklmtfSX~P8)h% z=w=W_9$}M{aQi!_IB;M$H{Wstdv;7SHK9mCgVF_-4kD><#zO`agX0T^9TgSl=P$C_ zf~{L%x-o^05-P5ZN!vqaG3Es-{hcHBnG@fiRRxm}Y)2|veF$j{Y@Al8<3A*B{;Hmw zq1TB3i&kBr1GeE=&?QeSSq~d|Y;qK;K80&;r>idV*bDI7YYSwBM^+R;k*|(T%DELk zf9DA;>iZkCe}*dyBKvD(4Z~KE;(ym@!j3)AXbL;`R@kv~g1x(UF)>+V$Br4Mw@i{$ z63A13QIUh`psnvjJ}jSS67HY{ii0Z|Ej7?p_;o$TNj8GgIaA+zUW^1^#NCm$8iu# zySu=i-4i(Ho1uR2LpM@y_|*87b_Y$lVM=Is+Kg_TKxwipquce}4g;+jZBw;natHNl z!qh~CT3u6ZC>qU>ty}6eCWLCUN23`M28uLIsa6a+SjH7A2#_v!2?B-I&R0JVRDSPn zbIuIiU!@mkVSb5r2exjV_GRJS9?m&}AoLZ&>!5V20Z4|?ic~pzREgv5sLD=IO^PD^ z)3sM_h(HOp|0y8Oy>_DFmiDit7s!f>)RM@bwuM(ffU7q3M`fX+$CjFK(;mya4twhO z=zRxy$2)JQa2eKGdfmeNAMq5JWxcWcnQ}R;HNNX1W~5+L(Wo{^!iXf+ICA2O!?_%B zJx@vEu;hp>2oo?xkD>tUG~yJ24m>40?-fxh!8@USs=Sj`z!#&&jRqG^wV1oO0Kne8 zy9vX;5;rS6LL6=!vrmQe_ZrB_}5Jb9&>*6qo%`ioQNP#0KaJEFIcT_uv51!)Xk#hJV z&ymdXp%y&!m-^v(R@iB$v|4TA7$&FcKK*ZS5Xi=?YFH#{F{;4o1fm&~-iOq#L*hf5 zT5X#maM8H~z)~n8`Gp4^y?M-PhIVtHGOYBQ0a11W>?3GRe*Z?$pNF$J=SZR|w;jF_ zC6%)D_&jO8jI}vIRN6~}yAGe_lpv;GMphm~=NOb>0~C2amd&k|KRd$bClrV)4QnfC zl()lN;5#Nsj?e=Noa#4@9nHBc5Ha+r2?C13(Q0}5U2jxTS}|VKYRCTWAFqe9SY2SG zhtU(DcT;QJj>I=#(&WAr1ujjAECB@7gP*25_dC7SC6ErH%7cyv1KNlRH_2-oor@@N z22loG6VjCh;tDv^CQf`htF@`OkJM;>)-c57Y}wdnaFkA|6qE}cgnh4nxej?&Mx*_r z_1%P0*7}!Q(ifzbeL9DBsTx6`8qRU#EWjjExS~{;TpM4Jw1EvReJ6Hl?<$26Mb0ET z*oO+QC#oLUU|q*yQv~*r2O#wwpL2`He<#7vYqcQnppEw+s9^Mrcr_y6S;I2a#s@={ zaYx(&aa{ogqWe2q4a?$6Q(_RC98j_UPY)=hua1`stVTCjsfxNjJ5*&~I2l|kWm&8% z`=5BqQzI^@(FrK96_AiH46*GMYH`lNJzmo6+y+{;aYff33uHl*;3%nH%Cr>}P6cFc z3KiZ+9KW5odE}C;vkzBXog`BS#VtSM0(*?SwL+d>Byc%u><)dZ>+e=O=H~LfzSi7# zQ2v#zLGBfJS2i7N|2jMJzWhx3O2c65Y?!(}{#?bz<4~mm*xx(_Q}nvAVO{RoHY1%tq)b^UV|}@cF|w6v?RLWIZAk6bEAm!eU439wCrA!Kk!NhB@~@bn`*Uz zQi^saWR26DI*c)&8tHmyiNp4W$J{> zFW!eOzC-UUDqEoFERYwDD7rLPUVqkYsS#f80QUi9z);H_ebz?LwLT*8W)!e&+7DnP zo~I0mH*;L6Z7QX(29!ipYctr!?daMaNaN~}2X8>jkZ{}mBzymwJ-r;Z`Q z5qN{z;gf?#5@3h0nf-W$aM>X4nmm_lnQOY)ct-9o;>*Fr4$luZCEHvrDiqO9f~i}G znl}=>PUOKG>C^;Bxc6R{p8YJr-p>%eNRYft-hP?D_R#ovJP) z$XUBtwclWeukEI2)yA@_4{G2gp?t>p6Vy7P+Lvv^J8X3S{$})MqZ*nf&NnrXer9{= zCX-OeT|iOYf>ifG{T68449S7FZ8qgAfoql-VHclpdHV(2>QQX>H6n8ntuhJ~`jeVV zuY(?hdi+9K8C~4VQ>2y=_9=GEjkgi3&8%CCz$@_rqBQ5&%LbIYt!_TdZo5)Q%wPIwaVGRQ4c~hp*|H zzo}&4+AljUJVdwk9F|#vU=i{q0#~325gQEB=>AqB&?)GLvlQeQy3S$30Ew%hcT#J< zhcLPxl3lM!^}vBQ_VD#^OyH-LKoy(QAi!)?Sz$ssBkY~{T9&Mzbf(GkG~m1cvBGwpqsZzR-9N0vSdO!$8oL( z#YIfoLzCfx6?}%52On`7+KA&yyw8=y4LkT=PMKMI_S1M?>3I07?}*YyP%`SMJ-qT6oJeiC%H){Gt93Re(1)S>z=xw z&Y2J7=^0Eq3)wkzv4AU<(I!O~*+}Ht4!r#SluQ5j>#8;#*lStY5_#CeQ$}jxjd;Z6 zl8{p;s_#IeX`<>B63!4*t^+r5pH#Npf34Q!YNzo-2j?yvq1*dB?WH?WUC0)ocOLS2 z+N~2P+9+iRR6$m>aW3;oC(e*)?Gf4U7QfcFX~wCuFr_m=N)tpIR1n~l24X})jMG@7 z2%`xsjZ(&KLBbiFoS8h!MNRL)*#SYQ?~N(dG=PoDh(22RY(+1 zp>ze&F;;5iu1Qkej&AJm{mE?O=gY+BG05D7BjFDLi9eDETnlOEnCrCX>9sGAcFt3Y z3u=j_8Wu=z35pJ`Fj#9@Tj6T$`uK)Fc02$TLp%i;w0o9L>TtSBQrk*%VjDyasP6cr z$eXWCzFzoSfxuD7(ia?N8JjPm@+DY32U&}}$jS49V$E@OW=(`ciJ${b6h^3^N~N|N sbOWL~1XYhvRRNuRQi2~{#P#9-1O9BTZSN-6p8x;=07*qoM6N<$f}gY-s{jB1 diff --git a/data/looking respectfully.jpg b/data/looking respectfully.jpg deleted file mode 100644 index d7a9c738ab9ba055d128c5cde416339e10227b00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82919 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

ZV`D4rdc*sMxMA$KbS_au-T}@;eB-a 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 Date: Sat, 14 Dec 2024 19:01:09 -0800 Subject: [PATCH 13/20] test(image_converter): fix tests for new conversion parameter Updated test cases in `test_image_converter.py` to accommodate the additional format parameter in the `ImageConverter` class and `convert_image` function. This change ensures that the correct output format is specified during conversion, preventing unnecessary file creation when input and output formats are identical. Adjusted assertions to reflect these changes, particularly ensuring no new file is created when converting an image to the same format. --- tests/test_image_converter.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/test_image_converter.py b/tests/test_image_converter.py index 6a7a5fa..0445cbc 100644 --- a/tests/test_image_converter.py +++ b/tests/test_image_converter.py @@ -23,37 +23,37 @@ def tearDown(self): shutil.rmtree(self.test_dir) def test_convert_jpg_to_png(self): - converter = ImageConverter(self.input_jpg, self.output_png) + converter = ImageConverter(self.input_jpg, self.output_png, 'png') converter.convert() self.assertTrue(os.path.exists(self.output_png)) self.assertEqual(Image.open(self.output_png).format, 'PNG') def test_convert_jpg_to_jpg(self): - converter = ImageConverter(self.input_jpg, self.output_jpg) + converter = ImageConverter(self.input_jpg, self.output_jpg, 'jpg') converter.convert() - self.assertTrue(os.path.exists(self.output_jpg)) - self.assertNotEqual(self.input_jpg, self.output_jpg) + 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') open(invalid_input, 'w').close() with self.assertRaises(ValueError): - converter = ImageConverter(invalid_input, self.output_png) + 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) + converter = ImageConverter(self.input_jpg, invalid_output, 'gif') converter.convert() - def test_same_input_output_path(self): - with self.assertRaises(ValueError): - converter = ImageConverter(self.input_jpg, self.input_jpg) - 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) + convert_image(self.input_jpg, self.output_png, 'png') self.assertTrue(os.path.exists(self.output_png)) self.assertEqual(Image.open(self.output_png).format, 'PNG') From 01fe536a6c2abc4a3edef5fbbdbbf62c93453e6d Mon Sep 17 00:00:00 2001 From: Daethyra <109057945+Daethyra@users.noreply.github.com> Date: Sat, 14 Dec 2024 19:02:15 -0800 Subject: [PATCH 14/20] refactor(tests): use context managers for file and image handling Updated test_image_converter.py to utilize context managers when opening files and images. This change ensures that resources are properly managed and closed, reducing the risk of resource leaks. The modifications include using 'with' statements for creating and opening images and files, enhancing code readability and reliability. --- tests/test_image_converter.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/test_image_converter.py b/tests/test_image_converter.py index 0445cbc..db0e290 100644 --- a/tests/test_image_converter.py +++ b/tests/test_image_converter.py @@ -17,7 +17,8 @@ def setUp(self): self.output_jpg = os.path.join(self.test_dir, 'test_output.jpg') # Create a test JPEG image - Image.new('RGB', (100, 100), color='red').save(self.input_jpg) + with Image.new('RGB', (100, 100), color='red') as img: + img.save(self.input_jpg) def tearDown(self): shutil.rmtree(self.test_dir) @@ -26,7 +27,8 @@ 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)) - self.assertEqual(Image.open(self.output_png).format, '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') @@ -35,7 +37,8 @@ def test_convert_jpg_to_jpg(self): def test_unsupported_input_format(self): invalid_input = os.path.join(self.test_dir, 'test.txt') - open(invalid_input, 'w').close() + 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() @@ -55,7 +58,8 @@ def test_same_input_output_format(self): def test_convert_image_function(self): convert_image(self.input_jpg, self.output_png, 'png') self.assertTrue(os.path.exists(self.output_png)) - self.assertEqual(Image.open(self.output_png).format, '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() From aeaf9cf7122d86e0aa4d3a9c62a9288bf65467f5 Mon Sep 17 00:00:00 2001 From: Daethyra <109057945+Daethyra@users.noreply.github.com> Date: Sat, 14 Dec 2024 19:48:21 -0800 Subject: [PATCH 15/20] chore(project): migrate from Poetry to PDM Migrated the project from using Poetry to PDM for package management. This involved updating the configuration files: replacing `pyproject.toml` with PDM-specific settings, adding a new `pdm.lock` file, and removing the old `poetry.lock`. The `.gitignore` was updated to include `.pdm-python`. The project name was changed from "image-type-converter" to "Formaverter", and the version was bumped to 2.0.0. Dependencies were adjusted to align with PDM's format, maintaining compatibility with Python 3.13 and Pillow 11.0.0. This change aims to streamline the build process and leverage PDM's features. --- .gitignore | 2 +- pdm.lock | 40 +++++++++++++++++++ poetry.lock | 98 ----------------------------------------------- pyproject.toml | 33 +++++++++------- tests/__init__.py | 0 5 files changed, 61 insertions(+), 112 deletions(-) create mode 100644 pdm.lock delete mode 100644 poetry.lock create mode 100644 tests/__init__.py 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/pdm.lock b/pdm.lock new file mode 100644 index 0000000..c6f91e4 --- /dev/null +++ b/pdm.lock @@ -0,0 +1,40 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default"] +strategy = ["inherit_metadata"] +lock_version = "4.5.0" +content_hash = "sha256:94118d9f507db27efc23611be5dd2f6f758590f573ac540c2e6b03a4c405c3ac" + +[[metadata.targets]] +requires_python = "~=3.13" + +[[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"}, +] diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index a200750..0000000 --- a/poetry.lock +++ /dev/null @@ -1,98 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. - -[[package]] -name = "pillow" -version = "11.0.0" -description = "Python Imaging Library (Fork)" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pillow-11.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947"}, - {file = "pillow-11.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba"}, - {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086"}, - {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9"}, - {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488"}, - {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f"}, - {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb"}, - {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97"}, - {file = "pillow-11.0.0-cp310-cp310-win32.whl", hash = "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50"}, - {file = "pillow-11.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c"}, - {file = "pillow-11.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1"}, - {file = "pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc"}, - {file = "pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a"}, - {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3"}, - {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5"}, - {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b"}, - {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa"}, - {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306"}, - {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9"}, - {file = "pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5"}, - {file = "pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291"}, - {file = "pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9"}, - {file = "pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923"}, - {file = "pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903"}, - {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4"}, - {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f"}, - {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9"}, - {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7"}, - {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6"}, - {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc"}, - {file = "pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6"}, - {file = "pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47"}, - {file = "pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25"}, - {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-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba"}, - {file = "pillow-11.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a"}, - {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916"}, - {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d"}, - {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7"}, - {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e"}, - {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f"}, - {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae"}, - {file = "pillow-11.0.0-cp39-cp39-win32.whl", hash = "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4"}, - {file = "pillow-11.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd"}, - {file = "pillow-11.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316"}, - {file = "pillow-11.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06"}, - {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273"}, - {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790"}, - {file = "pillow-11.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944"}, - {file = "pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739"}, -] - -[package.extras] -docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] -fpx = ["olefile"] -mic = ["olefile"] -tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] -typing = ["typing-extensions"] -xmp = ["defusedxml"] - -[metadata] -lock-version = "2.0" -python-versions = "^3.13" -content-hash = "9dd06d93b71f8db5159cc6afa9e229be38b7bcee1707e571f1c81145cc58ef8b" diff --git a/pyproject.toml b/pyproject.toml index 2e970f5..ad4a09e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,16 +1,23 @@ -[tool.poetry] -name = "image-type-converter" -version = "0.3.0" -description = "Converts images from one format to another using Pillow." -authors = ["Daethyra <109057945+Daethyra@users.noreply.github.com>"] -license = "GNU Aferro GPL" -readme = "README.md" +[tool.pdm] +distribution = true -[tool.poetry.dependencies] -python = "^3.13" -pillow = "^11.0.0" +[tool.pdm.build] +includes = [] +[build-system] +requires = ["pdm-backend"] +build-backend = "pdm.backend" -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +[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"} diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 From 97c6107882f0f5b6ff3f63e24b572cb36ea24e83 Mon Sep 17 00:00:00 2001 From: Daethyra <109057945+Daethyra@users.noreply.github.com> Date: Sat, 14 Dec 2024 19:56:39 -0800 Subject: [PATCH 16/20] refactor(structure): reorganize src directory into formaverter package Moved source files from the 'src' directory to a new 'formaverter' package to improve project organization and modularity. Updated import statements in both source and test files to reflect the new package structure. This change enhances maintainability by clearly defining the scope of the formaverter module, making it easier to manage dependencies and future expansions. --- src/{ => formaverter}/__init__.py | 0 src/{ => formaverter}/image_collector.py | 2 +- src/{ => formaverter}/image_converter.py | 0 tests/test_image_collector.py | 4 ++-- tests/test_image_converter.py | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename src/{ => formaverter}/__init__.py (100%) rename src/{ => formaverter}/image_collector.py (95%) rename src/{ => formaverter}/image_converter.py (100%) diff --git a/src/__init__.py b/src/formaverter/__init__.py similarity index 100% rename from src/__init__.py rename to src/formaverter/__init__.py diff --git a/src/image_collector.py b/src/formaverter/image_collector.py similarity index 95% rename from src/image_collector.py rename to src/formaverter/image_collector.py index bdc9286..1c8afca 100644 --- a/src/image_collector.py +++ b/src/formaverter/image_collector.py @@ -4,7 +4,7 @@ import os from typing import List -from src.image_converter import ImageConverter +from .image_converter import ImageConverter def collect_images(input_dir: str) -> List[str]: """ diff --git a/src/image_converter.py b/src/formaverter/image_converter.py similarity index 100% rename from src/image_converter.py rename to src/formaverter/image_converter.py diff --git a/tests/test_image_collector.py b/tests/test_image_collector.py index 896664b..22a9104 100644 --- a/tests/test_image_collector.py +++ b/tests/test_image_collector.py @@ -5,8 +5,8 @@ import shutil sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src'))) -from image_collector import collect_images -from image_converter import ImageConverter +from formaverter.image_collector import collect_images +from formaverter.image_converter import ImageConverter class TestImageCollector(unittest.TestCase): diff --git a/tests/test_image_converter.py b/tests/test_image_converter.py index db0e290..e4e8dfb 100644 --- a/tests/test_image_converter.py +++ b/tests/test_image_converter.py @@ -6,7 +6,7 @@ import tempfile import shutil from PIL import Image -from image_converter import ImageConverter, convert_image +from formaverter.image_converter import ImageConverter, convert_image class TestImageConverter(unittest.TestCase): From ef3ab52ddd7406f2f127febe398c2d432ad3e534 Mon Sep 17 00:00:00 2001 From: Daethyra <109057945+Daethyra@users.noreply.github.com> Date: Sat, 14 Dec 2024 19:56:56 -0800 Subject: [PATCH 17/20] chore(pdm): specify package directory in pyproject.toml Added `package-dir = "src"` to the [tool.pdm] section of pyproject.toml. This change specifies the source directory for packages, aligning with common project structures where source files are organized under a 'src' directory. This adjustment helps ensure that PDM correctly locates and manages the project's packages during build and distribution processes. --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index ad4a09e..5bb39bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,6 @@ [tool.pdm] distribution = true +package-dir = "src" [tool.pdm.build] includes = [] From d2498e2b7f164ad8efd2e3db0cb9cd53b14fdc37 Mon Sep 17 00:00:00 2001 From: Daethyra <109057945+Daethyra@users.noreply.github.com> Date: Sat, 14 Dec 2024 20:15:17 -0800 Subject: [PATCH 18/20] feature(ci): add GitHub Actions workflows for Black and CodeQL This commit introduces two new GitHub Actions workflows to the repository. The first workflow, `black.yml`, is set up to automatically format code using Black on pull requests targeting the main branch. It ensures consistent code style by running Black with color output on the source directory. The second workflow, `codeql.yml`, integrates CodeQL analysis into the CI process. It triggers on pull requests to the main branch and on a weekly schedule. This workflow is configured to analyze Python code, providing security and quality insights through automated code scanning. Additionally, the README has been updated to reflect changes in the project's description, features, installation instructions, usage examples, and development setup. The tool, now named Formaverter, focuses on image format conversion and includes detailed guidance for both command-line use and integration into other Python projects. --- .github/workflows/black.yml | 16 ++++ .github/workflows/codeql.yml | 84 +++++++++++++++++++++ pdm.lock | 99 ++++++++++++++++++++++++- pyproject.toml | 26 ++++--- readme.md | 138 ++++++++++++++++++++++++----------- 5 files changed, 309 insertions(+), 54 deletions(-) create mode 100644 .github/workflows/black.yml create mode 100644 .github/workflows/codeql.yml 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/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..40658a1 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,84 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + #push: + #branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '15 23 * * 3' + +jobs: + analyze: + name: Analyze + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners + # Consider using larger runners for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + # required for all workflows + security-events: write + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] + # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/pdm.lock b/pdm.lock index c6f91e4..58801e7 100644 --- a/pdm.lock +++ b/pdm.lock @@ -2,14 +2,98 @@ # It is not intended for manual editing. [metadata] -groups = ["default"] +groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:94118d9f507db27efc23611be5dd2f6f758590f573ac540c2e6b03a4c405c3ac" +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" @@ -38,3 +122,14 @@ files = [ {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 index 5bb39bc..52f6d9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,3 @@ -[tool.pdm] -distribution = true -package-dir = "src" - -[tool.pdm.build] -includes = [] -[build-system] -requires = ["pdm-backend"] -build-backend = "pdm.backend" - - [project] name = "Formaverter" version = "2.0.0" @@ -22,3 +11,18 @@ dependencies = [ 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 From 9ae989415e938466da2d867af17f9cde9bb6827b Mon Sep 17 00:00:00 2001 From: Daethyra <109057945+Daethyra@users.noreply.github.com> Date: Sat, 14 Dec 2024 20:15:39 -0800 Subject: [PATCH 19/20] refactor(formaverter): Ran black; update import paths and improve code formatting Updated the import paths to reflect the new package structure, changing from 'src' to 'formaverter'. Improved code readability by reformatting long lines and ensuring consistent use of quotes. These changes enhance maintainability and align with Python's PEP 8 style guide. No functional changes were made, so existing functionality should remain unaffected. --- main.py | 28 +++++++++++++----- src/formaverter/__init__.py | 2 +- src/formaverter/image_collector.py | 12 ++++++-- src/formaverter/image_converter.py | 37 ++++++++++++++--------- tests/test_image_collector.py | 19 +++++++----- tests/test_image_converter.py | 47 +++++++++++++++++------------- 6 files changed, 93 insertions(+), 52 deletions(-) diff --git a/main.py b/main.py index fb9cbce..9dd062c 100644 --- a/main.py +++ b/main.py @@ -5,8 +5,9 @@ import os import argparse from typing import List -from src.image_converter import convert_image -from src.image_collector import collect_images +from formaverter.image_converter import convert_image +from formaverter.image_collector import collect_images + def process_images(input_path: str, output_path: str, output_format: str) -> List[str]: """ @@ -30,7 +31,9 @@ def process_images(input_path: str, output_path: str, output_format: str) -> Lis if os.path.isfile(input_path): # Single image processing - output_filename = f"{os.path.splitext(os.path.basename(input_path))[0]}.{output_format}" + 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): @@ -41,7 +44,9 @@ def process_images(input_path: str, output_path: str, output_format: str) -> Lis 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_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) @@ -52,15 +57,22 @@ def process_images(input_path: str, output_path: str, output_format: str) -> Lis return converted_images + if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Convert images to a specified format.") + 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)") + 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) + 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)}") \ No newline at end of file + print(f"Error: {str(e)}") diff --git a/src/formaverter/__init__.py b/src/formaverter/__init__.py index 264707d..e89240c 100644 --- a/src/formaverter/__init__.py +++ b/src/formaverter/__init__.py @@ -1,2 +1,2 @@ from .image_converter import convert_image, ImageConverter -from .image_collector import collect_images \ No newline at end of file +from .image_collector import collect_images diff --git a/src/formaverter/image_collector.py b/src/formaverter/image_collector.py index 1c8afca..5190a20 100644 --- a/src/formaverter/image_collector.py +++ b/src/formaverter/image_collector.py @@ -6,6 +6,7 @@ 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. @@ -22,12 +23,17 @@ def collect_images(input_dir: str) -> List[str]: 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'} + 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: + if ( + os.path.isfile(file_path) + and os.path.splitext(filename)[1].lower() in supported_extensions + ): image_files.append(file_path) - return image_files \ No newline at end of file + return image_files diff --git a/src/formaverter/image_converter.py b/src/formaverter/image_converter.py index c51dc0a..d368840 100644 --- a/src/formaverter/image_converter.py +++ b/src/formaverter/image_converter.py @@ -5,12 +5,13 @@ 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'} + ".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): @@ -37,15 +38,21 @@ def convert(self) -> None: 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()}") + 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()}") + 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)") + print( + f"Skipping conversion for {self.input_path} (already in {self.output_format} format)" + ) return else: self._convert_image(input_ext, output_ext) @@ -64,7 +71,9 @@ def _move_or_error(self) -> None: 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.") + raise ValueError( + f"Input and output paths are the same: {self.input_path}. Please provide a different output path." + ) @staticmethod def supported_conversions() -> str: @@ -74,11 +83,13 @@ def supported_conversions() -> str: 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" + 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: diff --git a/tests/test_image_collector.py b/tests/test_image_collector.py index 22a9104..de8416a 100644 --- a/tests/test_image_collector.py +++ b/tests/test_image_collector.py @@ -3,11 +3,15 @@ import os import tempfile import shutil -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src'))) + +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): @@ -15,11 +19,11 @@ def setUp(self): 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'] + 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() + open(os.path.join(self.test_dir, filename), "a").close() def tearDown(self): # Remove the directory after the test @@ -37,7 +41,7 @@ def test_collect_images(self): 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') + collect_images("/path/to/non/existent/directory") def test_empty_directory(self): # Test if the function returns an empty list for an empty directory @@ -54,5 +58,6 @@ def test_supported_extensions(self): _, ext = os.path.splitext(image) self.assertIn(ext.lower(), supported_extensions) -if __name__ == '__main__': - unittest.main() \ No newline at end of file + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_image_converter.py b/tests/test_image_converter.py index e4e8dfb..dbebc75 100644 --- a/tests/test_image_converter.py +++ b/tests/test_image_converter.py @@ -1,6 +1,9 @@ import sys import os -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src'))) + +sys.path.insert( + 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")) +) import unittest import tempfile @@ -8,58 +11,61 @@ from PIL import Image 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') + 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: + 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 = 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') + self.assertEqual(img.format, "PNG") def test_convert_jpg_to_jpg(self): - converter = ImageConverter(self.input_jpg, self.output_jpg, 'jpg') + 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 + 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: + 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 = ImageConverter(invalid_input, self.output_png, "png") converter.convert() def test_unsupported_conversion(self): - invalid_output = os.path.join(self.test_dir, 'test.gif') + invalid_output = os.path.join(self.test_dir, "test.gif") with self.assertRaises(ValueError): - converter = ImageConverter(self.input_jpg, invalid_output, 'gif') + 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') + 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') + 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') + self.assertEqual(img.format, "PNG") def test_supported_conversions_string(self): supported_str = ImageConverter.supported_conversions() @@ -69,5 +75,6 @@ def test_supported_conversions_string(self): self.assertIn("BMP", supported_str) self.assertIn("WebP", supported_str) -if __name__ == '__main__': - unittest.main() \ No newline at end of file + +if __name__ == "__main__": + unittest.main() From eb12dd9f7cb320d1324a3e06eb58a9c20c20d001 Mon Sep 17 00:00:00 2001 From: Daethyra <109057945+Daethyra@users.noreply.github.com> Date: Sat, 14 Dec 2024 20:38:11 -0800 Subject: [PATCH 20/20] Deleted codeql yaml file as it is set up on GitHub's backend --- .github/workflows/codeql.yml | 84 ------------------------------------ 1 file changed, 84 deletions(-) delete mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 40658a1..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,84 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - #push: - #branches: [ "main" ] - pull_request: - branches: [ "main" ] - schedule: - - cron: '15 23 * * 3' - -jobs: - analyze: - name: Analyze - # Runner size impacts CodeQL analysis time. To learn more, please see: - # - https://gh.io/recommended-hardware-resources-for-running-codeql - # - https://gh.io/supported-runners-and-hardware-resources - # - https://gh.io/using-larger-runners - # Consider using larger runners for possible analysis time improvements. - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} - timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} - permissions: - # required for all workflows - security-events: write - - # only required for workflows in private repositories - actions: read - contents: read - - strategy: - fail-fast: false - matrix: - language: [ 'python' ] - # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] - # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both - # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}"