Skip to content

Core API Reference

This section provides documentation for the core logic modules.

main

Entry point for the Image Converter CLI application.

Handles command-line arguments and either launches the interactive menu or executes CLI-specified image processing operations.

StoreInOrder

Bases: Action

Custom argparse Action to store arguments in the order they are provided.

Source code in src/image_converter/main.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class StoreInOrder(argparse.Action):
    """Custom argparse Action to store arguments in the order they are provided."""

    def __call__(self, parser, namespace, values, option_string=None):
        """Store the argument destination and values in the 'ordered_operations' list.

        Args:
            parser (argparse.ArgumentParser): The ArgumentParser object.
            namespace (argparse.Namespace): The Namespace object that will hold the parsed attributes.
            values (str | list): The parsed argument values.
            option_string (str, optional): The option string that was used to invoke this action. Defaults to None.

        """
        if not hasattr(namespace, "ordered_operations"):
            setattr(namespace, "ordered_operations", [])
        if values is None:
            norm_values = []
        elif isinstance(values, (str, int, float)):
            norm_values = [values]

        else:
            norm_values = values
        namespace.ordered_operations.append({"dest": self.dest, "values": norm_values})

__call__(parser, namespace, values, option_string=None)

Store the argument destination and values in the 'ordered_operations' list.

Parameters:

Name Type Description Default
parser ArgumentParser

The ArgumentParser object.

required
namespace Namespace

The Namespace object that will hold the parsed attributes.

required
values str | list

The parsed argument values.

required
option_string str

The option string that was used to invoke this action. Defaults to None.

None
Source code in src/image_converter/main.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def __call__(self, parser, namespace, values, option_string=None):
    """Store the argument destination and values in the 'ordered_operations' list.

    Args:
        parser (argparse.ArgumentParser): The ArgumentParser object.
        namespace (argparse.Namespace): The Namespace object that will hold the parsed attributes.
        values (str | list): The parsed argument values.
        option_string (str, optional): The option string that was used to invoke this action. Defaults to None.

    """
    if not hasattr(namespace, "ordered_operations"):
        setattr(namespace, "ordered_operations", [])
    if values is None:
        norm_values = []
    elif isinstance(values, (str, int, float)):
        norm_values = [values]

    else:
        norm_values = values
    namespace.ordered_operations.append({"dest": self.dest, "values": norm_values})

main()

Run the main entry point for the image conversion CLI application.

Parse command-line arguments and execute the specified image processing pipeline. If no arguments are provided or the --menu flag is used, it launches the interactive menu interface. By default, the program searches for images in the Base Images/ directory if no specific file path is given.

