Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/python_linter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,6 @@ jobs:

- name: Run flake8
run: make flake8

- name: Run pylint
run: make pylint
2 changes: 2 additions & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[FORMAT]
max-line-length=120
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
all:
all: clang-tidy mypy ruff flake8 pylint
echo 'All'

python-install:
Expand All @@ -23,4 +23,7 @@ ruff:
ruff check ./src/python/

flake8:
flake8 ./src/python/
flake8 --ignore=E127 ./src/python/

pylint:
pylint ./src/python/
8 changes: 4 additions & 4 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ console_scripts =

[options.extras_require]
development =
mypy==1.18.2,
ruff==0.14.6,
flake8==7.3.0,
pylint==4.0.2,
mypy==1.18.2
ruff==0.14.6
flake8==7.3.0
pylint==4.0.2
nbqa==1.9.1
pytest==9.0.1

Expand Down
6 changes: 6 additions & 0 deletions src/python/app/command/io.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
"""Module for input/output operations related to parsing commandline"""

from sys import stdin, stdout
from typing import BinaryIO


def map_input(input_source: str) -> BinaryIO:
"""Maps input source to stdin or to the file path"""

if input_source is None:
return stdin.buffer

return open(input_source, mode='rb')


def map_output(output_source: str) -> BinaryIO:
"""Maps output source to stdout or to the file path"""

if output_source is None:
return stdout.buffer

Expand Down
39 changes: 19 additions & 20 deletions src/python/app/command/parser.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
"""Module containing functions that provide functionality related to commandline arguments parsing"""

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 import Rotate90, Identity, Flip, BGR2RGB, Roll, Grayscale, HistogramEqualization, IOperation


def get_parser() -> ArgumentParser:
"""Functions that initialises the Argument Parser"""

parser = ArgumentParser(prog='PROG',
description='Image CLI that performs different image operations like scaling, rotating etc')
parser.add_argument('input',
parser.add_argument('--input',
nargs='?',
default=None,
help='program input')
parser.add_argument('output',
parser.add_argument('--output',
nargs='?',
default=None,
help='program output')
Expand All @@ -32,7 +28,7 @@ def get_parser() -> ArgumentParser:
help='program output')

subparser = parser.add_subparsers(required=True,
help='Command to be performed on an image')
help='Command or operation to be performed on an image')

for operation_class in available_commands():
operation = operation_class()
Expand All @@ -46,7 +42,8 @@ def get_parser() -> ArgumentParser:
return parser


def prepare_command(command: Operation) -> Callable[[Namespace], int]:
def prepare_command(command: IOperation) -> Callable[[Namespace], int]:
"""Function that decorates the operation in order to provide input and output to it"""

def wrapper(args: Namespace) -> int:
data_format = KnownFormat.from_string(args.format)
Expand All @@ -64,12 +61,14 @@ def wrapper(args: Namespace) -> int:


def available_commands():
"""Function that returns all the supported commandline operations by the program"""

return [
Rotate90Operation,
IdentityOperation,
FlipOperation,
BGR2RGBOperation,
RollOperation,
GrayscaleOperation,
HistogramEqualizationOperation,
Rotate90,
Identity,
Flip,
BGR2RGB,
Roll,
Grayscale,
HistogramEqualization,
]
5 changes: 4 additions & 1 deletion src/python/app/error/app_exception.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
"""Module providing root class for all exceptions in this pacakge"""


class AppException(Exception):
pass
"""Root of the exceptions to provided by this package"""
12 changes: 10 additions & 2 deletions src/python/app/error/invalid_format_exception.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
"""Module providing invalid format exception"""

from app.error.app_exception import AppException


class UnknownFormatException(AppException):
pass
class InvalidFormatException(AppException):
"""Class that signals that the format is known, but the is some error in the binary related to the image"""

def __init__(self, message: str) -> None:
self.message = message

def __str__(self) -> str:
return self.message
3 changes: 3 additions & 0 deletions src/python/app/error/unknown_format_exception.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"""Module providing unknown format exception"""

from app.error.app_exception import AppException


class UnknownFormatException(AppException):
"""The image is provided in unsupported format"""

def __init__(self, data_format: str) -> None:
self.data_format = data_format
Expand Down
4 changes: 4 additions & 0 deletions src/python/app/image/image.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Module implementing the image class used in the operations"""

from dataclasses import dataclass
from typing import final

Expand All @@ -7,4 +9,6 @@
@final
@dataclass(slots=True)
class Image:
"""Class that stores the image object as a numpy array"""

data: np.ndarray
3 changes: 2 additions & 1 deletion src/python/app/imcli.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Main entrypoint for imcli program"""
"""Main module for imcli program entrypoint"""

from app.command.parser import get_parser


def main() -> None:
"""Main entrypoint for imcli program"""
args = get_parser().parse_args()
return args.func(args)

Expand Down
88 changes: 63 additions & 25 deletions src/python/app/io/bmp.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
"""Module providing serialization and deserialization for BMP format (https://en.wikipedia.org/wiki/BMP_file_format)"""

