Converting your world to grayscale, one image at a time.
Features • Quick Start • Installation • Usage • Architecture • Demo
A modern CLI tool for fetching images from URLs and converting them to grayscale (black and white).
Built with Kotlin, Spring Boot 4, and Picocli, demonstrating clean architecture principles and modern development practices.
Note
Project is 💯 completed!
| Feature | Description |
|---|---|
| Multi-Format Support | Process JPEG, PNG, GIF, BMP, and WebP images seamlessly |
| Async Operations | Non-blocking I/O using Kotlin Coroutines for optimal performance |
| Progress Tracking | Real-time progress indicators for long-running operations |
| Flexible Output | Configurable output directory and custom filenames |
| Developer Friendly | Verbose and quiet modes for different use cases |
| Clean Architecture | SOLID principles and design patterns for maintainability |
+------------------------+ +------------------------+ +------------------------+
| URL Fetching | | Grayscale Convert | | PNG Output |
|------------------------| |------------------------| |------------------------|
| - HTTP/HTTPS support | | - High-quality algo | | - Lossless format |
| - Async WebClient | --> | - Luminosity method | --> | - Configurable path |
| - 3MB buffer limit | | - AWT ImageIO | | - Custom naming |
+------------------------+ +------------------------+ +------------------------+
# Build the application
./mvnw clean package
# Run with a URL
java -jar target/noir-1.0.0.jar https://example.com/image.jpg
# Custom output filename
java -jar target/noir-1.0.0.jar https://example.com/image.jpg -o my_image.png
# Custom output directory
java -jar target/noir-1.0.0.jar https://example.com/image.jpg -d ./processed- Prerequisites
- Installation
- Usage
- Architecture
- Technology Stack
- Exit Codes
- Demo
- Acknowledgments
- License
- Java 21 or higher
- Maven 3.8+ (or use the included Maven Wrapper)
# Clone the repository
git clone https://github.com/valentinsoare/noir.git
cd noir
# Build the project
./mvnw clean package
# The executable JAR will be at target/noir-1.0.0.jarmvn clean package# Convert an image to grayscale
java -jar target/noir-1.0.0.jar <image-url>
# Example
java -jar target/noir-1.0.0.jar https://picsum.photos/800/600| Option | Short | Description | Default |
|---|---|---|---|
--output |
-o |
Custom output filename | processed_image_<timestamp>.png |
--output-dir |
-d |
Output directory | Current directory (.) |
--quiet |
-q |
Suppress progress output | false |
--verbose |
-v |
Enable verbose logging | false |
--help |
-h |
Show help message | - |
--version |
-V |
Show version info | - |
# Save with custom filename
java -jar target/noir-1.0.0.jar https://example.com/photo.jpg -o vacation.png
# Save to specific directory
java -jar target/noir-1.0.0.jar https://example.com/photo.jpg -d ./grayscale-images
# Quiet mode (no progress output)
java -jar target/noir-1.0.0.jar https://example.com/photo.jpg -q
# Verbose mode (debug logging)
java -jar target/noir-1.0.0.jar https://example.com/photo.jpg -v
# Combine options
java -jar target/noir-1.0.0.jar https://example.com/photo.jpg -o result.png -d ./output -vNoir follows a layered architecture with clear separation of concerns:
┌─────────────────────────────────────────────────────────┐
│ CLI Layer │
│ ImageProcessorCommand (Picocli Command) │
│ CLIRunner (Spring CommandLineRunner) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Service Layer (Abstractions) │
│ ImageFetcherService ImageProcessingService │
│ InputParserService CatchThatExceptionHandler │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Service Implementation Layer │
│ ImageFetcherServiceImpl ImageProcessingServiceImpl│
│ InputParserServiceImpl CatchThatExceptionHandler │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Infrastructure Layer │
│ Spring WebClient Java AWT/ImageIO │
│ Kotlin Coroutines Java NIO │
└─────────────────────────────────────────────────────────┘
User Input (CLI args)
│
▼
┌───────────────────┐
│ CLIRunner │ ──→ Parses args, invokes command
└───────────────────┘
│
▼
┌───────────────────┐
│ ImageProcessor │ ──→ Validates input, orchestrates pipeline
│ Command │
└───────────────────┘
│
├──→ InputParserService (URL validation)
│
├──→ ImageFetcherService (HTTP GET → ByteArray)
│
├──→ ImageProcessingService (Grayscale conversion)
│
└──→ Files.write() (Save to disk)
The application uses Kotlin Coroutines with specialized dispatcher scopes:
- IO Scope (
Dispatchers.IO): Network operations, file I/O - Processing Scope (
Dispatchers.Default): CPU-intensive image processing
@IOScope private val ioScope: CoroutineScope
@ProcessingScope private val processingScope: CoroutineScope| Technology | Version | Purpose |
|---|---|---|
| Kotlin | 2.2.21 | Programming language |
| Java | 21 | Runtime |
| Spring Boot | 4.0.0 | Application framework |
| Picocli | 4.7.7 | CLI framework |
| Kotlinx Coroutines | 1.10.2 | Async programming |
| Spring WebClient | - | Reactive HTTP client |
| Kotlin Logging | 7.0.3 | Logging facade |
| Logback | - | Logging implementation |
| Maven | 3.8+ | Build tool |
| Code | Name | Description |
|---|---|---|
0 |
SUCCESS | Image processed successfully |
1 |
GENERAL_ERROR | Unexpected error occurred |
2 |
INVALID_INPUT | Invalid URL or arguments |
3 |
NETWORK_ERROR | Failed to fetch image |
4 |
PROCESSING_ERROR | Failed to convert image |
5 |
IO_ERROR | Failed to save output file |
6 |
USER_CANCELLED | Operation cancelled by user |
NoirIsRunning.mp4
- Spring Boot - Application framework
- Picocli - CLI framework
- Kotlin Coroutines - Async programming
- shields.io - README badges
This project is licensed under the MIT License - see the LICENSE file for details.
Made with Kotlin