Source code in src/image_converter/main.py
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
def main():
    """Run the main entry point for the image conversion CLI application.

    Parse command-line arguments and execute the specified image processing
    pipeline. If no arguments are provided or the `--menu` flag is used, it
    launches the interactive menu interface. By default, the program searches
    for images in the `Base Images/` directory if no specific file path is given.
    """
    # If --menu is used or no arguments are provided, start the menu.

    if "--menu" in sys.argv or len(sys.argv) == 1:
        # Import dynamically to prevent circular dependency issues with tests
        from .menu import interactive_menu

        interactive_menu()
        return

    parser = argparse.ArgumentParser(
        description="A versatile command-line image manipulation tool."
    )
    parser.add_argument(
        "file",
        type=str,
        nargs="?",
        default=None,
        help='The image file or pattern to process (e.g., "input.jpg", "images/*.png"). '
        'If omitted or set to "*", searches in the "Base Images/" directory by default.',
    )
    parser.add_argument(
        "--menu",
        action="store_true",
        help="Start the application in interactive menu mode.",
    )
    # Initialize ordered_operations to an empty list by default
    parser.set_defaults(ordered_operations=[])

    parser.add_argument(
        "-bg",
        "--remove-background",
        dest="remove_background",
        action=StoreInOrder,
        nargs=0,
        help="Remove image background.",
    )
    parser.add_argument(
        "-s",
        "--scale",
        dest="scale",
        action=StoreInOrder,
        nargs="+",
        help="Scale image by factor (e.g., '1.5x') or to a specific size (e.g., '400px 300px').",
    )
    parser.add_argument(
        "--resample",
        type=str,
        default="bilinear",
        choices=["nearest", "bilinear", "bicubic", "lanczos"],
        help="Resampling filter for scaling.",
    )
    parser.add_argument(
        "-i",
        "--invert",
        dest="invert",
        action=StoreInOrder,
        nargs=0,
        help="Invert the colors of an image.",
    )
    parser.add_argument(
        "-g",
        "--grayscale",
        dest="grayscale",
        action=StoreInOrder,
        nargs=0,
        help="Convert an image to grayscale.",
    )
    parser.add_argument(
        "--flip",
        dest="flip",
        action=StoreInOrder,
        type=str,
        choices=["horizontal", "vertical", "both"],
        help="Flip image horizontally, vertically, or both.",
    )
    parser.add_argument(
        "--edge-detection",
        dest="edge_detection",
        action=StoreInOrder,
        type=str,
        choices=["sobel", "canny", "kovalevsky"],
        help="Apply edge detection using the specified method.",
    )
    parser.add_argument(
        "--threshold",
        type=int,
        default=50,
        help="Threshold for the Kovalevsky edge detection method (0-255).",
    )
    parser.add_argument(
        "--brightness",
        dest="brightness",
        action=StoreInOrder,
        type=int,
        help="Adjust brightness (-100 to 100).",
    )
    parser.add_argument(
        "--contrast",
        dest="contrast",
        action=StoreInOrder,
        type=int,
        help="Adjust contrast (-100 to 100).",
    )
    parser.add_argument(
        "--saturation",
        dest="saturation",
        action=StoreInOrder,
        type=int,
        help="Adjust saturation (-100 to 100).",
    )
    parser.add_argument(
        "--blur",
        dest="blur",
        action=StoreInOrder,
        type=float,
        help="Apply Gaussian Blur with specified radius.",
    )
    parser.add_argument(
        "--sharpen",
        dest="sharpen",
        action=StoreInOrder,
        type=int,
        help="Resulting image sharpness (0-100).",
    )
    parser.add_argument(
        "--color-balance",
        dest="color_balance",
        action=StoreInOrder,
        nargs=3,
        type=float,
        help="Adjust R, G, B channels (e.g., 1.2 0.8 1.0).",
    )
    parser.add_argument(
        "--hue-rotation",
        dest="hue_rotation",
        action=StoreInOrder,
        type=int,
        help="Rotate hue by specified degrees (0-360).",
    )
    parser.add_argument(
        "--posterize",
        dest="posterize",
        action=StoreInOrder,
        type=int,
        help="Reduce color depth to N bits (1-8).",
    )
    parser.add_argument(
        "--border",
        dest="border",
        action=StoreInOrder,
        nargs=3,
        help="Add border: thickness (int) color (str) position (expand/inside).",
    )
    parser.add_argument(
        "--vignette",
        dest="vignette",
        action=StoreInOrder,
        type=int,
        help="Apply vignette effect with intensity (0-100).",
    )
    parser.add_argument(
        "--rotate",
        dest="rotate",
        action=StoreInOrder,
        type=int,
        help="Rotate image by 90-degree increments (0, 90, 180, 270).",
    )
    # Metadata Flags
    parser.add_argument(
        "-vm",
        "--view-metadata",
        dest="view_metadata",
        action=StoreInOrder,
        nargs=0,
        help="Prints the existing metadata to the console.",
    )
    parser.add_argument(
        "-em",
        "--export-metadata",
        dest="export_metadata",
        action=StoreInOrder,
        nargs="?",
        const="",
        help="Exports the image's existing metadata and saves it to a specified JSON file.",
    )
    parser.add_argument(
        "-sm",
        "--strip-metadata",
        dest="strip_metadata",
        action=StoreInOrder,
        nargs=0,
        help="Removes all privacy metadata from the image.",
    )
    parser.add_argument(
        "-cm",
        "--copy-metadata",
        dest="copy_metadata",
        action=StoreInOrder,
        nargs=1,
        help="Extracts metadata from a specified source image and applies it to the current image in the pipeline.",
    )
    parser.add_argument(
        "-setm",
        "--set-metadata",
        dest="set_metadata",
        action=StoreInOrder,
        nargs="+",
        help="Overwrites the image's metadata completely with the provided input (JSON file, inline JSON, or Key=Value pairs). Set Key=None to delete a tag.",
    )
    parser.add_argument(
        "-um",
        "--update-metadata",
        dest="update_metadata",
        action=StoreInOrder,
        nargs="+",
        help="Merges the provided input with the image's existing metadata. Set Key=None to delete a specific tag.",
    )
    parser.add_argument(
        "--author",
        dest="author",
        action=StoreInOrder,
        nargs=1,
        help="Quick-access flag to set the Author/Artist tag.",
    )
    parser.add_argument(
        "--copyright",
        dest="copyright",
        action=StoreInOrder,
        nargs=1,
        help="Quick-access flag to set the Copyright tag.",
    )

    parser.add_argument(
        "--flatten",
        nargs="?",
        const="white",
        default=None,
        help="Composite image against a solid color background (default: white) for output formats that may not consistently support alpha channels (e.g., JPEG, WEBP).",
    )
    # Global output options (not piped)
    parser.add_argument(
        "--format",
        action="append",
        type=str,
        help="Output format (e.g. png, jpg, webp, heic, avif). Can be used multiple times.",
    )
    parser.add_argument(
        "--quality",
        action="append",
        type=int,
        help="Output quality (1-100) per format. Evaluated in order of --format arguments.",
    )

    args = parser.parse_args()

    # Check if any action was specified (operations or explicit formats)
    if not args.ordered_operations and not args.format:
        console.print(
            "[yellow]No actions specified. Please provide at least one operation flag (e.g., --invert, --scale 2x) "
            "or an output format (e.g., --format webp).[/]\n"
            "[dim white]To see all available options, run with --help or use the interactive --menu.[/]"
        )
        return

    move_images_to_subdirectory("Base Images")
    images_data = []
    image_path_pattern = (
        args.file if args.file and args.file != "*" else "Base Images/*"
    )

    try:
        filepaths = glob.glob(image_path_pattern)
        if not filepaths:
            console.print(
                f"[yellow]No files found matching pattern: '{image_path_pattern}'[/]\n"
                f"[dim white]Please verify the file path or ensure images exist in the target directory.[/]"
            )
            console.print(
                "[dim white]Please check the path or place some images in the specified directory and try again.[/]"
            )
            return
        for filepath in filepaths:
            if os.path.isfile(filepath):
                filename = Path(filepath).name
                images_data.append([filename, filepath])
    except Exception as e:
        console.print(f"[red]Error while loading file(s): {e}[/]")
        return

    process_images_and_save(images_data, args.ordered_operations, args)

