Implement issue 154: CLI -i flag with image file reading
- Added read_image_as_data_uri() function to base64_util for reading, validating, and base64-encoding image files - Added -i CLI flag to accept image file paths (multiple allowed) - Added -I debug flag for testing image file reading - Image files are validated at startup with clear error messages for missing or unreadable files - Updated help text and README.md documentation - Added test_image_flag.sh with tests for PNG, JPEG, GIF formats, error cases, and multiple -i flags
This commit is contained in:
@@ -73,6 +73,7 @@ configuration.
|
||||
* `-p <file>`: Pretty-print a conversation JSON file without running the harness.
|
||||
* `-t <tool>`: Manually constrain the harness to a specific subset of tools (by default no tools are provided to the model)
|
||||
* `-s <name>`: Manually constrain the harness to a specific subset of skills (can be specified multiple times)
|
||||
* `-i <path>`: Image file to attach to the prompt (can be specified multiple times)
|
||||
* `-m <prompt>`: The user prompt to send to the agent.
|
||||
* `-M <model>`: Override the Ollama model (e.g., `mistral`, `llama3`).
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <expected>
|
||||
#include <vector>
|
||||
|
||||
// Base64 encode binary data to base64 string
|
||||
std::string base64_encode(const std::string& input);
|
||||
@@ -13,3 +14,6 @@ std::string get_mime_type_from_extension(const std::string& extension);
|
||||
|
||||
// Generate data URI prefix with base64 data
|
||||
std::string generate_data_uri(const std::string& extension, const std::string& base64_data);
|
||||
|
||||
// Read an image file and return it as a base64 data URI
|
||||
std::expected<std::string, std::string> read_image_as_data_uri(const std::string& file_path);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "base64_util.hpp"
|
||||
#include <unordered_map>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
|
||||
static const std::string base64_chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
@@ -128,3 +130,36 @@ std::string generate_data_uri(const std::string& extension, const std::string& b
|
||||
std::string mime_type = get_mime_type_from_extension(extension);
|
||||
return "data:image/" + mime_type + ";base64," + base64_data;
|
||||
}
|
||||
|
||||
std::expected<std::string, std::string> read_image_as_data_uri(const std::string& file_path) {
|
||||
// Check if file exists
|
||||
if (!std::filesystem::exists(file_path)) {
|
||||
return std::unexpected("Image file not found: " + file_path);
|
||||
}
|
||||
|
||||
// Check if it's a regular file
|
||||
if (!std::filesystem::is_regular_file(file_path)) {
|
||||
return std::unexpected("Not a regular file: " + file_path);
|
||||
}
|
||||
|
||||
// Open and read the file
|
||||
std::ifstream file(file_path, std::ios::binary);
|
||||
if (!file.is_open()) {
|
||||
return std::unexpected("Cannot open image file: " + file_path);
|
||||
}
|
||||
|
||||
// Read file contents
|
||||
std::string content((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
file.close();
|
||||
|
||||
// Get file extension
|
||||
std::filesystem::path p(file_path);
|
||||
std::string extension = p.extension().string();
|
||||
|
||||
// Base64 encode the content
|
||||
std::string encoded = base64_encode(content);
|
||||
|
||||
// Generate data URI
|
||||
return generate_data_uri(extension, encoded);
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ int main(int argc, char** argv) {
|
||||
char const* test_mime_type = nullptr;
|
||||
char const* test_data_uri_ext = nullptr;
|
||||
char const* test_data_uri_b64 = nullptr;
|
||||
char const* test_image_file = nullptr;
|
||||
|
||||
ToolRegistry master_registery;
|
||||
ToolRegistry agent_tools;
|
||||
@@ -132,6 +133,7 @@ int main(int argc, char** argv) {
|
||||
std::vector<std::string> tools;
|
||||
bool full_history = false;
|
||||
std::vector<std::string> skill_filters;
|
||||
std::vector<std::string> image_paths;
|
||||
|
||||
consume_arg(); // progname
|
||||
|
||||
@@ -252,6 +254,16 @@ int main(int argc, char** argv) {
|
||||
consume_arg();
|
||||
skill_filters.push_back(consume_arg());
|
||||
}
|
||||
else if (argv[0] == std::string("-i"))
|
||||
{
|
||||
consume_arg();
|
||||
image_paths.push_back(consume_arg());
|
||||
}
|
||||
else if (argv[0] == std::string("-I"))
|
||||
{
|
||||
consume_arg();
|
||||
test_image_file = consume_arg();
|
||||
}
|
||||
else if (argv[0] == std::string("-h"))
|
||||
{
|
||||
std::cout << "Usage: drharness [options]\n"
|
||||
@@ -260,6 +272,7 @@ int main(int argc, char** argv) {
|
||||
<< " -p <file> Pretty print a saved conversation JSON file\n"
|
||||
<< " -t <tool> Tool name (can be specified multiple times)\n"
|
||||
<< " -s <name> Skill name to enable (can be specified multiple times)\n"
|
||||
<< " -i <path> Image file to attach (can be specified multiple times)\n"
|
||||
<< " -n Start a new conversation (auto-generates temp file if -F not specified)\n"
|
||||
<< " -m <prompt> Prompt message (required)\n"
|
||||
<< " -M <model> Ollama model name\n"
|
||||
@@ -277,6 +290,7 @@ int main(int argc, char** argv) {
|
||||
<< " -b <string> Debug: test base64 decode and exit\n"
|
||||
<< " -T <ext> Debug: test MIME type detection and exit\n"
|
||||
<< " -U <ext> <b64> Debug: test data URI generation and exit\n"
|
||||
<< " -I <file> Debug: test image file reading and exit\n"
|
||||
<< " -d Use debug client (mock API via stdin)\n"
|
||||
<< " -h Display this help menu\n";
|
||||
exit(0);
|
||||
@@ -323,6 +337,17 @@ int main(int argc, char** argv) {
|
||||
|
||||
expect_or_exit(master_registery.register_tool(make_skill_tool(enabled_skills)));
|
||||
|
||||
// Validate and read image files
|
||||
std::vector<std::string> image_data_uris;
|
||||
for (const auto& path : image_paths) {
|
||||
auto result = read_image_as_data_uri(path);
|
||||
if (!result) {
|
||||
std::cerr << "Error: " << result.error() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
image_data_uris.push_back(result.value());
|
||||
}
|
||||
|
||||
if (debug_paths) {
|
||||
print_discovered_paths(discovered_paths);
|
||||
return 0;
|
||||
@@ -412,6 +437,16 @@ int main(int argc, char** argv) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (test_image_file) {
|
||||
auto result = read_image_as_data_uri(test_image_file);
|
||||
if (!result) {
|
||||
std::cerr << "Error: " << result.error() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
std::cout << result.value() << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (list_models)
|
||||
{
|
||||
auto models_result = create_client(use_debug)->list_models();
|
||||
|
||||
@@ -56,3 +56,6 @@ add_dr_test(test_expected_errors)
|
||||
|
||||
# Add base64 encoding tests
|
||||
add_dr_test(test_base64_encode)
|
||||
|
||||
# Add image file reading tests
|
||||
add_dr_test(test_image_flag)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
# test_image_flag.sh - Tests for -i CLI flag with image file reading
|
||||
|
||||
# Test 1: Nonexistent file produces clear error and non-zero exit
|
||||
OUTPUT=$("${DRHARNESS}" -I "nonexistent.png" 2>&1) || true
|
||||
if ! echo "${OUTPUT}" | grep -q "Image file not found"; then
|
||||
echo "FAIL: nonexistent file error - expected 'Image file not found', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 2: Create a test image file and read it
|
||||
# Create a minimal 1x1 PNG file
|
||||
printf '\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53\xDE\x00\x00\x00\x0C\x49\x44\x41\x54\x08\xD7\x63\xF8\xCF\xC0\x00\x00\x03\x01\x01\x00\x18\xDD\x8D\xB4\x00\x00\x00\x00\x49\x45\x4E\x44\xAE\x42\x60\x82' > "${TEST_TEMP_DIR}/test.png"
|
||||
|
||||
# Test 3: Valid PNG file is read and returns data URI
|
||||
OUTPUT=$("${DRHARNESS}" -I "${TEST_TEMP_DIR}/test.png" 2>&1)
|
||||
if ! echo "${OUTPUT}" | grep -q "^data:image/png;base64,"; then
|
||||
echo "FAIL: valid PNG file - expected data URI starting with 'data:image/png;base64,', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 4: Create a minimal JPEG file
|
||||
printf '\xFF\xD8\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xFF\xD9' > "${TEST_TEMP_DIR}/test.jpg"
|
||||
|
||||
# Test 5: Valid JPG file is read and returns correct data URI
|
||||
OUTPUT=$("${DRHARNESS}" -I "${TEST_TEMP_DIR}/test.jpg" 2>&1)
|
||||
if ! echo "${OUTPUT}" | grep -q "^data:image/jpeg;base64,"; then
|
||||
echo "FAIL: valid JPG file - expected data URI starting with 'data:image/jpeg;base64,', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 6: Create a GIF file
|
||||
printf '\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00\x00\xFF\xFF\xFF\x00\x00\x00\x21\xF9\x04\x01\x00\x00\x00\x00\x2C\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02\x44\x01\x00\x3B' > "${TEST_TEMP_DIR}/test.gif"
|
||||
|
||||
# Test 7: Valid GIF file is read and returns correct data URI
|
||||
OUTPUT=$("${DRHARNESS}" -I "${TEST_TEMP_DIR}/test.gif" 2>&1)
|
||||
if ! echo "${OUTPUT}" | grep -q "^data:image/gif;base64,"; then
|
||||
echo "FAIL: valid GIF file - expected data URI starting with 'data:image/gif;base64,', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 8: Not a file (directory) produces error
|
||||
mkdir -p "${TEST_TEMP_DIR}/not_a_file"
|
||||
OUTPUT=$("${DRHARNESS}" -I "${TEST_TEMP_DIR}/not_a_file" 2>&1) || true
|
||||
if ! echo "${OUTPUT}" | grep -q "Not a regular file"; then
|
||||
echo "FAIL: directory error - expected 'Not a regular file', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 9: Multiple images - second file nonexistent should produce error
|
||||
printf '\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53\xDE\x00\x00\x00\x0C\x49\x44\x41\x54\x08\xD7\x63\xF8\xCF\xC0\x00\x00\x03\x01\x01\x00\x18\xDD\x8D\xB4\x00\x00\x00\x00\x49\x45\x4E\x44\xAE\x42\x60\x82' > "${TEST_TEMP_DIR}/valid.png"
|
||||
OUTPUT=$("${DRHARNESS}" -i "${TEST_TEMP_DIR}/valid.png" -i "nonexistent.png" -F "${TEST_TEMP_DIR}/chat.json" -n -m "test" 2>&1) || true
|
||||
if ! echo "${OUTPUT}" | grep -q "Image file not found"; then
|
||||
echo "FAIL: multiple images with nonexistent second file - expected error, got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All image flag tests passed"
|
||||
Reference in New Issue
Block a user