import struct
from enum import IntEnum
from typing import final, BinaryIO, override
from typing import final, BinaryIO, override, ClassVar
from dataclasses import dataclass

import numpy as np

from app.error.invalid_format_exception import InvalidFormatException
from app.image.image import Image
from app.io.format_reader import FormatReader
from app.io.format_writer import FormatWriter
from app.io.format_reader import IFormatReader
from app.io.format_writer import IFormatWriter


class DBIHeaderType(IntEnum):
"""Enum containing all the possible DBI headers and their lengths"""

class DBIHeader(IntEnum):
BITMAP_CORE_HEADER = 12
OS22X_BITMAP_HEADER = 16
BITMAP_INFO_HEADER = 40
Expand All @@ -19,22 +25,59 @@ class DBIHeader(IntEnum):
BITMAP_V5_HEADER = 124


class Signature(IntEnum):
"""Enum containing all possible signature values"""

BM = 16973
BA = 16961
CI = 17225
CP = 17232
IC = 18755
PT = 20564


@dataclass(slots=True)
class BitmapFileHeader:
"""Implements the struct for the """

HEADER_LENGTH: ClassVar[int] = 14

signature: int
file_size: int
reserved_1: int
reserved_2: int
file_offset_to_pixel_array: int

@classmethod
def from_bytes(cls, data: bytes) -> 'BitmapFileHeader':
if len(data) < cls.HEADER_LENGTH:
raise InvalidFormatException(f"Header too short, received: {len(data)} instead of {cls.HEADER_LENGTH}")

return cls(*struct.unpack('HIHHI', data))

def __post_init__(self) -> None:
if self.signature not in set(e.value for e in Signature):
raise InvalidFormatException("Invalid signature")

if self.file_size < self.HEADER_LENGTH + DBIHeaderType.BITMAP_CORE_HEADER:
raise InvalidFormatException("File too short to parse the next data")

if self.file_offset_to_pixel_array > self.file_size:
raise InvalidFormatException("Invalid offset to pixel array")


@dataclass(slots=True)
class BMP:
pass


@final
class BMPReader(FormatReader):
class BMPReader(IFormatReader): # pylint: disable=too-few-public-methods
"""Class that deserializes BMP format to Image"""

@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:])
header = BitmapFileHeader.from_bytes(data=file.read(BitmapFileHeader.HEADER_LENGTH))

dib_header_size = file.read(4)
dib_header_size, = struct.unpack('I', dib_header_size)
Expand All @@ -60,18 +103,13 @@ def read_format(self, file: BinaryIO) -> Image:
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)
)
return Image(data=np.frombuffer(bytes(image_bytes),
dtype=np.uint8).reshape(image_height, image_width, num_colors_end))


@final
class BMPWriter(FormatWriter):
class BMPWriter(IFormatWriter): # pylint: disable=too-few-public-methods
"""Class that serializes Image to BMP format"""

def write_format(self, file: BinaryIO, input_image: Image) -> None:
input_arr = input_image.data
Expand Down
24 changes: 20 additions & 4 deletions src/python/app/io/format_factory.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
"""Module implementing routing functions for input image formats"""

import enum

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
from app.io.format_reader import IFormatReader
from app.io.format_writer import IFormatWriter


class KnownFormat(enum.Enum):
"""Enum providing all the image formats that are supported by the program"""

BMP = 0
PNG = enum.auto()
PBM = enum.auto()
PGM = enum.auto()
PPM = enum.auto()

@classmethod
def from_string(cls, data_format: str) -> 'KnownFormat':
"""Additional constructor for this class"""

match data_format:
case 'bmp':
return cls.BMP
Expand All @@ -20,14 +30,19 @@ def from_string(cls, data_format: str) -> 'KnownFormat':

@classmethod
def get_available_formats(cls) -> list[str]:
"""Return default format for commandline to use"""

return [e.name.lower() for e in cls]

@classmethod
def default(cls) -> 'KnownFormat':
"""Return default format for commandline to use"""

return KnownFormat.BMP


def get_reader_from_format(data_format: KnownFormat) -> FormatReader:
def get_reader_from_format(data_format: KnownFormat) -> IFormatReader:
"""Factory function for format reader"""

match data_format:
case KnownFormat.BMP:
Expand All @@ -37,7 +52,8 @@ def get_reader_from_format(data_format: KnownFormat) -> FormatReader:
assert False, "unreachable"


def get_writer_from_format(data_format: KnownFormat) -> FormatWriter:
def get_writer_from_format(data_format: KnownFormat) -> IFormatWriter:
"""Factory function for format writer"""

match data_format:
case KnownFormat.BMP:
Expand Down
7 changes: 5 additions & 2 deletions src/python/app/io/format_reader.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""Module providing deserialization interface for binary streams"""

from abc import abstractmethod, ABC
from typing import BinaryIO

from app.image.image import Image


class FormatReader(ABC):
class IFormatReader(ABC): # pylint: disable=too-few-public-methods
"""Interface that helps to deserialize image formats"""

@abstractmethod
def read_format(self, file: BinaryIO) -> Image:
pass
"""Abstract method that deserialize image to binary stream"""
Loading
Loading