processing

Core image processing pipeline and handlers.

Applies chained operations in sequence to a list of images and handles the saving of the processed outputs, managing UI progress reporting.

StyledTimeElapsedColumn

Bases: TimeElapsedColumn

A TimeElapsedColumn that supports custom styling.

Attributes:

Name Type Description
style str

The Rich style string to apply to the time elapsed text.

Source code in src/image_converter/processing.py
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
class StyledTimeElapsedColumn(TimeElapsedColumn):
    """A TimeElapsedColumn that supports custom styling.

    Attributes:
        style (str): The Rich style string to apply to the time elapsed text.

    """

    def __init__(self, style="none"):
        """Initialize the StyledTimeElapsedColumn.

        Args:
            style (str, optional): The Rich style string to apply. Defaults to "none".

        """
        super().__init__()
        self.style = style

    def render(self, task) -> Text:
        """Render the time elapsed for a given task with the configured style.

        Args:
            task (Task): The progress task.

        Returns:
            Text: A Rich Text object representing the styled time elapsed.

        """
        from rich.text import Text

        text = super().render(task)
        return Text(str(text), style=self.style)

__init__(style='none')

Initialize the StyledTimeElapsedColumn.

Parameters:

Name Type Description Default
style str

The Rich style string to apply. Defaults to "none".

'none'
Source code in src/image_converter/processing.py
68
69
70
71
72
73
74
75
76
def __init__(self, style="none"):
    """Initialize the StyledTimeElapsedColumn.

    Args:
        style (str, optional): The Rich style string to apply. Defaults to "none".

    """
    super().__init__()
    self.style = style

render(task)

Render the time elapsed for a given task with the configured style.

Parameters:

Name Type Description Default
task Task

The progress task.

required

Returns:

Name Type Description
Text Text

A Rich Text object representing the styled time elapsed.

Source code in src/image_converter/processing.py
78
79
80
81
82
83
84
85
86
87
88
89
90
91
def render(self, task) -> Text:
    """Render the time elapsed for a given task with the configured style.

    Args:
        task (Task): The progress task.

    Returns:
        Text: A Rich Text object representing the styled time elapsed.

    """
    from rich.text import Text

    text = super().render(task)
    return Text(str(text), style=self.style)

handle_blur(image, image_name, values, args)

Handle the 'blur' operation.

Parameters:

Name Type Description Default
image Image

The input image.

required
image_name str

The name of the image file.

required
values list

The arguments for the operation (blur radius).

required
args Namespace

The parsed CLI arguments.

required

Returns:

Type Description
Image

Image.Image: The blurred image.

Source code in src/image_converter/processing.py
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
def handle_blur(image: Image.Image, image_name, values, args) -> Image.Image:
    """Handle the 'blur' operation.

    Args:
        image (Image.Image): The input image.
        image_name (str): The name of the image file.
        values (list): The arguments for the operation (blur radius).
        args (argparse.Namespace): The parsed CLI arguments.

    Returns:
        Image.Image: The blurred image.

    """
    console.print(
        f"  [bright_yellow]›[/] [yellow]Applying Gaussian Blur (radius: {values[0]})...[/]"
    )
    return apply_blur(image, values[0])

handle_border(image, image_name, values, args)

Handle the 'border' operation.

Parameters:

Name Type Description Default
image Image

The input image.

required
image_name str

The name of the image file.

required
values list

A list containing [thickness, color, position]. Expects thickness to be int (or convertible string), color (str), position (str).

required
args Namespace

The parsed CLI arguments.

required

Returns:

Type Description
Image

Image.Image: The image with the border added, or the original image if arguments are invalid.

Source code in src/image_converter/processing.py
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
def handle_border(image: Image.Image, image_name, values, args) -> Image.Image:
    """Handle the 'border' operation.

    Args:
        image (Image.Image): The input image.
        image_name (str): The name of the image file.
        values (list): A list containing [thickness, color, position].
            Expects thickness to be int (or convertible string), color (str), position (str).
        args (argparse.Namespace): The parsed CLI arguments.

    Returns:
        Image.Image: The image with the border added, or the original image if arguments are invalid.

    """
    try:
        thickness = int(values[0])
        color = values[1]
        position = values[2]
        console.print(
            f"  [bright_yellow]›[/] [yellow]Adding border: {thickness}px, {color}, {position}[/]"
        )
        return apply_border(image, thickness, color, position)
    except (ValueError, IndexError) as e:
        console.print(
            f"  [bright_red]✗[/] [red]Invalid border arguments: {values}. Error: {e}[/]"
        )
        return image

handle_brightness(image, image_name, values, args)

Handle the 'brightness' adjustment operation.

Parameters:

Name Type Description Default
image Image

The input image.

required
image_name str

The name of the image file.

required
values list

The arguments for the operation (brightness level).

required
args Namespace

The parsed CLI arguments.

required

Returns:

Type Description
Image

Image.Image: The image with adjusted brightness.

