From ec7e81d32646b5d06f1b29136c228cbd54e21f39 Mon Sep 17 00:00:00 2001 From: Dominik Breksa Date: Mon, 8 Dec 2025 16:34:44 +0100 Subject: [PATCH] feature: added cli improvements --- .github/workflows/cpp_linters.yaml | 68 +- .github/workflows/linters.yaml | 64 +- .gitignore | 994 +++++++++--------- LICENSE | 42 +- README.md | 208 ++-- setup.cfg | 7 +- setup.py | 6 +- src/app/cmd/parser.py | 26 - src/app/cmd/rotate.py | 21 - src/app/cmd/scale.py | 21 - src/app/cmd/translate.py | 21 - src/app/imcli.py | 9 - src/app/img/read.py | 7 - src/{app/fast/foo.c => cpp/fast/foo.cpp} | 166 ++- src/{ => python}/app/__init__.py | 0 .../cmd => python/app/command}/__init__.py | 0 src/python/app/command/io.py | 16 + src/python/app/command/parser.py | 65 ++ src/{app/img => python/app/error}/__init__.py | 0 src/python/app/error/app_exception.py | 2 + .../app/error/invalid_format_exception.py | 5 + .../app/error/unknown_format_exception.py | 10 + .../logs => python/app/image}/__init__.py | 0 src/python/app/image/image.py | 10 + src/python/app/imcli.py | 9 + .../py.typed => python/app/io/__init__.py} | 0 src/python/app/io/bmp.py | 97 ++ src/python/app/io/format_factory.py | 47 + src/python/app/io/format_reader.py | 13 + src/python/app/io/format_writer.py | 11 + src/python/app/operation/__init__.py | 0 src/python/app/operation/bgr2rgb.py | 24 + src/python/app/operation/flip.py | 38 + src/python/app/operation/grayscale.py | 32 + .../app/operation/histogram_equalization.py | 40 + src/python/app/operation/identity.py | 24 + .../app/operation/operation.py} | 54 +- src/python/app/operation/roll.py | 41 + src/python/app/operation/rotate90.py | 30 + src/python/app/py.typed | 0 40 files changed, 1322 insertions(+), 906 deletions(-) delete mode 100644 src/app/cmd/parser.py delete mode 100644 src/app/cmd/rotate.py delete mode 100644 src/app/cmd/scale.py delete mode 100644 src/app/cmd/translate.py delete mode 100644 src/app/imcli.py delete mode 100644 src/app/img/read.py rename src/{app/fast/foo.c => cpp/fast/foo.cpp} (60%) rename src/{ => python}/app/__init__.py (100%) rename src/{app/cmd => python/app/command}/__init__.py (100%) create mode 100644 src/python/app/command/io.py create mode 100644 src/python/app/command/parser.py rename src/{app/img => python/app/error}/__init__.py (100%) create mode 100644 src/python/app/error/app_exception.py create mode 100644 src/python/app/error/invalid_format_exception.py create mode 100644 src/python/app/error/unknown_format_exception.py rename src/{app/logs => python/app/image}/__init__.py (100%) create mode 100644 src/python/app/image/image.py create mode 100644 src/python/app/imcli.py rename src/{app/py.typed => python/app/io/__init__.py} (100%) create mode 100644 src/python/app/io/bmp.py create mode 100644 src/python/app/io/format_factory.py create mode 100644 src/python/app/io/format_reader.py create mode 100644 src/python/app/io/format_writer.py create mode 100644 src/python/app/operation/__init__.py create mode 100644 src/python/app/operation/bgr2rgb.py create mode 100644 src/python/app/operation/flip.py create mode 100644 src/python/app/operation/grayscale.py create mode 100644 src/python/app/operation/histogram_equalization.py create mode 100644 src/python/app/operation/identity.py rename src/{app/cmd/cmd_command.py => python/app/operation/operation.py} (56%) create mode 100644 src/python/app/operation/roll.py create mode 100644 src/python/app/operation/rotate90.py create mode 100644 src/python/app/py.typed diff --git a/.github/workflows/cpp_linters.yaml b/.github/workflows/cpp_linters.yaml index 20cd5f6..172d368 100644 --- a/.github/workflows/cpp_linters.yaml +++ b/.github/workflows/cpp_linters.yaml @@ -1,34 +1,34 @@ -name: CPPLinters - -on: - push: - branches: - - '**' - pull_request: - branches: - - '**' - -jobs: - cpplinter: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Python Setup - uses: actions/setup-python@v3 - with: - python-version: 3.13 - - - name: Install libraries - run: make python-install-editable - - - name: Install pipx - run: | - sudo apt update - sudo apt install pipx - pipx ensurepath - sudo pipx ensurepath --global - - name: Install cpplint - run: pipx install cpplint - - name: Run cpplint - run: cpplint --filter=-whitespace/line_length,-whitespace/parens ./src/app/fast/* +name: CPPLinters + +on: + push: + branches: + - '**' + pull_request: + branches: + - '**' + +jobs: + cpplinter: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Python Setup + uses: actions/setup-python@v3 + with: + python-version: 3.13 + + - name: Install libraries + run: make python-install-editable + + - name: Install pipx + run: | + sudo apt update + sudo apt install pipx + pipx ensurepath + sudo pipx ensurepath --global + - name: Install cpplint + run: pipx install cpplint + - name: Run cpplint + run: cpplint --filter=-whitespace/line_length,-whitespace/parens ./src/app/fast/* diff --git a/.github/workflows/linters.yaml b/.github/workflows/linters.yaml index 29e355c..a283504 100644 --- a/.github/workflows/linters.yaml +++ b/.github/workflows/linters.yaml @@ -1,32 +1,32 @@ -name: Linters - -on: - push: - branches: - - '**' - pull_request: - branches: - - '**' - -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Python Setup - uses: actions/setup-python@v3 - with: - python-version: 3.13 - - - name: Install libraries - run: make python-install-editable - - - name: mypy - run: mypy ./src - - - name: ruff - run: ruff check . - - - name: flake8 - run: flake8 . +name: Linters + +on: + push: + branches: + - '**' + pull_request: + branches: + - '**' + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Python Setup + uses: actions/setup-python@v3 + with: + python-version: 3.13 + + - name: Install libraries + run: make python-install-editable + + - name: mypy + run: mypy ./src + + - name: ruff + run: ruff check . + + - name: flake8 + run: flake8 . diff --git a/.gitignore b/.gitignore index efac517..0827a7b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,497 +1,497 @@ -# Created by https://www.toptal.com/developers/gitignore/api/cmake,c++,c,python,jupyternotebooks,venv,visualstudiocode,pycharm,clion -# Edit at https://www.toptal.com/developers/gitignore?templates=cmake,c++,c,python,jupyternotebooks,venv,visualstudiocode,pycharm,clion - -### C ### -# Prerequisites -*.d - -# Object files -*.o -*.ko -*.obj -*.elf - -# Linker output -*.ilk -*.map -*.exp - -# Precompiled Headers -*.gch -*.pch - -# Libraries -*.lib -*.a -*.la -*.lo - -# Shared objects (inc. Windows DLLs) -*.dll -*.so -*.so.* -*.dylib - -# Executables -*.exe -*.out -*.app -*.i*86 -*.x86_64 -*.hex - -# Debug files -*.dSYM/ -*.su -*.idb -*.pdb - -# Kernel Module Compile Results -*.mod* -*.cmd -.tmp_versions/ -modules.order -Module.symvers -Mkfile.old -dkms.conf - -### C++ ### -# Prerequisites - -# Compiled Object files -*.slo - -# Precompiled Headers - -# Compiled Dynamic libraries - -# Fortran module files -*.mod -*.smod - -# Compiled Static libraries -*.lai - -# Executables - -### CLion ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# AWS User-specific -.idea/**/aws.xml - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# SonarLint plugin -.idea/sonarlint/ - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -### CLion Patch ### -# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 - -# *.iml -# modules.xml -# .idea/misc.xml -# *.ipr - -# Sonarlint plugin -# https://plugins.jetbrains.com/plugin/7973-sonarlint -.idea/**/sonarlint/ - -# SonarQube Plugin -# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin -.idea/**/sonarIssues.xml - -# Markdown Navigator plugin -# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced -.idea/**/markdown-navigator.xml -.idea/**/markdown-navigator-enh.xml -.idea/**/markdown-navigator/ - -# Cache file creation bug -# See https://youtrack.jetbrains.com/issue/JBR-2257 -.idea/$CACHE_FILE$ - -# CodeStream plugin -# https://plugins.jetbrains.com/plugin/12206-codestream -.idea/codestream.xml - -# Azure Toolkit for IntelliJ plugin -# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij -.idea/**/azureSettings.xml - -### CMake ### -CMakeLists.txt.user -CMakeCache.txt -CMakeFiles -CMakeScripts -Testing -Makefile -cmake_install.cmake -install_manifest.txt -compile_commands.json -CTestTestfile.cmake -_deps - -### CMake Patch ### -CMakeUserPresets.json - -# External projects -*-prefix/ - -### JupyterNotebooks ### -# gitignore template for Jupyter Notebooks -# website: http://jupyter.org/ - -.ipynb_checkpoints -*/.ipynb_checkpoints/* - -# IPython -profile_default/ -ipython_config.py - -# Remove previous ipynb_checkpoints -# git rm -r .ipynb_checkpoints/ - -### PyCharm ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff - -# AWS User-specific - -# Generated files - -# Sensitive or high-churn files - -# Gradle - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake - -# Mongo Explorer plugin - -# File-based project format - -# IntelliJ - -# mpeltonen/sbt-idea plugin - -# JIRA plugin - -# Cursive Clojure plugin - -# SonarLint plugin - -# Crashlytics plugin (for Android Studio and IntelliJ) - -# Editor-based Rest Client - -# Android studio 3.1+ serialized cache file - -### PyCharm Patch ### -# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 - -# *.iml -# modules.xml -# .idea/misc.xml -# *.ipr - -# Sonarlint plugin -# https://plugins.jetbrains.com/plugin/7973-sonarlint - -# SonarQube Plugin -# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin - -# Markdown Navigator plugin -# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced - -# Cache file creation bug -# See https://youtrack.jetbrains.com/issue/JBR-2257 - -# CodeStream plugin -# https://plugins.jetbrains.com/plugin/12206-codestream - -# Azure Toolkit for IntelliJ plugin -# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij - -### Python ### -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook - -# IPython - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -### Python Patch ### -# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration -poetry.toml - -# ruff -.ruff_cache/ - -# LSP config files -pyrightconfig.json - -### venv ### -# Virtualenv -# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ -[Bb]in -[Ii]nclude -[Ll]ib -[Ll]ib64 -[Ll]ocal -[Ss]cripts -pyvenv.cfg -pip-selfcheck.json - -### VisualStudioCode ### -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -!.vscode/*.code-snippets - -# Local History for Visual Studio Code -.history/ - -# Built Visual Studio Code Extensions -*.vsix - -### VisualStudioCode Patch ### -# Ignore all local history of files -.history -.ionide - -# End of https://www.toptal.com/developers/gitignore/api/cmake,c++,c,python,jupyternotebooks,venv,visualstudiocode,pycharm,clion +# Created by https://www.toptal.com/developers/gitignore/api/cmake,c++,c,python,jupyternotebooks,venv,visualstudiocode,pycharm,clion +# Edit at https://www.toptal.com/developers/gitignore?templates=cmake,c++,c,python,jupyternotebooks,venv,visualstudiocode,pycharm,clion + +### C ### +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +### C++ ### +# Prerequisites + +# Compiled Object files +*.slo + +# Precompiled Headers + +# Compiled Dynamic libraries + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai + +# Executables + +### CLion ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### CLion Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### CMake ### +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps + +### CMake Patch ### +CMakeUserPresets.json + +# External projects +*-prefix/ + +### JupyterNotebooks ### +# gitignore template for Jupyter Notebooks +# website: http://jupyter.org/ + +.ipynb_checkpoints +*/.ipynb_checkpoints/* + +# IPython +profile_default/ +ipython_config.py + +# Remove previous ipynb_checkpoints +# git rm -r .ipynb_checkpoints/ + +### PyCharm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff + +# AWS User-specific + +# Generated files + +# Sensitive or high-churn files + +# Gradle + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake + +# Mongo Explorer plugin + +# File-based project format + +# IntelliJ + +# mpeltonen/sbt-idea plugin + +# JIRA plugin + +# Cursive Clojure plugin + +# SonarLint plugin + +# Crashlytics plugin (for Android Studio and IntelliJ) + +# Editor-based Rest Client + +# Android studio 3.1+ serialized cache file + +### PyCharm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook + +# IPython + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### venv ### +# Virtualenv +# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ +[Bb]in +[Ii]nclude +[Ll]ib +[Ll]ib64 +[Ll]ocal +[Ss]cripts +pyvenv.cfg +pip-selfcheck.json + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/cmake,c++,c,python,jupyternotebooks,venv,visualstudiocode,pycharm,clion diff --git a/LICENSE b/LICENSE index b959a39..f6c5ebe 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2025 Dominik Breksa - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2025 Dominik Breksa + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 7a859e0..4940244 100644 --- a/README.md +++ b/README.md @@ -1,104 +1,104 @@ -# CPython-based Extension for Image Manipulation - - -* [CPython-based Extension for Image Manipulation](#cpython-based-extension-for-image-manipulation) - * [Team members](#team-members) - * [Project Overview](#project-overview) - * [Used tools and dependencies](#used-tools-and-dependencies) - * [Installation Guide](#installation-guide) - * [Prerequisites](#prerequisites) - * [End user package installation:](#end-user-package-installation) - - * [Dependencies](#dependencies) - - -### Team members - -- Konrad Bodzioch -- Dominik Breksa -- Miłosz Góralczyk - -------------- - -## Project Overview - -This repository contains our efforts for creating a Python based C extension image enhance library. As the first part of the project, a python-compatible library will be implemented, containing various functionalities from the area of Image Processing and Manipulation. - -The calculations are going to be performed within c++ code, with the extension bridging the gap between languages, allowing for full access within the Python environment while utilising the computational speed of low-level language. - -Further on, we plan on displaying the capabilities of developed extension with a python-based User Interface or Pipeline for Image Manipulation and Image Processing, allowing user to utilise the functionalities from the level of a command line or a Graphic Interface Upload-Manipulate-Output tool. - -------------- - -## Used tools and dependencies - -The project contains the `app` python package. Short for Advanced-Python-Programming. - -The package requires the following configuration in order to be properly installed (see later sections): - -- Python 3.13 -- CPython implementation -- numpy 2.3.5 - -### Installation Guide - -The package can be installed with two dependencies bundles: -- baseline package dependencies intended for end user. -- development dependencies used to test, build the project (requires additional installations). - -Our package uses the `setuptools` backend. - -#### Prerequisites - -Create python environment using `venv`: - -```bash -python3 -m venv .venv -``` - -Next, you need to choose one of the dependencies bundles. - -#### End user package installation: - -Run the following commands in your terminal and install the `app` package: - -```bash -pip3 install -e . -``` - -Alternatively previous step can be accomplished using by running the `Makefile` command: - -```bash -make python-install -``` - -#### -Package with development dependencies installation: - -```bash -pip3 install -e .[developement] -``` - -It is not needed to install the package in editable mode (`-e` flag). -However, if one would want to do code modification, the option is very helpful. - -And using makefile: - -```bash -make python-install-editable -``` - -### Dependencies - -See also `setup.cfg` for more details. - -```txt -mypy==1.18.2, -ruff==0.14.6, -flake8==7.3.0, -pylint==4.0.2, -nbqa==1.9.1 -pytest==9.0.1 -``` - -------------- +# CPython-based Extension for Image Manipulation + + +* [CPython-based Extension for Image Manipulation](#cpython-based-extension-for-image-manipulation) + * [Team members](#team-members) + * [Project Overview](#project-overview) + * [Used tools and dependencies](#used-tools-and-dependencies) + * [Installation Guide](#installation-guide) + * [Prerequisites](#prerequisites) + * [End user package installation:](#end-user-package-installation) + + * [Dependencies](#dependencies) + + +### Team members + +- Konrad Bodzioch +- Dominik Breksa +- Miłosz Góralczyk + +------------- + +## Project Overview + +This repository contains our efforts for creating a Python based C extension image enhance library. As the first part of the project, a python-compatible library will be implemented, containing various functionalities from the area of Image Processing and Manipulation. + +The calculations are going to be performed within c++ code, with the extension bridging the gap between languages, allowing for full access within the Python environment while utilising the computational speed of low-level language. + +Further on, we plan on displaying the capabilities of developed extension with a python-based User Interface or Pipeline for Image Manipulation and Image Processing, allowing user to utilise the functionalities from the level of a command line or a Graphic Interface Upload-Manipulate-Output tool. + +------------- + +## Used tools and dependencies + +The project contains the `app` python package. Short for Advanced-Python-Programming. + +The package requires the following configuration in order to be properly installed (see later sections): + +- Python 3.13 +- CPython implementation +- numpy 2.3.5 + +### Installation Guide + +The package can be installed with two dependencies bundles: +- baseline package dependencies intended for end user. +- development dependencies used to test, build the project (requires additional installations). + +Our package uses the `setuptools` backend. + +#### Prerequisites + +Create python environment using `venv`: + +```bash +python3 -m venv .venv +``` + +Next, you need to choose one of the dependencies bundles. + +#### End user package installation: + +Run the following commands in your terminal and install the `app` package: + +```bash +pip3 install -e . +``` + +Alternatively previous step can be accomplished using by running the `Makefile` command: + +```bash +make python-install +``` + +#### +Package with development dependencies installation: + +```bash +pip3 install -e .[developement] +``` + +It is not needed to install the package in editable mode (`-e` flag). +However, if one would want to do code modification, the option is very helpful. + +And using makefile: + +```bash +make python-install-editable +``` + +### Dependencies + +See also `setup.cfg` for more details. + +```txt +mypy==1.18.2, +ruff==0.14.6, +flake8==7.3.0, +pylint==4.0.2, +nbqa==1.9.1 +pytest==9.0.1 +``` + +------------- diff --git a/setup.cfg b/setup.cfg index 461b482..697d1df 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,18 +8,21 @@ classifiers = Natural Language :: English, Programming Language :: C++, Programming Language :: Python, + Programming Language :: Python :: 3, + Programming Language :: Python :: 3 :: Only, Programming Language :: Python :: 3.13, Programming Language :: Python :: Implementation :: CPython [options] include_package_data = True package_dir = - =./src/ + =./src/python packages = find: python_requires = >=3.13 install_requires = importlib-metadata; python_version<"3.13" cython + numpy==2.3.5 matplotlib==3.10.0 pandas==2.3.3 @@ -40,7 +43,7 @@ development = pytest==9.0.1 [options.packages.find] -where = ./src/ +where = ./src/python include = app app.* diff --git a/setup.py b/setup.py index ba37983..0005f18 100644 --- a/setup.py +++ b/setup.py @@ -3,10 +3,12 @@ def main() -> None: setup(ext_modules=[ - Extension('app.fast', ['src/app/fast/foo.c'], include_dirs=[numpy.get_include()]) + Extension(name='app.fast', + sources=['src/cpp/fast/foo.cpp'], + include_dirs=[numpy.get_include()], + language='c++'), ]) - if __name__ == '__main__': main() diff --git a/src/app/cmd/parser.py b/src/app/cmd/parser.py deleted file mode 100644 index c4d934f..0000000 --- a/src/app/cmd/parser.py +++ /dev/null @@ -1,26 +0,0 @@ -from argparse import ArgumentParser - -from app.cmd.cmd_command import CmdCommand -from app.cmd.rotate import RotateCommand -from app.cmd.scale import ScaleCommand -from app.cmd.translate import TranslateCommand - - -def get_parser() -> ArgumentParser: - parser = ArgumentParser(prog='PROG', - description='Image CLI that performs different image operations like scaling, rotating etc') - - subparser = parser.add_subparsers(required=True, - help='Command to be performed on an image') - - command_class: type[CmdCommand] - for command_class in [RotateCommand, ScaleCommand, TranslateCommand]: - command = command_class() - - command_parser = subparser.add_parser(name=command_class.name(), - help=command_class.help()) - command_parser.set_defaults(func=command) - - command_class.parser(command_parser) - - return parser diff --git a/src/app/cmd/rotate.py b/src/app/cmd/rotate.py deleted file mode 100644 index 2eab351..0000000 --- a/src/app/cmd/rotate.py +++ /dev/null @@ -1,21 +0,0 @@ -from argparse import Namespace, ArgumentParser - -from app.cmd.cmd_command import CmdCommand - - -class RotateCommand(CmdCommand): - - @classmethod - def name(cls) -> str: - return 'rotate' - - @classmethod - def help(cls) -> str: - return 'Rotate the image' - - @classmethod - def parser(cls, parser: ArgumentParser) -> str: - pass - - def __call__(self, args: Namespace) -> None: - pass diff --git a/src/app/cmd/scale.py b/src/app/cmd/scale.py deleted file mode 100644 index a94c1b4..0000000 --- a/src/app/cmd/scale.py +++ /dev/null @@ -1,21 +0,0 @@ -from argparse import ArgumentParser, Namespace - -from app.cmd.cmd_command import CmdCommand - - -class ScaleCommand(CmdCommand): - - @classmethod - def name(cls) -> str: - return 'scale' - - @classmethod - def help(cls) -> str: - return 'Scale the image' - - @classmethod - def parser(cls, parser: ArgumentParser) -> str: - pass - - def __call__(self, args: Namespace) -> None: - pass diff --git a/src/app/cmd/translate.py b/src/app/cmd/translate.py deleted file mode 100644 index b8f4900..0000000 --- a/src/app/cmd/translate.py +++ /dev/null @@ -1,21 +0,0 @@ -from argparse import ArgumentParser, Namespace - -from app.cmd.cmd_command import CmdCommand - - -class TranslateCommand(CmdCommand): - - @classmethod - def name(cls) -> str: - return 'translate' - - @classmethod - def help(cls) -> str: - return 'Translate the image' - - @classmethod - def parser(cls, parser: ArgumentParser) -> str: - pass - - def __call__(self, args: Namespace) -> None: - pass diff --git a/src/app/imcli.py b/src/app/imcli.py deleted file mode 100644 index f776a97..0000000 --- a/src/app/imcli.py +++ /dev/null @@ -1,9 +0,0 @@ -from app.cmd.parser import get_parser - - -def main() -> None: - args = get_parser().parse_args() - args.func(args) - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/src/app/img/read.py b/src/app/img/read.py deleted file mode 100644 index cba43d7..0000000 --- a/src/app/img/read.py +++ /dev/null @@ -1,7 +0,0 @@ -from pathlib import Path - -import numpy as np - -def read_bitmap_image(path: Path) -> np.ndarray: - image_bytes = path.read_bytes() - breakpoint() diff --git a/src/app/fast/foo.c b/src/cpp/fast/foo.cpp similarity index 60% rename from src/app/fast/foo.c rename to src/cpp/fast/foo.cpp index 3ec7ac6..2b1f510 100644 --- a/src/app/fast/foo.c +++ b/src/cpp/fast/foo.cpp @@ -1,85 +1,83 @@ -#define PY_SSIZE_T_CLEAN -#include -#include - -static PyObject * -spam_system(PyObject *self, PyObject *args) -{ - const char *command; - int sts; - - if (!PyArg_ParseTuple(args, "s", &command)) - return NULL; - sts = system(command); - return PyLong_FromLong(sts); -} - -static PyObject * -numpy_add(PyObject *self, PyObject *args){ - PyArrayObject *arr; - PyArg_ParseTuple(args, "O", &arr); - if(PyErr_Occurred()){ - return NULL; - } - if(!PyArray_Check(arr) || PyArray_TYPE(arr) != NPY_DOUBLE) { - PyErr_SetString(PyExc_TypeError, "Argument must be a numpy array of type double!"); - return NULL; - } - - - double *data = PyArray_DATA(arr); - int64_t size = PyArray_SIZE(arr); - - double total=0; - for (int i=0; i < size; i++){ - total += data[i]; - } - return PyFloat_FromDouble(total); -} - -static PyObject *SpamError = NULL; - -static int -spam_module_exec(PyObject *m) -{ - if (SpamError != NULL) { - PyErr_SetString(PyExc_ImportError, - "cannot initialize spam module more than once"); - return -1; - } - SpamError = PyErr_NewException("spam.error", NULL, NULL); - if (PyModule_AddObjectRef(m, "SpamError", SpamError) < 0) { - return -1; - } - - return 0; -} - -static PyMethodDef spam_methods[] = { - {"system", spam_system, METH_VARARGS, - "Execute a shell command."}, - {"numpy_add", numpy_add, METH_VARARGS, - "Perform adding operation."}, - {NULL, NULL, 0, NULL} /* Sentinel */ -}; - -static PyModuleDef_Slot spam_module_slots[] = { - {Py_mod_exec, spam_module_exec}, - {0, NULL} -}; - -static struct PyModuleDef spam_module = { - .m_base = PyModuleDef_HEAD_INIT, - .m_name = "spam", - .m_size = 0, // non-negative - .m_slots = spam_module_slots, - .m_methods = spam_methods -}; - -PyMODINIT_FUNC -PyInit_fast(void) -{ - PyObject* module_obj = PyModuleDef_Init(&spam_module); - import_array(); - return module_obj; +#define PY_SSIZE_T_CLEAN +#include +#include + +static PyObject * +numpy_system(PyObject *self, PyObject *args) +{ + const char *command; + int sts; + + if (!PyArg_ParseTuple(args, "s", &command)) + return NULL; + sts = system(command); + return PyLong_FromLong(sts); +} + +static PyObject * +numpy_add(PyObject *self, PyObject *args){ + PyArrayObject *arr; + PyArg_ParseTuple(args, "O", &arr); + if(PyErr_Occurred()){ + return NULL; + } + if(!PyArray_Check(arr) || PyArray_TYPE(arr) != NPY_DOUBLE) { + PyErr_SetString(PyExc_TypeError, "Argument must be a numpy array of type double!"); + return NULL; + } + + + double *data = (double *) PyArray_DATA(arr); + int64_t size = PyArray_SIZE(arr); + + double total=0; + for (int i=0; i < size; i++){ + total += data[i]; + } + return PyFloat_FromDouble(total); +} + +static PyObject *SpamError = NULL; + +static int +numpy_module_exec(PyObject *m) +{ + if (SpamError != NULL) { + PyErr_SetString(PyExc_ImportError, + "cannot initialize numpy module more than once"); + return -1; + } + SpamError = PyErr_NewException("numpy.error", NULL, NULL); + if (PyModule_AddObjectRef(m, "SpamError", SpamError) < 0) { + return -1; + } + + return 0; +} + +static PyMethodDef numpy_methods[] = { + {"system", numpy_system, METH_VARARGS, "Execute a shell command."}, + {"numpy_add", numpy_add, METH_VARARGS, "Perform adding operation."}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +static PyModuleDef_Slot numpy_module_slots[] = { + {Py_mod_exec, (void*) numpy_module_exec}, + {0, NULL} +}; + +static struct PyModuleDef numpy_module = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = "numpy", + .m_size = 0, // non-negative + .m_methods = numpy_methods, + .m_slots = numpy_module_slots, +}; + +PyMODINIT_FUNC +PyInit_fast(void) +{ + PyObject* module_obj = PyModuleDef_Init(&numpy_module); + import_array(); + return module_obj; } \ No newline at end of file diff --git a/src/app/__init__.py b/src/python/app/__init__.py similarity index 100% rename from src/app/__init__.py rename to src/python/app/__init__.py diff --git a/src/app/cmd/__init__.py b/src/python/app/command/__init__.py similarity index 100% rename from src/app/cmd/__init__.py rename to src/python/app/command/__init__.py diff --git a/src/python/app/command/io.py b/src/python/app/command/io.py new file mode 100644 index 0000000..c702c6d --- /dev/null +++ b/src/python/app/command/io.py @@ -0,0 +1,16 @@ +from sys import stdin, stdout +from typing import BinaryIO + + +def map_input(input_source: str) -> BinaryIO: + if input_source is None: + return stdin.buffer + + return open(input_source, mode='rb') + + +def map_output(output_source: str) -> BinaryIO: + if output_source is None: + return stdout.buffer + + return open(output_source, mode='wb') diff --git a/src/python/app/command/parser.py b/src/python/app/command/parser.py new file mode 100644 index 0000000..d4abc8e --- /dev/null +++ b/src/python/app/command/parser.py @@ -0,0 +1,65 @@ +from argparse import ArgumentParser, Namespace +from typing import Callable + +from app.command.io import map_input, map_output +from app.io.format_factory import get_reader_from_format, KnownFormat, get_writer_from_format +from app.operation.bgr2rgb import BGR2RGBOperation +from app.operation.flip import FlipOperation +from app.operation.grayscale import GrayscaleOperation +from app.operation.histogram_equalization import HistogramEqualizationOperation +from app.operation.identity import IdentityOperation +from app.operation.operation import Operation +from app.operation.roll import RollOperation + +from app.operation.rotate90 import Rotate90Operation +from app.operation.scale import ScaleOperation +from app.operation.translate import TranslateOperation + + +def get_parser() -> ArgumentParser: + parser = ArgumentParser(prog='PROG', + description='Image CLI that performs different image operations like scaling, rotating etc') + parser.add_argument('input', + nargs='?', + default=None, + help='program input') + parser.add_argument('output', + nargs='?', + default=None, + help='program output') + parser.add_argument('--format', + nargs='?', + default=KnownFormat.default().name.lower(), + choices=KnownFormat.get_available_formats(), + help='program output') + + subparser = parser.add_subparsers(required=True, + help='Command to be performed on an image') + + operation_class: type[Operation] + for operation_class in [Rotate90Operation, IdentityOperation, FlipOperation, BGR2RGBOperation, RollOperation, GrayscaleOperation, HistogramEqualizationOperation]: + operation = operation_class() + + operation_parser = subparser.add_parser(name=operation_class.name(), + help=operation_class.help()) + operation_parser.set_defaults(func=prepare_command(operation)) + + operation_class.parser(operation_parser) + + return parser + +def prepare_command(command: Operation) -> Callable[[Namespace], int]: + + def wrapper(args: Namespace) -> int: + data_format = KnownFormat.from_string(args.format) + reader = get_reader_from_format(data_format) + writer = get_writer_from_format(data_format) + + with map_input(args.input) as input_source, map_output(args.output) as output_source: + input_arr = reader.read_format(input_source) + result = command(args, input_arr) + writer.write_format(output_source, result) + + return 0 + + return wrapper diff --git a/src/app/img/__init__.py b/src/python/app/error/__init__.py similarity index 100% rename from src/app/img/__init__.py rename to src/python/app/error/__init__.py diff --git a/src/python/app/error/app_exception.py b/src/python/app/error/app_exception.py new file mode 100644 index 0000000..4c31690 --- /dev/null +++ b/src/python/app/error/app_exception.py @@ -0,0 +1,2 @@ +class AppException(Exception): + pass diff --git a/src/python/app/error/invalid_format_exception.py b/src/python/app/error/invalid_format_exception.py new file mode 100644 index 0000000..19ad924 --- /dev/null +++ b/src/python/app/error/invalid_format_exception.py @@ -0,0 +1,5 @@ +from app.error.app_exception import AppException + + +class UnknownFormatException(AppException): + pass diff --git a/src/python/app/error/unknown_format_exception.py b/src/python/app/error/unknown_format_exception.py new file mode 100644 index 0000000..a142965 --- /dev/null +++ b/src/python/app/error/unknown_format_exception.py @@ -0,0 +1,10 @@ +from app.error.app_exception import AppException + + +class UnknownFormatException(AppException): + + def __init__(self, data_format: str) -> None: + self.data_format = data_format + + def __str__(self) -> str: + return f'unknown data format provided: {self.data_format}' diff --git a/src/app/logs/__init__.py b/src/python/app/image/__init__.py similarity index 100% rename from src/app/logs/__init__.py rename to src/python/app/image/__init__.py diff --git a/src/python/app/image/image.py b/src/python/app/image/image.py new file mode 100644 index 0000000..a8e43c0 --- /dev/null +++ b/src/python/app/image/image.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass +from typing import final + +import numpy as np + + +@final +@dataclass(slots=True) +class Image: + data: np.ndarray diff --git a/src/python/app/imcli.py b/src/python/app/imcli.py new file mode 100644 index 0000000..0aeeaa4 --- /dev/null +++ b/src/python/app/imcli.py @@ -0,0 +1,9 @@ +from app.command.parser import get_parser + + +def main() -> None: + args = get_parser().parse_args() + return args.func(args) + +if __name__ == '__main__': + main() diff --git a/src/app/py.typed b/src/python/app/io/__init__.py similarity index 100% rename from src/app/py.typed rename to src/python/app/io/__init__.py diff --git a/src/python/app/io/bmp.py b/src/python/app/io/bmp.py new file mode 100644 index 0000000..44dc20d --- /dev/null +++ b/src/python/app/io/bmp.py @@ -0,0 +1,97 @@ +import struct +from enum import IntEnum +from typing import final, BinaryIO, override + +import numpy as np + +from app.image.image import Image +from app.io.format_reader import FormatReader +from app.io.format_writer import FormatWriter + + +class DBIHeader(IntEnum): + BITMAP_CORE_HEADER = 12 + OS22X_BITMAP_HEADER = 16 + BITMAP_INFO_HEADER = 40 + BITMAP_V2_INFO_HEADER = 52 + BITMAP_V3_INFO_HEADER = 56 + BITMAP_V4_HEADER = 108 + BITMAP_V5_HEADER = 124 + + +@final +class BMPReader(FormatReader): + + @override + def read_format(self, file: BinaryIO) -> Image: + header = file.read(14) + assert len(header) == 14 + + signature_first, signature_second = struct.unpack('BB', header[:2]) + # signature_first, signature_second = hex(signature_first), hex(signature_second) + assert (signature_first, signature_second) in [(0x42, 0x4D), (0x42, 0x41), (0x43, 0x49), (0x43, 0x50), + (0x49, 0x43), (0x50, 0x54)] + + file_size, = struct.unpack('I', header[2:6]) + res1, res2 = struct.unpack('HH', header[6:10]) + file_offset_to_pixel_array, = struct.unpack('I', header[10:]) + + dib_header_size = file.read(4) + dib_header_size, = struct.unpack('I', dib_header_size) + assert dib_header_size in (12, 64, 16, 40, 52, 56, 108, 124) + assert dib_header_size == 40 # BITMAPINFOHEADER + + dib_header_no_size = file.read(dib_header_size - 4) + image_width, image_height, panes, bits_per_pixel = struct.unpack('iiHH', dib_header_no_size[:12]) + assert panes == 1 + assert bits_per_pixel in (1, 4, 8, 16, 24, 32) + + compression_method, raw_bitmap_data_size, horizontal_resolution, vertical_resolution, num_colors, num_important_colors = \ + struct.unpack('IIiiII', dib_header_no_size[12:]) + assert compression_method == 0 + assert raw_bitmap_data_size != 0 + + padding = (4 - ((3 * image_width) % 4)) % 4 + row_size = ((bits_per_pixel * image_width + 31) // 32) * 4 + + image_bytes = bytearray() + for _ in range(image_height): + image_bytes += file.read(row_size)[:row_size - padding] + + num_colors_end = bits_per_pixel // 8 + return Image(data=np.flip( + np.flip( + np.frombuffer(bytes(image_bytes), dtype=np.uint8).reshape(image_height, image_width, num_colors_end)[:, :, ::-1], + axis=0 + ), + axis=1) + ) + + +@final +class BMPWriter(FormatWriter): + + def write_format(self, file: BinaryIO, input_image: Image) -> None: + input_arr = input_image.data + input_arr = np.flip(np.flip(input_arr[:, :, ::-1], axis=1), axis=0) + + image_height, image_width, num_colors = input_arr.shape + bits_per_pixel = num_colors * 8 + padding = (4 - ((3 * image_width) % 4)) % 4 + row_size = ((bits_per_pixel * image_width + 31) // 32) * 4 + raw_bitmap_data_size = row_size * image_height + + file_size = 54 + raw_bitmap_data_size + + file.write(struct.pack('BB', 0x42, 0x4D)) # signature + file.write(struct.pack('I', file_size)) # file_size + file.write(struct.pack('HH', 0, 0)) # reserved1 & reserved2 + file.write(struct.pack('I', 54)) # file_offset_to_pixel_array + + file.write(struct.pack('I', 40)) # dbi header size + file.write(struct.pack('iiHH', image_width, image_height, 1, bits_per_pixel)) + file.write(struct.pack('IIiiII', 0, raw_bitmap_data_size, 2834, 2834, 0, 0)) + + for i in range(image_height): + file.write(input_arr[i].tobytes()) + file.write(bytes([0] * padding)) diff --git a/src/python/app/io/format_factory.py b/src/python/app/io/format_factory.py new file mode 100644 index 0000000..85ac265 --- /dev/null +++ b/src/python/app/io/format_factory.py @@ -0,0 +1,47 @@ +import enum +from typing import Self + +from app.error.unknown_format_exception import UnknownFormatException +from app.io.bmp import BMPReader, BMPWriter +from app.io.format_reader import FormatReader +from app.io.format_writer import FormatWriter + + +class KnownFormat(enum.Enum): + BMP = 0 + + @classmethod + def from_string(cls, data_format: str) -> Self: + match data_format: + case 'bmp': + return cls.BMP + + case _: + raise UnknownFormatException(data_format) + + @classmethod + def get_available_formats(cls) -> list[str]: + return [e.name.lower() for e in cls] + + @classmethod + def default(cls) -> Self: + return KnownFormat.BMP + + +def get_reader_from_format(data_format: KnownFormat) -> FormatReader: + + match data_format: + case KnownFormat.BMP: + return BMPReader() + + case _: + assert False, "unreachable" + +def get_writer_from_format(data_format: KnownFormat) -> FormatWriter: + + match data_format: + case KnownFormat.BMP: + return BMPWriter() + + case _: + assert False, "unreachable" diff --git a/src/python/app/io/format_reader.py b/src/python/app/io/format_reader.py new file mode 100644 index 0000000..73dd446 --- /dev/null +++ b/src/python/app/io/format_reader.py @@ -0,0 +1,13 @@ +from abc import abstractmethod, ABC +from typing import BinaryIO + +import numpy as np + +from app.image.image import Image + + +class FormatReader(ABC): + + @abstractmethod + def read_format(self, file: BinaryIO) -> Image: + pass diff --git a/src/python/app/io/format_writer.py b/src/python/app/io/format_writer.py new file mode 100644 index 0000000..3ad846a --- /dev/null +++ b/src/python/app/io/format_writer.py @@ -0,0 +1,11 @@ +from abc import ABC, abstractmethod +from typing import BinaryIO + +from app.image.image import Image + + +class FormatWriter(ABC): + + @abstractmethod + def write_format(self, file: BinaryIO, input_image: Image) -> None: + pass diff --git a/src/python/app/operation/__init__.py b/src/python/app/operation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/python/app/operation/bgr2rgb.py b/src/python/app/operation/bgr2rgb.py new file mode 100644 index 0000000..ba91ee1 --- /dev/null +++ b/src/python/app/operation/bgr2rgb.py @@ -0,0 +1,24 @@ +from argparse import Namespace, ArgumentParser +from typing import final + +from app.image.image import Image +from app.operation.operation import Operation + + +@final +class BGR2RGBOperation(Operation): + + @classmethod + def name(cls) -> str: + return 'bgr2rgb' + + @classmethod + def help(cls) -> str: + return 'Converts the image from bgr to rgb' + + @classmethod + def parser(cls, parser: ArgumentParser) -> None: + pass + + def __call__(self, args: Namespace, input_image: Image) -> Image: + return Image(input_image.data[:, :, ::-1]) diff --git a/src/python/app/operation/flip.py b/src/python/app/operation/flip.py new file mode 100644 index 0000000..dd2aee8 --- /dev/null +++ b/src/python/app/operation/flip.py @@ -0,0 +1,38 @@ +from argparse import Namespace, ArgumentParser +from typing import final + +import numpy as np + +from app.image.image import Image +from app.operation.operation import Operation + + +@final +class FlipOperation(Operation): + + @classmethod + def name(cls) -> str: + return 'flip' + + @classmethod + def help(cls) -> str: + return 'Flip image horizontal or vertically' + + @classmethod + def parser(cls, parser: ArgumentParser) -> None: + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('--horizontal', + dest='horizontal', + action='store_true') + group.add_argument('--vertical', + dest='vertical', + action='store_true') + + def __call__(self, args: Namespace, input_image: Image) -> Image: + if args.horizontal: + return Image(np.flip(input_image.data, axis=1)) + + if args.vertical: + return Image(np.flip(input_image.data, axis=0)) + + assert False, 'unreachable' diff --git a/src/python/app/operation/grayscale.py b/src/python/app/operation/grayscale.py new file mode 100644 index 0000000..0163aee --- /dev/null +++ b/src/python/app/operation/grayscale.py @@ -0,0 +1,32 @@ +from argparse import Namespace, ArgumentParser +from typing import final + +import numpy as np + +from app.image.image import Image +from app.operation.operation import Operation + + +@final +class GrayscaleOperation(Operation): + + @classmethod + def name(cls) -> str: + return 'grayscale' + + @classmethod + def help(cls) -> str: + return 'Converts the image to grayscale' + + @classmethod + def parser(cls, parser: ArgumentParser) -> None: + pass + + def __call__(self, args: Namespace, input_image: Image) -> Image: + return Image(np.expand_dims(np.clip( + 0.2126 * input_image.data[:, :, 0] + + 0.7152 * input_image.data[:, :, 1] + + 0.0722 * input_image.data[:, :, 1], + a_min=0., + a_max=255.).astype(np.uint8), + axis=-1)) diff --git a/src/python/app/operation/histogram_equalization.py b/src/python/app/operation/histogram_equalization.py new file mode 100644 index 0000000..b203f32 --- /dev/null +++ b/src/python/app/operation/histogram_equalization.py @@ -0,0 +1,40 @@ +from argparse import Namespace, ArgumentParser +from typing import final + +import numpy as np + +from app.image.image import Image +from app.operation.operation import Operation + + +@final +class HistogramEqualizationOperation(Operation): + + @classmethod + def name(cls) -> str: + return 'histogram_equalization' + + @classmethod + def help(cls) -> str: + return 'Performs histogram equalization on the image' + + @classmethod + def parser(cls, parser: ArgumentParser) -> None: + pass + + def __call__(self, args: Namespace, input_image: Image) -> Image: + output_image = np.empty_like(input_image.data) + + output_image[:, :, 0] = self.equalize_chanel(input_image.data[:, :, 0]) + output_image[:, :, 1] = self.equalize_chanel(input_image.data[:, :, 1]) + output_image[:, :, 2] = self.equalize_chanel(input_image.data[:, :, 2]) + + return Image(data=output_image) + + def equalize_chanel(self, image: np.ndarray) -> np.ndarray: + image_histogram, bins = np.histogram(image.flatten(), 256, density=True) + cdf = image_histogram.cumsum() + cdf = (256 - 1) * cdf / cdf[-1] + + image_equalized = np.interp(image.flatten(), bins[:-1], cdf) + return image_equalized.reshape(image.shape) \ No newline at end of file diff --git a/src/python/app/operation/identity.py b/src/python/app/operation/identity.py new file mode 100644 index 0000000..7c7500e --- /dev/null +++ b/src/python/app/operation/identity.py @@ -0,0 +1,24 @@ +from argparse import Namespace, ArgumentParser +from typing import final + +from app.image.image import Image +from app.operation.operation import Operation + + +@final +class IdentityOperation(Operation): + + @classmethod + def name(cls) -> str: + return 'identity' + + @classmethod + def help(cls) -> str: + return 'Perform no operation on the image' + + @classmethod + def parser(cls, parser: ArgumentParser) -> None: + pass + + def __call__(self, args: Namespace, input_image: Image) -> Image: + return input_image diff --git a/src/app/cmd/cmd_command.py b/src/python/app/operation/operation.py similarity index 56% rename from src/app/cmd/cmd_command.py rename to src/python/app/operation/operation.py index bb3c05e..5e233a7 100644 --- a/src/app/cmd/cmd_command.py +++ b/src/python/app/operation/operation.py @@ -1,25 +1,29 @@ -from abc import ABC, abstractmethod -from argparse import Namespace, ArgumentParser - - -class CmdCommand(ABC): - - @classmethod - @abstractmethod - def name(cls) -> str: - pass - - @classmethod - @abstractmethod - def help(cls) -> str: - pass - - @classmethod - @abstractmethod - def parser(cls, parser: ArgumentParser) -> str: - pass - - @abstractmethod - def __call__(self, args: Namespace) -> None: - pass - +from abc import ABC, abstractmethod +from argparse import Namespace, ArgumentParser +from typing import final + +import numpy as np + +from app.image.image import Image + + +class Operation(ABC): + + @classmethod + @abstractmethod + def name(cls) -> str: + pass + + @classmethod + @abstractmethod + def help(cls) -> str: + pass + + @classmethod + @abstractmethod + def parser(cls, parser: ArgumentParser) -> None: + pass + + @abstractmethod + def __call__(self, args: Namespace, input_image: Image) -> Image: + pass diff --git a/src/python/app/operation/roll.py b/src/python/app/operation/roll.py new file mode 100644 index 0000000..b40cfdd --- /dev/null +++ b/src/python/app/operation/roll.py @@ -0,0 +1,41 @@ +from argparse import ArgumentParser, Namespace +from typing import final + +import numpy as np + +from app.image.image import Image +from app.operation.operation import Operation + + +@final +class RollOperation(Operation): + + @classmethod + def name(cls) -> str: + return 'roll' + + @classmethod + def help(cls) -> str: + return 'Perform data roll on given axes' + + @classmethod + def parser(cls, parser: ArgumentParser) -> None: + parser.add_argument('--vertical-shift', + default=0, + dest='ver_shift', + type=int) + parser.add_argument('--horizontal-shift', + default=0, + dest='hor_shift', + type=int) + + def __call__(self, args: Namespace, input_image: Image) -> Image: + return Image(data=np.roll( + np.roll( + input_image.data, + shift=args.ver_shift, + axis=0 + ), + shift=args.hor_shift, + axis=1 + )) diff --git a/src/python/app/operation/rotate90.py b/src/python/app/operation/rotate90.py new file mode 100644 index 0000000..a0ce731 --- /dev/null +++ b/src/python/app/operation/rotate90.py @@ -0,0 +1,30 @@ +from argparse import ArgumentParser, Namespace +from typing import final + +import numpy as np + +from app.image.image import Image +from app.operation.operation import Operation + + +@final +class Rotate90Operation(Operation): + + @classmethod + def name(cls) -> str: + return 'rotate90' + + @classmethod + def help(cls) -> str: + return 'Perform 90 deg rotation' + + @classmethod + def parser(cls, parser: ArgumentParser) -> None: + parser.add_argument('--rotations', + default=1, + type=int, + help='number of full rotations (clockwise), negative numbers ' + ) + + def __call__(self, args: Namespace, input_image: Image) -> Image: + return Image(np.rot90(input_image.data, k=args.rotations)) diff --git a/src/python/app/py.typed b/src/python/app/py.typed new file mode 100644 index 0000000..e69de29