Skip to content

Commit 673a451

Browse files
authored
Merge pull request #19 from achimoraites/feat/extended-features
Feat/extended features
2 parents 8d9e66d + 08e49f4 commit 673a451

File tree

10 files changed

+225
-72
lines changed

10 files changed

+225
-72
lines changed

.vscode/settings.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"[python]": {
3+
"editor.defaultFormatter": "ms-python.black-formatter"
4+
},
5+
"python.formatting.provider": "none"
6+
}

CONTRIBUTING.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,12 @@ Please follow the [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guide
4848

4949
Ensure that your code works as expected without affecting existing functionality.
5050

51-
(We will add unit tests to the project in the near future)
51+
We are using pytest, the `tests` folder contains our unit tests.
52+
53+
```bash
54+
# run unit tests
55+
pytest tests
56+
```
5257

5358
## Reporting Issues
5459

README.md

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,34 @@ In that case using an virtual environment is recommended
3939
```
4040
## Example usage
4141

42+
### Arguments
43+
- `-s, --src` (required): set the path to the directory you want to convert (source)!
44+
- `-t, --tgt`: where to store the converted images (target), by default they are stored in the "converted" folder.
45+
- `-d, --delete-source-directory`: removes the directory with the original images (source)
46+
- `-r, --resolution`: Allows to set the converted image resolutions, you can use numbers or percentages: eg 75% -> width and height are 75% of the original size, 1000,1500 sets the width to 1000px and the height to 1500px
47+
- `-e, --ext`: specify the converted image format that will be used for the converted images; by default the `.jpg` is used. valid options are:
48+
- `.jpg`
49+
- `.png`
50+
4251
```
4352
# simple usage
44-
raw_image_converter --s <Enter-Path-Of-Directory>
53+
raw_image_converter -s <Enter-Path-Of-Directory>
4554
4655
# set a custom target image format
4756
raw_image_converter --s <Enter-Path-Of-Directory> --ext '.png'
57+
58+
# Advanced usage 1
59+
raw_image_converter -s images -t converted/png -r 25% -e .png -d
60+
61+
# Set custom resolution 1
62+
raw_image_converter -s images -t converted/jpg -r 1024,768
63+
64+
# Set custom resolution 2
65+
raw_image_converter -s images -t converted/jpg -r 50%,33%
4866
```
49-
- The --s argument is where you set the path to the directory you want to convert!
50-
- The --ext argument is where you specify the image format that will be used for the converted images; by default the `.jpg` is used. valid options are:
51-
- `.jpg`
52-
- `.png`
5367

54-
The application will create a folder 'converted' where all your converted images are located!
68+
69+
5570

5671
And you are done!
5772

raw_image_converter/__main__.py

Lines changed: 98 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,128 @@
11
from datetime import datetime
22
import os
3-
from raw_image_converter.utils import check_extension, convert_file, convert_raw, image_not_exists
3+
from raw_image_converter.utils import (
4+
check_extension,
5+
convert_file,
6+
convert_raw,
7+
image_not_exists,
8+
delete_directory,
9+
)
410
import argparse
511
import concurrent.futures
6-
from colorama import *
7-
#TODO use the extension argument of the command everywhere
12+
from colorama import Fore, Style
813

9-
# All images are converted to jpg
1014

11-
# where to save our images
12-
directory = "converted"
13-
14-
# create a directory if needed to store our converted images!
15-
if not os.path.exists(directory):
16-
os.makedirs(directory)
15+
def tuple_type(values):
16+
values = values.split(",")
17+
return tuple(values)
1718

1819

1920
def main():
20-
print('### PYTHON IMAGE CONVERTER ### \n \n')
21-
22-
parser = argparse.ArgumentParser(description="Convert images to JPG")
23-
parser.add_argument('-s', "--src", dest = "src_dir",help='specify the source directory!', required=True) # this argument is required to start the conversion
24-
parser.add_argument("-t","--tgt", dest = "tgt_dir", help="specify the target directory!") # if there is no target directory given, the script will store the converted images in the source folder
25-
parser.add_argument('-e',"--ext", dest = "ext", default=".jpg", choices=['.jpg', '.png'],
26-
help='the image format to be used for the converted images.')
27-
parser.add_argument("-f","--folder", dest = "seperate_folder", default=False, help="should the converted images be placed in a seperate folder")
21+
print("### PYTHON IMAGE CONVERTER ### \n \n")
22+
23+
parser = argparse.ArgumentParser(description="Convert images to JPG/PNG")
24+
parser.add_argument(
25+
"-s",
26+
"--src",
27+
dest="src_dir",
28+
help="specify the source directory!",
29+
required=True,
30+
)
31+
parser.add_argument(
32+
"-t",
33+
"--tgt",
34+
dest="tgt_dir",
35+
help="specify the target directory!",
36+
default="converted",
37+
)
38+
parser.add_argument(
39+
"-e",
40+
"--ext",
41+
dest="ext",
42+
default=".jpg",
43+
choices=[".jpg", ".png"],
44+
help="the image format to be used for the converted images.",
45+
)
46+
parser.add_argument(
47+
"-d",
48+
"--delete-source-directory",
49+
action="store_true",
50+
dest="delete_source_directory",
51+
default=False,
52+
help="Delete the source directory after the convesion",
53+
)
54+
parser.add_argument(
55+
"-r",
56+
"--resolution",
57+
type=tuple_type,
58+
dest="resolution",
59+
default="100%,100%",
60+
help="image dimensions (width, height) as a tuple (using numbers 800,600 or using percentages 75%,75%)",
61+
)
62+
2863
args = parser.parse_args()
29-
30-
if args.tgt_dir == None:
31-
if args.seperate_folder:
32-
if not os.path.exists(args.src_dir + "/" + directory):
33-
os.makedirs(args.src_dir + "/" + directory)
34-
srcDir = args.src_dir
35-
tgtDir = args.src_dir + directory
36-
else:
37-
srcDir = args.src_dir
38-
tgtDir = args.src_dir
39-
else:
40-
if args.seperate_folder: # if the converted files should be stored in a seperate folder, create the folder and add the images to the folder
41-
if not os.path.exists(args.tgt_dir + "/" + directory):
42-
os.makedirs(args.tgt_dir + "/" + directory)
43-
srcDir = os.path.abspath(args.src_dir)
44-
tgtDir = os.path.abspath(args.tgt_dir + directory)
45-
else: # if the converted files sould be kept in the source folder,
46-
srcDir = os.path.abspath(args.src_dir)
47-
tgtDir = os.path.abspath(args.tgt_dir)
64+
65+
srcDir = os.path.abspath(args.src_dir)
66+
tgtDir = os.path.abspath(args.tgt_dir)
67+
resolution = args.resolution
68+
69+
# Handle one dimensional tuples
70+
if len(resolution) == 1:
71+
resolution = (resolution[0], resolution[0])
72+
73+
if not os.path.exists(tgtDir):
74+
os.makedirs(tgtDir)
4875

4976
# find files to convert
5077
try:
51-
with concurrent.futures.ProcessPoolExecutor() as executor:
52-
print("Started conversion at : " + datetime.now().time().strftime('%H:%M:%S') + '\n')
53-
print("Converting -> " + srcDir + " Directory !\n")
78+
with concurrent.futures.ProcessPoolExecutor() as executor:
79+
print(
80+
"Started conversion at : "
81+
+ datetime.now().time().strftime("%H:%M:%S")
82+
+ "\n"
83+
)
84+
print("Converting -> " + srcDir + " Directory !\n")
5485
for file in os.listdir(srcDir):
55-
#TODO also use the extension from the command as a parameter to the image_not_exists function
5686
if image_not_exists(file, tgtDir, args.ext):
57-
if 'RAW' == check_extension(file):
58-
executor.submit(
59-
convert_raw,
60-
file,
61-
srcDir,
62-
tgtDir,
63-
args.ext,
64-
)
65-
66-
if 'NOT_RAW' == check_extension(file):
87+
if "RAW" == check_extension(file):
88+
executor.submit(
89+
convert_raw,
90+
file,
91+
srcDir,
92+
tgtDir,
93+
args.ext,
94+
resolution,
95+
)
96+
97+
if "NOT_RAW" == check_extension(file):
6798
executor.submit(
6899
convert_file,
69100
file,
70101
srcDir,
71102
tgtDir,
72103
args.ext,
104+
resolution,
73105
)
74106
else:
75-
print(f"{Fore.GREEN}File " + file + f" is already converted!{Style.RESET_ALL}"+" \n ")
107+
print(
108+
f"{Fore.GREEN}File "
109+
+ file
110+
+ f" is already converted!{Style.RESET_ALL}"
111+
+ " \n "
112+
)
113+
114+
print(
115+
f"{Fore.GREEN}Converted Images are stored at - > "
116+
+ os.path.abspath(tgtDir)
117+
+ f"{Style.RESET_ALL}"
118+
)
76119

77-
print(f"{Fore.GREEN}Converted Images are stored at - > " + os.path.abspath(tgtDir)+f"{Style.RESET_ALL}")
120+
if args.delete_source_directory:
121+
delete_directory(srcDir)
78122

79123
except Exception as e:
80124
print(f"{Fore.RED}ERROR IN APPLICATION{Style.RESET_ALL}" + e)
81125

126+
82127
if __name__ == "__main__":
83128
main()

raw_image_converter/utils.py

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,39 @@
11
from PIL import Image
22
import os
3+
import numpy as np
34
import rawpy
45
import imageio
56
from datetime import datetime
7+
import shutil
68

79

810
def message(file, converted):
11+
current_time = datetime.now().time().strftime("%H:%M:%S")
912
# if conversion finished
1013
if converted:
11-
print(datetime.now().time().strftime("%H:%M:%S") + " Converted: " + file)
14+
print(f"{current_time} Converted: {file}")
1215
# if conversion failed
1316
else:
14-
print(
15-
datetime.now().time().strftime("%H:%M:%S")
16-
+ " Conversion failed for File: "
17-
+ file
18-
)
17+
print(f"{current_time} Conversion failed for File: {file}")
1918

2019

2120
# convert RAW images function
22-
def convert_raw(file, srcDir, tgtDir, extension=".jpg"):
21+
def convert_raw(file, srcDir, tgtDir, extension=".jpg", resolution=("100%", "100%")):
2322
try:
2423
ext = "." + file.split(".")[-1].lower()
2524
print(datetime.now().time().strftime("%H:%M:%S") + " Converting: " + file)
2625
source = os.path.join(srcDir, file)
2726
with rawpy.imread(source) as raw:
2827
rgb = raw.postprocess()
28+
if resolution:
29+
pil_image = Image.fromarray(rgb)
30+
31+
# Resize the image
32+
width = calculate_image_dimension(pil_image.width, resolution[0])
33+
height = calculate_image_dimension(pil_image.height, resolution[1])
34+
pil_image = pil_image.resize((width, height))
35+
36+
rgb = np.array(pil_image)
2937
imageio.imsave(os.path.join(tgtDir, file.replace(ext, "") + extension), rgb)
3038
message(file, True)
3139
except Exception as e:
@@ -34,8 +42,17 @@ def convert_raw(file, srcDir, tgtDir, extension=".jpg"):
3442
pass
3543

3644

45+
def calculate_image_dimension(dimension, resolution):
46+
if "%" in resolution:
47+
factor = float(resolution.strip("%").strip()) / 100.0
48+
result = int(dimension * factor)
49+
else:
50+
result = int(resolution)
51+
return result
52+
53+
3754
# convert function
38-
def convert_file(file, srcDir, tgtDir, extension=".jpg"):
55+
def convert_file(file, srcDir, tgtDir, extension=".jpg", resolution=("100%", "100%")):
3956
mappings = {
4057
".jpg": "JPEG",
4158
".png": "PNG",
@@ -46,6 +63,10 @@ def convert_file(file, srcDir, tgtDir, extension=".jpg"):
4663
message(file, False)
4764
path = os.path.join(srcDir, file)
4865
im = Image.open(path)
66+
if resolution:
67+
width = calculate_image_dimension(im.width, resolution[0])
68+
height = calculate_image_dimension(im.height, resolution[1])
69+
im = im.resize((width, height))
4970
im.save(os.path.join(tgtDir, file.replace(ext, "") + extension), save_format)
5071
message(file, True)
5172
except:
@@ -64,9 +85,9 @@ def ai_2_pdf(file):
6485

6586

6687
# IT IS POINTLESS TO CONVERT WHAT IS ALREADY CONVERTED!!!!
67-
def image_not_exists(image, tgtDir,tgtExt):
88+
def image_not_exists(image, tgtDir, tgtExt):
6889
ext = image.split(".")[-1].lower()
69-
target = os.path.join(tgtDir, image.replace(ext, tgtExt.replace(".","")))
90+
target = os.path.join(tgtDir, image.replace(ext, tgtExt.replace(".", "")))
7091
if os.path.isfile(target):
7192
return False
7293
else:
@@ -111,3 +132,11 @@ def check_extension(file):
111132
return "NOT RAW"
112133
# check if an .ai exists and rename it to .pdf !
113134
ai_2_pdf(file)
135+
136+
137+
def delete_directory(dir):
138+
try:
139+
shutil.rmtree(dir)
140+
print(f"Removed source directory {dir}")
141+
except OSError as o:
142+
print(f"Error, {o.strerror}: {dir}")

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ imageio==2.16.2
22
Pillow==9.3.0
33
rawpy==0.17.1
44
numpy==1.22.3
5+
colorama==0.4.6

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
# This call to setup() does all the work
1111
setup(
1212
name="raw-image-converter",
13-
version="1.0.5",
13+
version="1.1.0",
1414
description="Batch conversions of raw images",
1515
long_description=README,
1616
long_description_content_type="text/markdown",
@@ -27,7 +27,7 @@
2727
keywords='cli, converter, raw, images',
2828
packages=["raw_image_converter"],
2929
install_requires=["numpy==1.22.3", "rawpy==0.17.1",
30-
"imageio==2.16.2", "Pillow==9.3.0"],
30+
"imageio==2.16.2", "Pillow==9.3.0", "colorama==0.4.6"],
3131
entry_points={
3232
"console_scripts": [
3333
"raw_image_converter=raw_image_converter.__main__:main",

tests/__init__.py

Whitespace-only changes.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from raw_image_converter.utils import calculate_image_dimension
2+
3+
4+
class TestCalculateImageDimension:
5+
# Case: resolution is a set number
6+
def test_valid_integer_dimension_and_resolution(self):
7+
dimension = 100
8+
resolution = "200"
9+
expected_result = 200
10+
11+
result = calculate_image_dimension(dimension, resolution)
12+
13+
assert result == expected_result
14+
15+
# Case: resolution is a percentage
16+
def test_valid_integer_dimension_and_percentage_resolution(self):
17+
dimension = 100
18+
resolution = "50%"
19+
expected_result = 50
20+
21+
result = calculate_image_dimension(dimension, resolution)
22+
23+
assert result == expected_result

0 commit comments

Comments
 (0)