Source code in src/image_converter/processing.py
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
def handle_brightness(image: Image.Image, image_name, values, args) -> Image.Image:
    """Handle the 'brightness' adjustment operation.

    Args:
        image (Image.Image): The input image.
        image_name (str): The name of the image file.
        values (list): The arguments for the operation (brightness level).
        args (argparse.Namespace): The parsed CLI arguments.

    Returns:
        Image.Image: The image with adjusted brightness.

    """
    console.print(
        f"  [bright_yellow]›[/] [yellow]Adjusting brightness by {values[0]}...[/]"
    )
    return adjust_brightness(image, values[0])

handle_color_balance(image, image_name, values, args)

Handle the 'color_balance' operation.

Parameters:

Name Type Description Default
image Image

The input image.

required
image_name str

The name of the image file.

required
values list

The arguments for the operation (R, G, B factors).

required
args Namespace

The parsed CLI arguments.

required

Returns:

Type Description
Image

Image.Image: The color-balanced image.

Source code in src/image_converter/processing.py
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
def handle_color_balance(image: Image.Image, image_name, values, args) -> Image.Image:
    """Handle the 'color_balance' operation.

    Args:
        image (Image.Image): The input image.
        image_name (str): The name of the image file.
        values (list): The arguments for the operation (R, G, B factors).
        args (argparse.Namespace): The parsed CLI arguments.

    Returns:
        Image.Image: The color-balanced image.

    """
    console.print(
        f"  [bright_yellow]›[/] [yellow]Applying Color Balance (R:{values[0]}, G:{values[1]}, B:{values[2]})...[/]"
    )
    return apply_color_balance(image, values[0], values[1], values[2])

handle_contrast(image, image_name, values, args)

Handle the 'contrast' adjustment operation.

Parameters:

Name Type Description Default
image Image

The input image.

required
image_name str

The name of the image file.

required
values list

The arguments for the operation (contrast level).

required
args Namespace

The parsed CLI arguments.

required

Returns:

Type Description
Image

Image.Image: The image with adjusted contrast.

Source code in src/image_converter/processing.py
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
def handle_contrast(image: Image.Image, image_name, values, args) -> Image.Image:
    """Handle the 'contrast' adjustment operation.

    Args:
        image (Image.Image): The input image.
        image_name (str): The name of the image file.
        values (list): The arguments for the operation (contrast level).
        args (argparse.Namespace): The parsed CLI arguments.

    Returns:
        Image.Image: The image with adjusted contrast.

    """
    console.print(
        f"  [bright_yellow]›[/] [yellow]Adjusting contrast by {values[0]}...[/]"
    )
    return adjust_contrast(image, values[0])

handle_edge_detection(image, image_name, values, args)

Handle the 'edge_detection' operation.

Parameters:

Name Type Description Default
image Image

The input image.

required
image_name str

The name of the image file.

required
values list

The arguments for the operation (method).

required
args Namespace

The parsed CLI arguments.

required

Returns:

Type Description
Image

Image.Image: The image with edge detection applied.

Source code in src/image_converter/processing.py
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
def handle_edge_detection(image: Image.Image, image_name, values, args) -> Image.Image:
    """Handle the 'edge_detection' operation.

    Args:
        image (Image.Image): The input image.
        image_name (str): The name of the image file.
        values (list): The arguments for the operation (method).
        args (argparse.Namespace): The parsed CLI arguments.

    Returns:
        Image.Image: The image with edge detection applied.

    """
    method = values[0]
    if method == "kovalevsky":
        console.print(
            f"  [bright_yellow]›[/] [yellow]Applying {method} edge detection (threshold: {args.threshold})...[/]"
        )
        return edge_detection(image, "kovalevsky", args.threshold)
    else:
        console.print(
            f"  [bright_yellow]›[/] [yellow]Applying {method} edge detection...[/]"
        )
        return edge_detection(image, method)

handle_flip(image, image_name, values, args)

Handle the 'flip' operation.

Parameters:

Name Type Description Default
image Image

The input image.

required
image_name str

The name of the image file.

required
values list

The arguments for the operation (direction).

required
args Namespace

The parsed CLI arguments.

required

Returns:

Type Description
Image

Image.Image: The flipped image.

Source code in src/image_converter/processing.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def handle_flip(image: Image.Image, image_name, values, args) -> Image.Image:
    """Handle the 'flip' operation.

    Args:
        image (Image.Image): The input image.
        image_name (str): The name of the image file.
        values (list): The arguments for the operation (direction).
        args (argparse.Namespace): The parsed CLI arguments.

    Returns:
        Image.Image: The flipped image.

    """
    console.print(f"  [bright_yellow]›[/] [yellow]Flipping {values[0]}...[/]")
    return flip_image(image, values[0])

handle_grayscale(image, image_name, values, args)

Handle the 'grayscale' operation.

Parameters:

Name Type Description Default
image Image

The input image.

required
image_name str

The name of the image file.

required
values list

The arguments for the operation.

required
args Namespace

The parsed CLI arguments.

required

Returns:

Type Description
Image

Image.Image: The grayscale image.

Source code in src/image_converter/processing.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
def handle_grayscale(image: Image.Image, image_name, values, args) -> Image.Image:
    """Handle the 'grayscale' operation.

    Args:
        image (Image.Image): The input image.
        image_name (str): The name of the image file.
        values (list): The arguments for the operation.
        args (argparse.Namespace): The parsed CLI arguments.

    Returns:
        Image.Image: The grayscale image.

    """
    console.print("  [bright_yellow]›[/] [yellow]Converting to grayscale...[/]")
    return grayscale(image)

