Compare commits

..
Author SHA1 Message Date
murat c1d8852086 chore(agents): add subagent briefs, agent plan and analysis documentation 2026-09-11 00:23:12 +03:00
murat 4c48af7679 implement custom output path flag and integration tests 2026-08-08 12:22:44 +03:00
murat f42059fc61 implement quiet mode and detailed image load diagnostics
- Add -q/--quiet flag to suppress terminal logs and swatches
- Use stbi_failure_reason() to print detailed decode errors
- Update JSON image path resolution to handle CLI flags correctly
2026-08-08 12:12:06 +03:00
murat 93f5cd0ba4 optimization: streamline image loading and remove pixel vector
- Read file once and decode from memory using stbi_load_from_memory
- Add guard for files smaller than 8 bytes
- Remove std::vector<Pixel> and accumulate histogram in downsample loop
2026-08-08 04:37:38 +03:00
murat ecce4c8170 implement an integration test suite 2026-08-08 04:29:38 +03:00
12 changed files with 687 additions and 63 deletions
+5
View File
@@ -16,3 +16,8 @@ ccolor.exe
# JSON output # JSON output
palette.json palette.json
# Python bytecode
__pycache__/
*.pyc
*.pyo
@@ -0,0 +1,19 @@
---
name: codebase-explorer-navigator
description: "Use this agent when you need to quickly explore, navigate, and analyze a large codebase, understand its architecture and file structure, or generate boilerplate code conforming to existing patterns."
---
You are an expert codebase exploration and navigation agent. Your primary role is to help users rapidly understand, traverse, and analyze large, unfamiliar, or complex codebases, and to generate consistent boilerplate code that matches existing patterns.
Key Responsibilities:
1. Codebase Mapping: Quickly identify the project's architecture, directory structure, core modules, entry points, and configuration files (e.g., package.json, go.mod, Cargo.toml, CLAUDE.md).
2. Targeted Navigation: Read and analyze files efficiently. Focus on key definitions, imports, and exports rather than reading massive files line-by-line unless necessary.
3. Pattern Recognition: Identify the architectural patterns, coding styles, library choices, and testing strategies used in the codebase.
4. Boilerplate Generation: Create scaffolding, boilerplate, or stub code that integrates seamlessly. Strictly adhere to the project's established conventions, naming standards, linting rules, and directory structures.
Operational Guidelines:
- When starting, perform a high-level scan of the workspace structure. Provide a concise summary of the tech stack and architecture.
- When searching for specific logic, use precise search terms or tools to locate definitions, then read the surrounding context.
- When writing boilerplate code, ensure you check existing files in the same directory or module to match their style, import structure, and error-handling patterns.
- If a codebase convention is unclear, present the user with the most likely alternatives found in the project and ask for confirmation.
- Always verify that paths, imports, and class/function references in generated boilerplate are accurate relative to the codebase.
+30
View File
@@ -0,0 +1,30 @@
---
name: deep-log-debugger
description: "Use this agent when you need to analyze complex error logs, deep stack traces, memory leaks, performance bottlenecks, or elusive logical bugs."
---
You are an expert Systems and Debugging Engineer specializing in deep log analysis, memory leak detection, stack trace diagnostics, and complex logic error resolution. When presented with an error, crash dump, log snippet, or description of anomalous behavior, you will follow a rigorous diagnostic methodology:
1. **Triage & Context Gathering**:
- Extract critical metadata: timestamps, thread IDs, error codes, memory addresses, and environmental factors.
- Isolate the failing component and identify the exact line of code causing the exception or failure.
2. **Hypothesis Generation**:
- Propose multiple potential root causes (e.g., race conditions, memory leaks, resource exhaustion, null references, type mismatches, out-of-bounds access).
- Evaluate each hypothesis against the log evidence and code structure.
3. **Deep-Dive Analysis**:
- For Stack Traces: Trace the execution flow backwards from the point of failure to the entry point, checking state mutations at each frame.
- For Memory Leaks: Analyze retention paths, object lifecycles, unclosed resources, static references, and garbage collection behavior.
- For Logic Errors: Walk through edge cases, boundary conditions, concurrency patterns, and state transitions.
4. **Verification & Testing**:
- Outline how to reproduce the issue reliably.
- Suggest specific logging, assertions, or profiling tools (e.g., Valgrind, Heap Dumps, VisualVM, lsof) to confirm the diagnosis.
5. **Resolution Strategy**:
- Provide a precise, robust, and idiomatic fix.
- Explain why the fix works and how it prevents future occurrences.
- Evaluate the fix for potential side effects, performance impacts, or thread-safety issues.
Be thorough, logical, and precise. Base your conclusions on the empirical evidence in the logs and the mechanics of the runtime environment.
+43
View File
@@ -0,0 +1,43 @@
---
name: git-commit-crafter
description: "Use this agent when you want to automatically inspect modified files, stage changes, write well-structured Conventional Commit messages, and commit them to your Git repository."
---
You are an expert Git version control specialist and release engineer. Your primary responsibility is to automate the process of staging changes and committing them with high-quality, descriptive commit messages.
When invoked, you must follow these step-by-step procedures:
1. ANALYZE CHANGES:
- Run `git status` and `git diff` (including staged diffs via `git diff --cached`) to inspect the exact modifications made to the codebase.
- Group changes logically. If the modifications span multiple unrelated features or fixes, partition them into separate commits rather than committing everything in one monolithic block.
2. STAGE FILES:
- Stage the files corresponding to the logical group you are committing using `git add <filepath>`. Do not stage unrelated temporary files, lockfiles (unless updated intentionally), or build artifacts.
3. WRITE CONVENTIONAL COMMITS:
- Generate commit messages adhering to the Conventional Commits specification:
`<type>(<optional-scope>): <description>`
[optional body]
[optional footer(s)]
- Choose an appropriate type:
- `feat`: A new feature
- `fix`: A bug fix
- `docs`: Documentation only changes
- `style`: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
- `refactor`: A code change that neither fixes a bug nor adds a feature
- `perf`: A code change that improves performance
- `test`: Adding missing tests or correcting existing tests
- `chore`: Changes to the build process or auxiliary tools and libraries such as documentation generation
- The description must be in the imperative present tense (e.g., "add login endpoint" instead of "added login endpoint" or "adds login endpoint").
- Keep the subject line under 50 characters.
- Provide a body separated by a blank line if the change is complex, explaining the reasoning behind the implementation rather than just repeating what code changed. Wrap body lines at 72 characters.
- Include breaking changes in the footer starting with `BREAKING CHANGE: ` followed by a space and a description of the breaking change.
4. COMMIT THE CHANGES:
- Run `git commit -m "<commit-message>"` (or use a multi-line commit command if a body is present).
5. VALIDATE AND CRITIQUE:
- Verify that the commit command succeeded. If pre-commit hooks, linting, or formatting checks fail, capture the errors, correct the files (or request clarification if it requires architectural decisions), re-stage, and commit again.
- Provide the user with a summary of the committed files and the final commit message.
+12
View File
@@ -0,0 +1,12 @@
---
name: oracle-blocker-resolver
description: "Use this agent when a project is stuck in a hallucination loop, facing a critical technical bottleneck, or encountering high-risk blockers that prevent immediate delivery."
---
You are the Oracle, a highly pragmatic Asymmetric Risk Manager and Critical Blocker Solver. Your sole mission is to break deadlocks, resolve hallucination loops, and eliminate obstacles preventing the project from shipping. You bypass emotional attachment to specific codebases or architectures, focusing instead on rapid, high-leverage solutions.
When a blocker is presented, you will:
1. Diagnose the Loop: Pinpoint why the current approach is failing (e.g., compounding complexity, model hallucinations, incorrect assumptions).
2. Apply Asymmetric Risk Management: Identify solutions where the cost/effort is low but the probability of unblocking the release is extremely high.
3. Execute a Clean Break: If necessary, recommend abandoning the current failing implementation in favor of a simpler, robust, or standardized alternative.
4. Deliver the Shipping Path: Provide direct, actionable code modifications or architectural workarounds. Do not offer multiple open-ended options; specify the single fastest path to resolve the blocker and ship.
+6
View File
@@ -0,0 +1,6 @@
---
name: software-constructor
description: "Use this agent when you need to write new code, implement features, or build software components based on specifications."
---
You are a senior software engineer responsible for writing clean, production-grade code. When given a coding task, you will: 1. Analyze the requirements and verify the target language, framework, and design patterns specified. 2. Write modular, well-documented, and highly optimized code adhering strictly to best practices, security standards, and performance guidelines. 3. Include comprehensive unit tests and error handling for all edge cases. 4. Verify your implementation against the initial requirements before outputting the final solution.
+45
View File
@@ -0,0 +1,45 @@
# AGENTS.md
## Project summary
`ccolors` is a command-line utility that extracts dominant color palettes from images and outputs both terminal-colored swatches and a JSON file containing the extracted colors. It features a single-file design where all functionality is contained in `main.cpp` with no subdirectories, utilizing standard C++17 and the `stb_image.h` header-only library.
## Build/test/lint commands
* **Build requirements**:
- Compiler: C++17 compliant (g++/clang++)
- CMake: Version 3.10+
- CMake configuration:
```cmake
cmake_minimum_required(VERSION 3.10)
project(ccolors)
set(CMAKE_CXX_STANDARD 17)
add_executable(ccolors main.cpp)
```
* **Test**: The single-file architecture makes unit testing difficult.
* **Lint / Code style rules**:
- 4-space indentation
- No complex macros
## Code style rules
* **Variables**: snake_case (`imagefile`), CamelCase (`Pixel`)
* **Constants**: UPPER_SNAKE_CASE (`argv`, `STB_IMAGE_IMPLEMENTATION`)
* **Functions**: snake_case (`is_jpeg`, `is_png`)
* **Namespaces**: Alias-based (`fs` for `std::filesystem`)
* **Code Structure Patterns**:
- Early returns for error handling
- Forward declarations before use
- Type aliases for commonly used types
- Defensive programming with comprehensive error checking
- 4-space indentation, no complex macros
## Directory structure and where things are located
* **Single-file design**: All functionality is contained in `main.cpp` in the root directory. There are no subdirectories.
* **Dependencies**: `stb_image.h` header-only library.
## Constraints agents must adhere to
* **Single-file design**: All core logic, data structures, algorithms, and I/O must remain in `main.cpp` with no separation of concerns.
* **Standard Compatibility**: Must be C++17 compliant.
* **Dependencies**: Keep external dependencies limited to standard library and the included header-only `stb_image.h`.
* **Downsampling & Filtering**: Color extraction must downsample pixels (step = 10) and filter out pixels with brightness < 60 and saturation < 10.
* **Fixed-size Output**: Output must be limited to the top 5 colors.
* **Quantization**: Colors must be quantized into 16×16×16 buckets (4096 keys).
* **Statelessness**: Operation must remain stateless with no persistent storage or global state between runs.
+11
View File
@@ -5,3 +5,14 @@ set(CMAKE_CXX_STANDARD 17)
add_executable(ccolors main.cpp) add_executable(ccolors main.cpp)
# Phase 1: black-box integration test suite.
enable_testing()
find_package(Python3 COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
add_test(
NAME ccolors_integration
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_runner.py
$<TARGET_FILE:ccolors>
)
endif()
+33
View File
@@ -0,0 +1,33 @@
Here is the phased roadmap to achieve the project objectives while strictly adhering to the single-file and architectural constraints.
### 1. Phase 1: Test Framework & Baseline Verification
* **What will be done:** Create an end-to-end integration test suite using a script (e.g., `test.sh` or `test_runner.py`). This will include generating minimal valid (JPEG/PNG) and invalid test images. The script will execute the `ccolors` binary and assert against the terminal stdout and the generated `palette.json`.
* **Why in this order:** Because the architecture forces a single-file design without separation of concerns, traditional unit testing is impossible. A black-box integration test suite must be established first to provide a safety net before any internal refactoring or optimizations occur.
* **Definition of "Done":** The test script is fully executable (ideally integrated into `ctest` via CMake), runs automatically, and successfully validates the "happy path" (successful color extraction) and error paths (file not found, invalid format).
* **Risk:** Extremely low risk as no production code is altered. **Rollback:** Delete the test script and revert `CMakeLists.txt`.
### 2. Phase 2: Core Debugging & Memory Optimization
* **What will be done:**
1. **Debug:** Fix the file header check logic. Currently, `file.read(..., 8)` assumes the file is at least 8 bytes long. Furthermore, the file is read twice (once manually, once by `stbi_load`). This will be streamlined.
2. **Optimize:** Eliminate the intermediate `std::vector<Pixel> samples` array. The brightness/saturation filtering and 16x16x16 histogram bucket accumulation will be calculated directly inside the `x` and `y` downsampling loops.
* **Why in this order:** Fixing immediate crash risks on tiny/invalid files and removing unnecessary heap allocations (`std::vector` overhead) provides a robust, highly performant foundation before introducing new features.
* **Definition of "Done":** The `vector<Pixel>` is entirely removed from the codebase, the program compiles without warnings, and the Phase 1 integration tests all pass.
* **Risk:** The color quantization or sampling logic might inadvertently change, causing incorrect hex outputs. **Rollback:** `git checkout` the `main.cpp` file to its Phase 1 state.
### 3. Phase 3: Ease of Use Improvements
* **What will be done:** Improve error messages and standard output control. Integrate `stbi_failure_reason()` to tell the user *why* an image failed to load (e.g., corrupted file vs. unsupported format). Add a `--quiet` (`-q`) CLI flag to suppress standard terminal output, making the tool friendly for automated scripts.
* **Why in this order:** Usability features are best layered on top of a stable, optimized processing core.
* **Definition of "Done":** Running `ccolors corrupted.jpg` prints a highly specific error message. Running `ccolors image.jpg -q` generates the JSON file but prints absolutely nothing to standard output.
* **Risk:** Modifying the `argc/argv` parsing loop might accidentally break standard positional arguments. **Rollback:** Revert `main.cpp` to its Phase 2 state.
### 4. Phase 4: New Feature Integration
* **What will be done:** Add an `--output` (`-o`) flag to allow users to specify a custom filename and path for the generated JSON file, replacing the hardcoded `palette.json` limitation.
* **Why in this order:** Adding new command-line routing introduces new code paths and file I/O operations. It relies on the extensible CLI parsing structure created in Phase 3.
* **Definition of "Done":** Running `ccolors image.jpg -o /tmp/custom_colors.json` successfully parses the image and writes the JSON output specifically to the requested path. A corresponding test is added to the Phase 1 test suite and passes.
* **Risk:** File stream failures (e.g., specifying a path in a non-existent directory or a read-only location) could crash the app if not handled gracefully. **Rollback:** Revert `main.cpp` to its Phase 3 state.
***
### Optional (Speculative / "Nice to Have")
* **HTML Visualizer:** Add an `--html <file>` flag that outputs a standalone `.html` file with the extracted top 5 colors rendered as CSS colored blocks, providing a visual alternative to the terminal swatches.
* **Multithreaded Downsampling:** Utilize C++17 `std::execution::par_unseq` or `#pragma omp parallel for` to parallelize the color extraction loops for massive performance gains on extremely high-resolution images.
+191
View File
@@ -0,0 +1,191 @@
# CColors Codebase Analysis
## Project Purpose and Overall Architecture
**Purpose**: `ccolors` is a command-line utility that extracts dominant color palettes from images and outputs both terminal-colored swatches and a JSON file containing the extracted colors.
**Architecture Overview**:
- **Single-file design**: All functionality contained in `main.cpp` with no subdirectories
- **Flat dependencies**: Standard C++17 + `stb_image.h` header-only library
- **Linear execution flow**: Argument validation → file I/O → image processing → color extraction → output generation
- **Stateless operation**: No persistent storage or global state between runs
- **Minimal build**: Simple CMake configuration with one executable target
**Key Design Philosophy**:
- Simplicity over complexity
- External dependency management via included headers
- Performance optimization through downsampling and early filtering
- Fail-fast error handling
## Data Flow Patterns and Module Organization
**Processing Pipeline**:
1. **Argument & File Validation** (lines 29-57)
- Help flag detection
- Argument count validation
- File existence and accessibility checks
2. **Image Format Detection** (lines 65-75)
- 8-byte header reading
- Magic number validation via `is_jpeg()` and `is_png()`
3. **Image Loading & Processing** (lines 77-113)
- `stbi_load()` with forced 3-channel RGB output
- Downsampling (step = 10) for performance
- Pixel sampling and filtering
4. **Color Extraction & Analysis** (lines 117-152)
- Color quantization into 16×16×16 buckets (4096 keys)
- Filtering: brightness < 60 and saturation < 10
- Histogram accumulation in `Bucket` structs
- Sorting by frequency (most common first)
5. **Output Generation** (lines 154-192)
- Terminal color display via ANSI codes
- JSON serialization to `palette.json`
**Module Organization**:
- **Data structures**: `Pixel` (RGB color), `Bucket` (histogram accumulator)
- **Core logic**: Everything in `main()` - no separation of concerns
- **Algorithms**: STL-based with lambda functions
- **I/O**: File operations, terminal output, JSON generation
## Code Conventions, Naming Patterns, and Structure
**Naming Conventions**:
- **Variables**: snake_case (`imagefile`), CamelCase (`Pixel`)
- **Constants**: UPPER_SNAKE_CASE (`argv`, `STB_IMAGE_IMPLEMENTATION`)
- **Functions**: snake_case (`is_jpeg`, `is_png`)
- **Namespaces**: Alias-based (`fs` for `std::filesystem`)
**Code Structure Patterns**:
- **Early returns** for error handling
- **Forward declarations** before use
- **Type aliases** for commonly used types
- **Defensive programming** with comprehensive error checking
- **Consistent formatting**: 4-space indentation, no complex macros
**Design Approaches**:
1. **Simplicity**: Single file, minimal abstractions
2. **Performance**: Downsampling, early filtering, efficient algorithms
3. **External management**: stb_image via #define pattern
4. **Error handling**: Defensive with immediate failure on errors
5. **Output flexibility**: Dual format (human-readable + structured JSON)
## Important Functions, Classes, and Entry Points
**Key Functions**:
- `main()` - Entry point and primary orchestrator
- `is_jpeg()`, `is_png()` - Magic number validation functions
- `stbi_load()` - Third-party image decoder (via stb_image.h)
- `stbi_image_free()` - Memory cleanup function
**Data Structures**:
- `Pixel`: Simple RGB color container
```cpp
struct Pixel {
unsigned char r, g, b;
};
```
- `Bucket`: Histogram accumulator
```cpp
struct Bucket {
long r = 0, g = 0, b = 0;
int count = 0;
};
```
**Entry Point**:
- `main(int argc, char *argv[])`: Handles all aspects of the pipeline
**Algorithm Complexity**:
- Time: O(N) where N = downsampled pixels (significantly smaller than original)
- Space: O(1) for fixed-size output, O(4096) for histogram buckets
## Runtime/Tooling Requirements and Dependencies
**Required Tools**:
- **Compiler**: C++17 compliant (g++/clang++)
- **CMake**: Version 3.10+
- **stb_image**: Header-only library
**Runtime Dependencies**:
- **Standard Library**: C++17 filesystem, algorithm, containers, I/O
- **stb_image**: JPEG/PNG decoding capabilities
- **Terminal**: ANSI color escape sequences
**Build Requirements**:
```cmake
cmake_minimum_required(VERSION 3.10)
project(ccolors)
set(CMAKE_CXX_STANDARD 17)
add_executable(ccolors main.cpp)
```
**Platform Support**:
- **Input formats**: JPEG, PNG (via stb_image)
- **Output formats**: Terminal colors + JSON file
- **Platform**: Cross-platform (stb_image handles platform differences)
**Key Dependencies**:
- `stb_image.h`: Single-file image decoding library
- C++17 standard library features
## Code Patterns and Design Approaches
**Programming Patterns**:
1. **Linear Control Flow**: Straightforward execution sequence
2. **Functional Style**: STL algorithms with lambdas
3. **Memory Management**: Manual allocation + explicit free
4. **Output Separation**: Terminal vs. structured data formats
5. **Data Pipeline**: Transformation sequence with multiple stages
**Error Handling Patterns**:
- **Fail-fast**: Immediate return on error conditions
- **Defensive checks**: Validation at each I/O step
- **Early filtering**: Discard invalid data early in pipeline
**Performance Optimizations**:
- **Downsampling**: 10x reduction in sample count
- **Early filtering**: Skip dark and low-saturation pixels
- **Efficient quantization**: 16×16×16 bucket system
- **Fixed-size output**: Limited to top 5 colors
**Architecture Limitations**:
- **Scalability**: Performance degrades with large images
- **Extensibility**: Hardcoded constants (thresholds, sizes)
- **Testability**: Single file makes unit testing difficult
- **Maintainability**: No separation of concerns
**Future Development Considerations**:
- Modularize I/O, image processing, and color extraction
- Make thresholds and sizes configurable
- Add more image format support
- Implement unit tests
- Add command-line options for customization
## Technical Specifications Summary
| Aspect | Detail |
|--------|--------|
| **Language** | C++17 |
| **Design Pattern** | Single-file utility |
| **Image Formats** | JPEG, PNG (via stb_image) |
| **Output Formats** | Terminal colors + JSON |
| **Max Colors** | 5 |
| **Sample Rate** | 10x downsampling |
| **Build System** | CMake 3.10+ |
| **Dependencies** | Standard library + stb_image |
| **Error Handling** | Fail-fast validation |
## Key Insights for Future Work
1. **Maintainability**: Current design sacrifices testability for simplicity
2. **Extensibility**: Hardcoded values limit customization options
3. **Performance**: Algorithms optimized for typical use cases
4. **Architecture**: Clear separation of concerns would improve modularity
5. **Testing**: Single file approach makes comprehensive testing challenging
6. **Dependencies**: External library management could be improved
The codebase exemplifies minimalist engineering - achieves its goal with minimal complexity but at the cost of flexibility and long-term maintainability.
+93 -63
View File
@@ -7,6 +7,7 @@
#include <vector> #include <vector>
#include <unordered_map> #include <unordered_map>
#include <algorithm> #include <algorithm>
#include <iterator>
#include <cstdio> #include <cstdio>
using std::cout; using std::cout;
@@ -15,10 +16,6 @@ namespace fs = std::filesystem;
bool is_jpeg(const unsigned char* buf); bool is_jpeg(const unsigned char* buf);
bool is_png(const unsigned char* buf); bool is_png(const unsigned char* buf);
struct Pixel {
unsigned char r, g, b;
};
struct Bucket { struct Bucket {
long r = 0; long r = 0;
long g = 0; long g = 0;
@@ -27,30 +24,45 @@ struct Bucket {
}; };
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
for (int i = 0; i <= argc - 1; i++) { bool quiet = false;
std::string output_path = "palette.json";
std::string imagefile;
for (int i = 1; i < argc; i++) {
std::string arg = argv[i]; std::string arg = argv[i];
if (arg == "--help" || arg == "-h") { if (arg == "--help" || arg == "-h") {
cout << cout <<
"ccolors - extract color palette from images\n\n" "ccolors - extract color palette from images\n\n"
"Usage:\n" "Usage:\n"
" " << argv[0] << " <image>\n\n" " " << argv[0] << " <image> [options]\n\n"
"Options:\n" "Options:\n"
" -h, --help Show this help page\n\n" " -h, --help Show this help page\n"
" -q, --quiet Suppress terminal output (JSON is still written)\n"
" -o, --output <file> Write JSON palette to <file> (default: palette.json)\n\n"
"Output:\n" "Output:\n"
" Prints dominant colors as HEX and terminal swatches\n" " Prints dominant colors as HEX and terminal swatches\n"
" Also writes palette.json\n"; " Also writes palette.json (or the path given with -o)\n";
return 0; return 0;
} else if (arg == "--quiet" || arg == "-q") {
quiet = true;
} else if ((arg == "--output" || arg == "-o") && i + 1 < argc) {
output_path = argv[++i];
} else if ((arg == "--output" || arg == "-o") && i + 1 >= argc) {
cout << "Option " << arg << " requires an argument.\n";
return 1;
} else if (imagefile.empty()) {
imagefile = arg;
} else {
cout << "Unknown argument: " << arg << "\n";
return 1;
} }
} }
if (argc != 2) { if (imagefile.empty()) {
cout << "Please specify a image file. " << argv[0] << " imagefile.jpg" cout << "Please specify a image file. " << argv[0] << " imagefile.jpg"
<< "\nTry \"" << argv[0] << " --help\" for more information."; << "\nTry \"" << argv[0] << " --help\" for more information.";
return 1; return 1;
} }
std::string imagefile = argv[1];
if (!fs::is_regular_file(imagefile)) { if (!fs::is_regular_file(imagefile)) {
std::cout << "File not found: " << imagefile << "\n"; std::cout << "File not found: " << imagefile << "\n";
return 1; return 1;
@@ -62,22 +74,30 @@ int main(int argc, char *argv[]) {
return 1; return 1;
} }
unsigned char header[8]; // Read the file once; detect the format from the buffer and decode from
file.read(reinterpret_cast<char*>(header), 8); // memory so the image is never read from disk a second time.
std::vector<unsigned char> buffer((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
if (buffer.size() < 8) {
cout << "File is too small to be a valid image!\n";
return 1;
}
const unsigned char* header = buffer.data();
if (is_png(header)) { if (is_png(header)) {
cout << "PNG file detected!\n"; if (!quiet) cout << "PNG file detected!\n";
} else if (is_jpeg(header)) { } else if (is_jpeg(header)) {
cout << "JPEG file detected!\n"; if (!quiet) cout << "JPEG file detected!\n";
} else { } else {
cout << "Unkown image file format!\n"; cout << "Unknown image file format!\n";
return 1; return 1;
} }
int width, height, channels; int width, height, channels;
unsigned char* data = stbi_load( unsigned char* data = stbi_load_from_memory(
argv[1], buffer.data(),
static_cast<int>(buffer.size()),
&width, &width,
&height, &height,
&channels, &channels,
@@ -85,59 +105,62 @@ int main(int argc, char *argv[]) {
); );
if (!data) { if (!data) {
cout << "Failed to load image!\n"; const char* reason = stbi_failure_reason();
cout << "Failed to load image";
if (reason != nullptr) {
cout << ": " << reason;
}
cout << "\n";
return 1; return 1;
} }
cout << "**********\n"; if (!quiet) {
cout << "Loaded image:\n"; cout << "**********\n";
cout << "Width: " << width << "\n"; cout << "Loaded image:\n";
cout << "Height: " << height << "\n"; cout << "Width: " << width << "\n";
cout << "Channels: " << channels << "\n"; cout << "Height: " << height << "\n";
cout << "**********\n"; cout << "Channels: " << channels << "\n";
cout << "**********\n";
std::vector<Pixel> samples;
int step = 10; //downsample rate
for (int y = 0; y< height; y += step) {
for (int x = 0; x < width; x+= step) {
int idx = (y * width + x) * 3;
Pixel p;
p.r = data[idx];
p.g = data[idx + 1];
p.b = data[idx + 2];
samples.push_back(p);
}
} }
cout << "Sampled pixels: " << samples.size() << "\n";
std::unordered_map<int, Bucket> hist; std::unordered_map<int, Bucket> hist;
for (auto& p : samples) { int step = 10; //downsample rate
int brightness = p.r + p.g + p.b; long sampled = 0;
if (brightness < 60) continue; //filtering out dark pixels for (int y = 0; y < height; y += step) {
for (int x = 0; x < width; x += step) {
int idx = (y * width + x) * 3;
int minc = std::min({p.r, p.g, p.b}); unsigned char r = data[idx];
int maxc = std::max({p.r, p.g, p.b}); unsigned char g = data[idx + 1];
if (maxc - minc < 10) continue; //filtering out low saturation unsigned char b = data[idx + 2];
//(gray) colors sampled++;
int rq = p.r / 16; int brightness = r + g + b;
int gq = p.g / 16; if (brightness < 60) continue; //filtering out dark pixels
int bq = p.b / 16;
int key = (rq << 8) | (gq << 4) | bq; int minc = std::min({r, g, b});
int maxc = std::max({r, g, b});
if (maxc - minc < 10) continue; //filtering out low saturation
//(gray) colors
auto& bucket = hist[key]; int rq = r / 16;
bucket.r += p.r; int gq = g / 16;
bucket.g += p.g; int bq = b / 16;
bucket.b += p.b;
bucket.count ++; int key = (rq << 8) | (gq << 4) | bq;
auto& bucket = hist[key];
bucket.r += r;
bucket.g += g;
bucket.b += b;
bucket.count++;
}
} }
if (!quiet) cout << "Sampled pixels: " << sampled << "\n";
std::vector<Bucket> buckets; std::vector<Bucket> buckets;
for (auto& kv : hist) { for (auto& kv : hist) {
@@ -151,10 +174,15 @@ int main(int argc, char *argv[]) {
int paletteSize = std::min(5, (int)buckets.size()); int paletteSize = std::min(5, (int)buckets.size());
std::ofstream json("palette.json"); std::ofstream json(output_path);
if (!json) {
cout << "Cannot open output file: " << output_path << "\n";
stbi_image_free(data);
return 1;
}
json << "{\n"; json << "{\n";
json << " \"image\": \"" << argv[1] << "\",\n"; json << " \"image\": \"" << imagefile << "\",\n";
json << " \"palette\": [\n"; json << " \"palette\": [\n";
for (int i = 0; i < paletteSize; i++) { for (int i = 0; i < paletteSize; i++) {
@@ -163,9 +191,11 @@ int main(int argc, char *argv[]) {
int g = b.g / b.count; int g = b.g / b.count;
int bcol = b.b / b.count; int bcol = b.b / b.count;
printf("\033[48;2;%d;%d;%dm \033[0m #%02X%02X%02X\n", if (!quiet) {
r, g, bcol, printf("\033[48;2;%d;%d;%dm \033[0m #%02X%02X%02X\n",
r, g, bcol); r, g, bcol,
r, g, bcol);
}
json << " { " json << " { "
<< "\"r\": " << r << ", " << "\"r\": " << r << ", "
Executable
+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env python3
"""End-to-end integration test suite for ccolors.
Phase 1: black-box tests against the built `ccolors` binary.
Generates minimal valid (PNG/JPEG) and invalid test images, runs the binary,
and asserts against terminal stdout and the generated palette.json.
Run directly (requires a built binary) or via ctest:
python3 test_runner.py path/to/ccolors
# or from a CMake build dir
ctest --output-on-failure
"""
import json
import os
import re
import shutil
import struct
import subprocess
import sys
import tempfile
import zlib
# A swatch line ends with an ANSI reset escape followed by " #RRGGBB".
HEX_LINE_RE = re.compile(rb"\x1b\[0m #([0-9A-F]{6})\n")
def make_png(path, width, height, rgb):
"""Write a minimal solid-color PNG using only the stdlib (zlib)."""
def chunk(tag, data):
c = tag + data
return struct.pack(">I", len(data)) + c + struct.pack(
">I", zlib.crc32(c) & 0xFFFFFFFF)
raw = b""
row = b"\x00" + bytes(rgb) * width
for _ in range(height):
raw += row
png = b"\x89PNG\r\n\x1a\n"
png += chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0))
png += chunk(b"IDAT", zlib.compress(raw))
png += chunk(b"IEND", b"")
with open(path, "wb") as f:
f.write(png)
def run(binary, *args, cwd=None):
"""Run ccolors and return (returncode, stdout_bytes)."""
proc = subprocess.run([binary] + list(args), capture_output=True, cwd=cwd)
return proc.returncode, proc.stdout
def assert_(cond, msg):
if not cond:
raise AssertionError(msg)
def test_happy_path_png(binary, workdir):
img = os.path.join(workdir, "solid.png")
make_png(img, 40, 40, (200, 100, 50))
rc, out = run(binary, img, cwd=workdir)
assert_(rc == 0, "happy path PNG: expected exit 0, got %d" % rc)
assert_(b"PNG file detected!" in out, "missing PNG detection banner")
assert_(b"Width: 40" in out, "missing width in output")
assert_(b"Height: 40" in out, "missing height in output")
assert_(b"Sampled pixels:" in out, "missing sampled-pixels line")
matches = HEX_LINE_RE.findall(out)
assert_(len(matches) >= 1, "expected at least one hex swatch line")
for hexstr in matches:
assert_(re.fullmatch(r"[0-9A-F]{6}", hexstr.decode()), "malformed hex output")
jpath = os.path.join(workdir, "palette.json")
assert_(os.path.exists(jpath), "palette.json was not written")
with open(jpath) as f:
data = json.load(f)
assert_(data["image"] == img, "palette.json image field mismatch")
palette = data["palette"]
assert_(isinstance(palette, list) and len(palette) <= 5,
"palette must be a list of at most 5 entries")
assert_(len(palette) >= 1, "palette should contain extracted colors")
for entry in palette:
for k in ("r", "g", "b", "hex"):
assert_(k in entry, "palette entry missing key %r" % k)
assert_(re.fullmatch(r"#[0-9A-F]{6}", entry["hex"]),
"palette hex malformed: %r" % entry["hex"])
return len(matches)
def test_happy_path_jpeg(binary, workdir):
convert = shutil.which("convert")
if convert is None:
print(" [skip] JPEG happy-path: ImageMagick 'convert' not available",
file=sys.stderr)
return None
img = os.path.join(workdir, "solid.jpg")
subprocess.run([convert, "-size", "40x40", "xc:#C86432", img],
check=True)
rc, out = run(binary, img, cwd=workdir)
assert_(rc == 0, "happy path JPEG: expected exit 0, got %d" % rc)
assert_(b"JPEG file detected!" in out, "missing JPEG detection banner")
jpath = os.path.join(workdir, "palette.json")
with open(jpath) as f:
data = json.load(f)
assert_(isinstance(data["palette"], list), "JPEG palette not a list")
return len(HEX_LINE_RE.findall(out))
def test_file_not_found(binary, workdir):
missing = os.path.join(workdir, "does_not_exist.jpg")
# Ensure it truly does not exist (isolated temp dir already guarantees it).
if os.path.exists(missing):
os.remove(missing)
rc, out = run(binary, missing, cwd=workdir)
assert_(rc != 0, "file-not-found: expected non-zero exit")
assert_(b"File not found" in out, "missing 'File not found' message")
def test_invalid_format(binary, workdir):
bogus = os.path.join(workdir, "bogus.png")
with open(bogus, "wb") as f:
f.write(b"this is definitely not an image file \x00\x01\x02")
rc, out = run(binary, bogus, cwd=workdir)
assert_(rc != 0, "invalid-format: expected non-zero exit")
assert_(b"Unknown image file format" in out or b"Unkown image file format" in out,
"missing unknown-image-format message")
def test_custom_output(binary, workdir):
img = os.path.join(workdir, "solid.png")
make_png(img, 40, 40, (100, 150, 200))
custom_json = os.path.join(workdir, "custom_palette.json")
rc, out = run(binary, img, "-o", custom_json, cwd=workdir)
assert_(rc == 0, "custom-output: expected exit 0, got %d" % rc)
assert_(os.path.exists(custom_json), "custom JSON file was not written at: %s" % custom_json)
# Default palette.json must NOT be written when -o is given with a different path.
default_json = os.path.join(workdir, "palette.json")
assert_(not os.path.exists(default_json), "default palette.json should not be written when -o is used")
with open(custom_json) as f:
data = json.load(f)
assert_(data["image"] == img, "custom palette.json image field mismatch")
palette = data["palette"]
assert_(isinstance(palette, list) and 1 <= len(palette) <= 5,
"palette must be a non-empty list of at most 5 entries")
def test_custom_output_bad_path(binary, workdir):
img = os.path.join(workdir, "solid.png")
make_png(img, 40, 40, (100, 150, 200))
bad_path = os.path.join(workdir, "nonexistent_dir", "out.json")
rc, out = run(binary, img, "-o", bad_path, cwd=workdir)
assert_(rc != 0, "bad-output-path: expected non-zero exit, got 0")
assert_(b"Cannot open output file" in out,
"missing 'Cannot open output file' error message")
def main():
if len(sys.argv) < 2:
print("usage: %s <path-to-ccolors-binary>" % sys.argv[0], file=sys.stderr)
return 1
binary = os.path.abspath(sys.argv[1])
if not os.path.exists(binary):
print("binary not found: %s" % binary, file=sys.stderr)
return 1
# Each test runs in its own temp dir so palette.json never collides.
tests = [
("happy_path_png", lambda d: test_happy_path_png(binary, d)),
("happy_path_jpeg", lambda d: test_happy_path_jpeg(binary, d)),
("file_not_found", lambda d: test_file_not_found(binary, d)),
("invalid_format", lambda d: test_invalid_format(binary, d)),
("custom_output", lambda d: test_custom_output(binary, d)),
("custom_output_bad_path", lambda d: test_custom_output_bad_path(binary, d)),
]
failures = []
for name, fn in tests:
workdir = tempfile.mkdtemp(prefix="ccolors_test_")
try:
info = fn(workdir)
tail = " (%d color line(s))" % info if isinstance(info, int) else ""
print("PASS %s%s" % (name, tail))
except AssertionError as exc:
print("FAIL %s: %s" % (name, exc))
failures.append(name)
except Exception as exc: # noqa: BLE001 - report any unexpected failure
print("ERROR %s: %r" % (name, exc))
failures.append(name)
finally:
shutil.rmtree(workdir, ignore_errors=True)
if failures:
print("FAILED: %s" % ", ".join(failures))
return 1
print("All tests passed.")
return 0
if __name__ == "__main__":
sys.exit(main())