handle_hue_rotation(image, image_name, values, args)

Handle the 'hue_rotation' operation.

Parameters:

Name Type Description Default
image Image

The input image.

required
image_name str

The name of the image file.

required
values list

The arguments for the operation (degrees).

required
args Namespace

The parsed CLI arguments.

required

Returns:

Type Description
Image

Image.Image: The image with rotated hue.

Source code in src/image_converter/processing.py
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
def handle_hue_rotation(image: Image.Image, image_name, values, args) -> Image.Image:
    """Handle the 'hue_rotation' operation.

    Args:
        image (Image.Image): The input image.
        image_name (str): The name of the image file.
        values (list): The arguments for the operation (degrees).
        args (argparse.Namespace): The parsed CLI arguments.

    Returns:
        Image.Image: The image with rotated hue.

    """
    console.print(
        f"  [bright_yellow]›[/] [yellow]Rotating Hue by {values[0]} degrees...[/]"
    )
    return rotate_hue(image, values[0])

handle_invert(image, image_name, values, args)

Handle the 'invert' operation.

Parameters:

Name Type Description Default
image Image

The input image.

required
image_name str

The name of the image file.

required
values list

The arguments for the operation.

required
args Namespace

The parsed CLI arguments.

required

Returns:

Type Description
Image

Image.Image: The image with inverted colors.

Source code in src/image_converter/processing.py
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
def handle_invert(image: Image.Image, image_name, values, args) -> Image.Image:
    """Handle the 'invert' operation.

    Args:
        image (Image.Image): The input image.
        image_name (str): The name of the image file.
        values (list): The arguments for the operation.
        args (argparse.Namespace): The parsed CLI arguments.

    Returns:
        Image.Image: The image with inverted colors.

    """
    console.print("  [bright_yellow]›[/] [yellow]Inverting colors...[/]")
    return invert_colors(image)

handle_posterize(image, image_name, values, args)

Handle the 'posterize' operation.

Parameters:

Name Type Description Default
image Image

The input image.

required
image_name str

The name of the image file.

required
values list

The arguments for the operation (bits).

required
args Namespace

The parsed CLI arguments.

required

Returns:

Type Description
Image

Image.Image: The posterized image.

Source code in src/image_converter/processing.py
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
def handle_posterize(image: Image.Image, image_name, values, args) -> Image.Image:
    """Handle the 'posterize' operation.

    Args:
        image (Image.Image): The input image.
        image_name (str): The name of the image file.
        values (list): The arguments for the operation (bits).
        args (argparse.Namespace): The parsed CLI arguments.

    Returns:
        Image.Image: The posterized image.

    """
    console.print(
        f"  [bright_yellow]›[/] [yellow]Posterizing to {values[0]} bits...[/]"
    )
    return apply_posterize(image, values[0])

handle_remove_background(image, image_name, values, args)

Handle the 'remove_background' operation.

Parameters:

Name Type Description Default
image Image

The input image.

required
image_name str

The name of the image file.

required
values list

The arguments for the operation.

required
args Namespace

The parsed CLI arguments.

required

Returns:

Type Description
Image

Image.Image: The image with the background removed.

Source code in src/image_converter/processing.py
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
def handle_remove_background(
    image: Image.Image, image_name, values, args
) -> Image.Image:
    """Handle the 'remove_background' operation.

    Args:
        image (Image.Image): The input image.
        image_name (str): The name of the image file.
        values (list): The arguments for the operation.
        args (argparse.Namespace): The parsed CLI arguments.

    Returns:
        Image.Image: The image with the background removed.

    """
    console.print("  [bright_yellow]›[/] [yellow]Removing background...[/]")
    return remove_background(image)

handle_rotate(image, image_name, values, args)

Handle the 'rotate' operation.

Parameters:

Name Type Description Default
image Image

The input image.

required
image_name str

The name of the image file.

required
values list

A list containing [angle]. Expects angle to be int.

required
args Namespace

The parsed CLI arguments.

required

Returns:

Type Description
Image

Image.Image: The rotated image.

Source code in src/image_converter/processing.py
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
def handle_rotate(image: Image.Image, image_name, values, args) -> Image.Image:
    """Handle the 'rotate' operation.

    Args:
        image (Image.Image): The input image.
        image_name (str): The name of the image file.
        values (list): A list containing [angle]. Expects angle to be int.
        args (argparse.Namespace): The parsed CLI arguments.

    Returns:
        Image.Image: The rotated image.

    """
    logger_str = (
        f"  [bright_yellow]›[/] [yellow]Rotating image by {values[0]} degrees...[/]"
    )
    console.print(logger_str)
    return rotate_image(image, values[0])

handle_saturation(image, image_name, values, args)

Handle the 'saturation' adjustment operation.

Parameters:

Name Type Description Default
image Image

The input image.

required
image_name str

The name of the image file.

required
values list

The arguments for the operation (saturation level).

required
args Namespace

The parsed CLI arguments.

required

Returns:

Type Description
Image

Image.Image: The image with adjusted saturation.

Source code in src/image_converter/processing.py
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
def handle_saturation(image: Image.Image, image_name, values, args) -> Image.Image:
    """Handle the 'saturation' adjustment operation.

    Args:
        image (Image.Image): The input image.
        image_name (str): The name of the image file.
        values (list): The arguments for the operation (saturation level).
        args (argparse.Namespace): The parsed CLI arguments.

    Returns:
        Image.Image: The image with adjusted saturation.

    """
    console.print(
        f"  [bright_yellow]›[/] [yellow]Adjusting saturation by {values[0]}...[/]"
    )
    return adjust_saturation(image, values[0])

handle_scale(image, image_name, values, args)

Handle the 'scale' operation.

Parameters:

Name Type Description Default
image Image

The input image.

required
image_name str

The name of the image file.

required
values list

The arguments for the operation (scale factor or dimensions).

required
args Namespace

The parsed CLI arguments.

required

Returns:

Type Description
Image

Image.Image: The scaled image, or the original image if scaling fails.

Source code in src/image_converter/processing.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
def handle_scale(image: Image.Image, image_name, values, args) -> Image.Image:
    """Handle the 'scale' operation.

    Args:
        image (Image.Image): The input image.
        image_name (str): The name of the image file.
        values (list): The arguments for the operation (scale factor or dimensions).
        args (argparse.Namespace): The parsed CLI arguments.

    Returns:
        Image.Image: The scaled image, or the original image if scaling fails.

    """
    scale_params = values
    scale_factor = None
    new_size = None

    if len(scale_params) == 1:
        # Handle single argument as scale factor (e.g., "1.5", "0.5x")
        try:
            # Remove 'x' if present, then parse float
            clean_param = scale_params[0].lower().replace("x", "")
            scale_factor = float(clean_param)
        except ValueError:
            console.print(
                f"  [bright_red]✗[/] [red]Invalid scale factor: {scale_params[0]}[/]"
            )
            return image

    elif len(scale_params) == 2:
        try:
            width = int(scale_params[0].lower().replace("px", ""))
            height = int(scale_params[1].lower().replace("px", ""))
            new_size = (width, height)
        except ValueError:
            console.print(
                f"  [bright_red]✗[/] [red]Invalid size format: {scale_params}[/]"
            )
            return image
    else:
        console.print(
            "  [bright_red]✗[/] [red]Invalid format for --scale argument. Use '1.5', '1.5x' or '400px 300px'.[/]"
        )
        return image
    if scale_factor is not None:
        console.print(
            f"  [bright_yellow]›[/] [yellow]Scaling by factor: {scale_factor}...[/]"
        )
    elif new_size is not None:
        console.print(
            f"  [bright_yellow]›[/] [yellow]Scaling to dimensions: {new_size[0]}x{new_size[1]}...[/]"
        )
    else:
        console.print("  [bright_yellow]›[/] [yellow]Scaling...[/]")
    return scale_image(
        image,
        scale_factor=scale_factor,
        new_size=new_size,
        resample_filter=args.resample,
    )

handle_sharpen(image, image_name, values, args)

Handle the 'sharpen' operation.

Parameters:

Name Type Description Default
image Image

The input image.

required
image_name str

The name of the image file.

required
values list

The arguments for the operation (sharpness intensity).

required
args Namespace

The parsed CLI arguments.

required

Returns:

Type Description
Image

Image.Image: The sharpened image.

Source code in src/image_converter/processing.py
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
def handle_sharpen(image: Image.Image, image_name, values, args) -> Image.Image:
    """Handle the 'sharpen' operation.

    Args:
        image (Image.Image): The input image.
        image_name (str): The name of the image file.
        values (list): The arguments for the operation (sharpness intensity).
        args (argparse.Namespace): The parsed CLI arguments.

    Returns:
        Image.Image: The sharpened image.

    """
    console.print(
        f"  [bright_yellow]›[/] [yellow]Applying Sharpen (intensity: {values[0]})...[/]"
    )
    return apply_sharpen(image, values[0])

handle_vignette(image, image_name, values, args)

Handle the 'vignette' operation.

Parameters:

Name Type Description Default
image Image

The input image.

required
image_name str

The name of the image file.

required
values list

A list containing [intensity]. Expects intensity to be int.

required
args Namespace

The parsed CLI arguments.

required

Returns:

Type Description
Image

Image.Image: The image with the vignette effect applied.

Source code in src/image_converter/processing.py
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
def handle_vignette(image: Image.Image, image_name, values, args) -> Image.Image:
    """Handle the 'vignette' operation.

    Args:
        image (Image.Image): The input image.
        image_name (str): The name of the image file.
        values (list): A list containing [intensity]. Expects intensity to be int.
        args (argparse.Namespace): The parsed CLI arguments.

    Returns:
        Image.Image: The image with the vignette effect applied.

    """
    console.print(
        f"  [bright_yellow]›[/] [yellow]Applying vignette with intensity {values[0]}...[/]"
    )
    return apply_vignette(image, values[0])

process_images_and_save(images_data, ordered_operations, cli_args)

Process a list of images by applying a sequence of operations and saves the results.

Parameters:

Name Type Description Default
images_data list

A list of tuples, where each tuple contains (filename, filepath).

required
ordered_operations list

A list of dictionaries detailing the operations to apply. Each dict should have 'dest' (operation name) and 'values' (operation arguments).

required
cli_args Namespace

The parsed command-line arguments.

required
Source code in src/image_converter/processing.py
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
def process_images_and_save(images_data, ordered_operations, cli_args):
    """Process a list of images by applying a sequence of operations and saves the results.

    Args:
        images_data (list): A list of tuples, where each tuple contains (filename, filepath).
        ordered_operations (list): A list of dictionaries detailing the operations to apply.
            Each dict should have 'dest' (operation name) and 'values' (operation arguments).
        cli_args (argparse.Namespace): The parsed command-line arguments.

    """
    operation_handlers = {
        "flip": handle_flip,
        "scale": handle_scale,
        "remove_background": handle_remove_background,
        "invert": handle_invert,
        "grayscale": handle_grayscale,
        "edge_detection": handle_edge_detection,
        "brightness": handle_brightness,
        "contrast": handle_contrast,
        "saturation": handle_saturation,
        "blur": handle_blur,
        "sharpen": handle_sharpen,
        "color_balance": handle_color_balance,
        "hue_rotation": handle_hue_rotation,
        "posterize": handle_posterize,
        "border": handle_border,
        "rotate": handle_rotate,
        "vignette": handle_vignette,
        "view_metadata": handle_view_metadata,
        "export_metadata": handle_export_metadata,
        "strip_metadata": handle_strip_metadata,
        "copy_metadata": handle_copy_metadata,
        "set_metadata": handle_set_metadata,
        "update_metadata": handle_update_metadata,
        "author": handle_author,
        "copyright": handle_copyright,
    }

    if not images_data:
        console.print("[yellow]No images to process.[/]")
        console.print(
            "[dim white]Please specify valid image files or ensure images exist in your input directory.[/]"
        )
        return

    # Processing Header
    console.print()
    console.print(
        "✨ [bold cyan]Image Converter[/] | [italic white]Interactive Image Processor[/]",
        justify="left",
    )
    console.rule(style="dim")
    console.print()
    console.print(
        f"[bold bright_white]Processing[/] [bright_cyan]{len(images_data)}[/] [bold bright_white]images...[/]"
    )
    console.print()

    total_images = len(images_data)
    results = []  # Store tuple of (filename, success, output_dims, output_size_bytes, error_msg)
    start_time = time.time()

    # Pre-compute operations and their handlers
    prepared_operations = [
        (op["dest"], op.get("values", []), operation_handlers.get(op["dest"]))
        for op in ordered_operations
    ]

    # Steps per image: Open(1) + Ops(N) + Save(1)
    image_total = 1 + len(ordered_operations) + 1

    with Progress(
        SpinnerColumn("dots", style="bright_cyan"),
        TextColumn("[bold bright_white]{task.description}"),
        BarColumn(
            bar_width=30,
            style="dim white",
            complete_style="bright_cyan",
            finished_style="bright_green",
        ),
        TaskProgressColumn(),
        StyledTimeElapsedColumn(style="bright_cyan"),
        console=console,
        transient=False,
    ) as progress:
        # Pre-add all image tasks (hidden) so they appear above Total
        image_tasks = []
        for original_name, _ in images_data:
            task_id = progress.add_task(
                original_name, total=image_total, visible=False, start=False
            )
            image_tasks.append(task_id)

        # Add Total last so it stays at the bottom
        overall = progress.add_task("Total", total=total_images)

        for i, (original_name, image_path) in enumerate(images_data, 1):
            image_task = image_tasks[i - 1]
            image_results = _process_single_image(
                original_name,
                image_path,
                prepared_operations,
                cli_args,
                progress,
                image_task,
                start_time,
                i,
                total_images,
                image_total,
                overall,
            )
            results.extend(image_results)

    elapsed = time.time() - start_time

    # ── Equivalent CLI Command ────────────────────────
    cli_args_list = []

    # 1. Add formatted file paths
    for img_name, img_path in images_data:
        # Wrap paths in quotes if they contain spaces
        safe_path = f'"{img_path}"' if " " in img_path else img_path
        cli_args_list.append(safe_path)

    # 2. Add the operations
    if ordered_operations:
        for op in ordered_operations:
            arg_name = op["dest"].replace("_", "-")
            arg_vals = " ".join(map(str, op.get("values", [])))
            cli_args_list.append(f"--{arg_name} {arg_vals}".strip())

            if (
                op["dest"] == "scale"
                and hasattr(cli_args, "resample")
                and cli_args.resample
            ):
                cli_args_list.append(f"--resample {cli_args.resample}")
            if (
                op["dest"] == "edge_detection"
                and op.get("values", [""])[0] == "kovalevsky"
            ):
                cli_args_list.append(
                    f"--threshold {getattr(cli_args, 'threshold', 50)}"
                )

    # 3. Add formatting options
    if hasattr(cli_args, "format") and cli_args.format:
        for fmt in cli_args.format:
            cli_args_list.append(f"--format {fmt}")
    if hasattr(cli_args, "quality") and cli_args.quality:
        for q in cli_args.quality:
            cli_args_list.append(f"--quality {q}")

    if hasattr(cli_args, "flatten") and cli_args.flatten:
        cli_args_list.append(f"--flatten {cli_args.flatten}")

    cli_str = " ".join(cli_args_list)

    if cli_str:
        console.print()
        console.print("💻  [bold bright_cyan]Equivalent CLI Command[/]")
        console.print(f"[bright_yellow]> image-converter {cli_str}[/]")

    # ── Handle Metadata Exports ────────────────────────
    if hasattr(cli_args, "metadata_manifest") and cli_args.metadata_manifest:
        import json

        export_path = getattr(cli_args, "export_metadata_path", None)
        if not export_path:
            # Default fallback logic
            if len(images_data) == 1:
                base_name = Path(images_data[0][0]).stem
                export_path = f"{base_name}_tags.json"
            else:
                export_path = "batch_tags.json"

        # If it was a list with None, correct it
        if isinstance(export_path, list):
            export_path = export_path[0] if export_path[0] else "batch_tags.json"

        try:
            export_dir = Path(export_path).parent
            if export_dir and str(export_dir) != ".":
                os.makedirs(export_dir, exist_ok=True)

            with open(export_path, "w", encoding="utf-8") as f:
                json.dump(cli_args.metadata_manifest, f, indent=2)
            console.print(
                f"\n[bold bright_green]✓[/] [green]Exported metadata manifest to[/] [bright_white]'{export_path}'[/]"
            )
        except Exception as e:
            console.print(
                f"\n[bold bright_red]✗ Error exporting metadata manifest to '{export_path}': {e}[/]"
            )

    # ── Results Table ──────────────────────────────────
    console.print()
    console.print(Rule("Results", style="bright_cyan"))
    console.print()

    results_table = Table(
        title="📋 Output Files",
        box=box.ROUNDED,
        title_style="bold bright_cyan",
        border_style="dim cyan",
        header_style="bold bright_white",
        padding=(0, 1),
    )
    results_table.add_column("#", style="dim white", justify="right", width=3)
    results_table.add_column(
        "Filename",
        min_width=26,
        no_wrap=True,
        overflow="ellipsis",
        max_width=30,
    )
    results_table.add_column("Status", justify="center", width=8)
    results_table.add_column("Dimensions", justify="center", width=14)
    results_table.add_column("File Size", justify="right", width=10)

    for i, (fname, success, dims, size_bytes, err) in enumerate(results, 1):
        if success:
            status = Text("✓ OK", style="bold bright_green")
            fname_style = "bright_white"
            dims = Text(dims, style="bright_green")

            # Format file size nicely
            if size_bytes >= 1024 * 1024:
                size_str = f"{size_bytes / (1024 * 1024):.1f} MB"
            elif size_bytes >= 1024:
                size_str = f"{size_bytes / 1024:.1f} KB"
            else:
                size_str = f"{size_bytes} B"

            size = Text(size_str, style="bright_yellow")
        else:
            status = Text("✗ FAIL", style="bold bright_red")
            fname_style = "dim red"
            dims = Text("—", style="dim red")
            size = Text("—", style="dim red")

        results_table.add_row(
            str(i),
            Text(fname, style=fname_style),
            status,
            dims,
            size,
        )

    console.print(results_table)

    # ── Summary Stats ──────────────────────────────────
    console.print()

    succeeded = sum(1 for _, success, *_ in results if success)
    failed = sum(1 for _, success, *_ in results if not success)

    summary = Text("  ")
    summary.append(f"✓ {succeeded} succeeded", style="bold bright_green")
    summary.append("  │  ", style="dim")
    summary.append(f"✗ {failed} failed", style="bold bright_red")
    summary.append("  │  ", style="dim")
    summary.append(f"{total_images} total", style="bold bright_white")
    summary.append("  │  ", style="dim")
    summary.append(f"⏱ {elapsed:.1f}s", style="bright_cyan")

    console.print(
        Panel(
            summary,
            border_style="dim white",
            box=box.ROUNDED,
            padding=(0, 1),
            expand=False,
        )
    )
    console.print()

file_management

Utilities for managing files and directories.

Provides functions to manage images, such as moving images from the current directory to a designated subdirectory.

move_images_to_subdirectory(subdirectory_name)

Move all image files from the current directory to a specified subdirectory.

Typically used to move images into the 'Base Images' directory, which is the default search path for image processing operations.

Parameters:

Name Type Description Default
subdirectory_name str

The name of the subdirectory to create and move the images into.

required

Raises:

Type Description
Exception

If an error occurs during directory creation or file moving.

Source code in src/image_converter/file_management.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
def move_images_to_subdirectory(subdirectory_name: str):
    """Move all image files from the current directory to a specified subdirectory.

    Typically used to move images into the 'Base Images' directory, which is the
    default search path for image processing operations.

    Args:
        subdirectory_name (str): The name of the subdirectory to create and move
            the images into.

    Raises:
        Exception: If an error occurs during directory creation or file moving.

    """
    try:
        # 1. Create the subdirectory if it doesn't exist
        if not os.path.exists(subdirectory_name):
            os.makedirs(
                subdirectory_name
            )  # Create with intermediate directories if needed

        # 2. Get a list of all files in the current directory
        files = os.listdir(".")  # "." represents the current directory

        # 3. Iterate through the files and move image files
        for filename in files:
            # Check if it's a file (and not a directory) - Important!
            if os.path.isfile(filename):
                # 4. Check if the file is an image (using common extensions)
                #   You can customize this list for other image types
                if filename.lower().endswith(
                    (".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".tif", ".webp")
                ):
                    source_path = filename
                    destination_path = os.path.join(
                        subdirectory_name, filename
                    )  # Join for correct path
                    shutil.move(source_path, destination_path)  # Move the file
                    console.print(
                        f"[bright_green]Moved:[/] {filename} to {subdirectory_name}"
                    )  # Informative message

    except Exception as e:  # Handle potential errors
        console.print(f"[red]An error occurred: {e}[/]")