Understanding imtools internals for contributors and maintainers.
- Project Structure
- Design Philosophy
- Code Architecture
- Adding a New Command
- Adding Image Format Support
- Testing
- Contributing Guidelines
imtools/
├── src/
│ └── main.zig # Single-file implementation (~1900 lines)
├── docs/
│ ├── installation.md # Installation guide
│ ├── commands.md # Command reference
│ ├── architecture.md # This file
│ ├── packaging.md # Packaging guide
│ └── ai-sorting.md # AI sorting guide
├── build.zig # Zig build configuration
├── imtools-1.0.0.ebuild # Gentoo stable ebuild
├── imtools-9999.ebuild # Gentoo live ebuild
├── README.md # Project overview
├── CLAUDE.md # AI assistant context
├── LICENSE # MIT license
└── .gitignore
imtools is intentionally a single-file Zig program. This provides:
- Simplicity - Easy to understand the entire codebase
- Portability - No complex build systems or dependencies
- Fast Compilation - Single compilation unit
- Easy Distribution - One source file to share
Image dimension parsing is implemented natively by reading binary headers:
- Why? Avoids dependency on ImageMagick, libpng, libjpeg, etc.
- Trade-off: Only extracts dimensions, not full image decoding
- Benefit: Extremely fast, no library compatibility issues
For operations requiring full image processing:
- ffmpeg - Image format conversion (battle-tested, universal)
- curl - HTTP requests (reliable, widely available)
- ollama - AI vision (local, privacy-preserving)
const ImageType = enum {
png,
jpeg,
gif,
bmp,
webp,
tiff,
unknown,
fn fromExtension(ext: []const u8) ImageType {
// Case-insensitive extension matching
}
fn isImage(filename: []const u8) bool {
// Check if file has image extension
}
};Key insight: Extension-based detection is used for filtering, but actual format is verified when reading headers.
Each format has specific header parsing:
fn getImageDimensions(allocator: mem.Allocator, file_path: []const u8) !ImageDimensions {
// Read first 512 bytes (sufficient for all format headers)
var header_buf: [512]u8 = undefined;
const bytes_read = try file.read(&header_buf);
// PNG: Dimensions at bytes 16-23 after 8-byte signature
if (mem.eql(u8, header[0..8], &[_]u8{ 0x89, 0x50, 0x4E, 0x47, ... })) {
const width = readU32BE(header, 16);
const height = readU32BE(header, 20);
return ImageDimensions{ .width = width, .height = height };
}
// JPEG: Scan for SOF0/SOF2 markers
// GIF: Dimensions at bytes 6-9
// BMP: Dimensions at bytes 18-25
// WebP: Multiple chunk formats (VP8, VP8L, VP8X)
// ...
}Binary reading helpers:
fn readU16BE(data: []const u8, offset: usize) u16 // Big-endian
fn readU32BE(data: []const u8, offset: usize) u32
fn readU16LE(data: []const u8, offset: usize) u16 // Little-endian
fn readU32LE(data: []const u8, offset: usize) u32Each command is a standalone function:
fn flattenImages(allocator: mem.Allocator, dry_run: bool) !void
fn findDuplicates(allocator: mem.Allocator, delete_mode: bool) !void
fn deletePortraitImages(allocator: mem.Allocator, dry_run: bool) !void
fn removeEmptyDirs(allocator: mem.Allocator, dry_run: bool) !void
fn convertToPng(allocator: mem.Allocator, dry_run: bool, delete_original: bool) !void
fn downloadWallpapers(allocator: mem.Allocator, query: []const u8, limit: usize, output_dir: []const u8) !void
fn sortImages(allocator: mem.Allocator, config: SortConfig) !voidCommon patterns:
- Open current directory with walker
- Filter for image files
- Process each file
- Track counts (processed, errors, skipped)
- Print summary
Subprocess execution pattern:
const result = std.process.Child.run(.{
.allocator = allocator,
.argv = &[_][]const u8{ "ffmpeg", "-i", input, "-y", output },
.max_output_bytes = 64 * 1024,
}) catch |err| {
// Handle spawn error
};
defer allocator.free(result.stdout);
defer allocator.free(result.stderr);
// Check exit code properly (tagged union)
const success = switch (result.term) {
.Exited => |code| code == 0,
else => false,
};Important: Never use result.term.Exited directly - it's a tagged union and will fail at runtime.
fn myNewCommand(allocator: mem.Allocator, some_option: bool) !void {
std.debug.print("Running my new command...\n", .{});
var dir = try fs.cwd().openDir(".", .{ .iterate = true });
defer dir.close();
var walker = try dir.walk(allocator);
defer walker.deinit();
while (try walker.next()) |entry| {
if (entry.kind != .file) continue;
if (!ImageType.isImage(entry.basename)) continue;
// Your logic here
}
std.debug.print("\nDone!\n", .{});
}fn printUsage() void {
std.debug.print(
\\...
\\ my-command Description of my command
\\...
, .{});
}} else if (mem.eql(u8, command, "my-command")) {
try myNewCommand(allocator, some_option);
}// In argument parsing loop
} else if (mem.eql(u8, arg, "--my-option")) {
my_option = true;
}const ImageType = enum {
png,
jpeg,
// ... existing
avif, // New format
unknown,
fn fromExtension(ext: []const u8) ImageType {
// ... existing
if (mem.eql(u8, lower, ".avif")) return .avif;
return .unknown;
}
};Research the format's binary structure and add to getImageDimensions():
// AVIF: Based on ISOBMFF container
// (simplified - actual AVIF parsing is more complex)
if (bytes_read >= 12 and mem.eql(u8, header[4..12], "ftypavif")) {
// Parse AVIF structure for dimensions
}Usually no changes needed - ffmpeg auto-detects input format.
# Create test directory with sample images
mkdir test-images
cd test-images
# Add test images of various formats
# Test each command
../zig-out/bin/imtools flatten --dry-run
../zig-out/bin/imtools find-duplicates
../zig-out/bin/imtools delete-portrait --dry-run- Empty directories
- Deeply nested directories
- Filenames with spaces and special characters
- Corrupted image headers
- Very large files
- Mixed image formats
- Follow Zig standard library conventions
- Use descriptive variable names
- Add comments for complex logic
- Keep functions focused and single-purpose
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Make your changes
- Test thoroughly
- Commit with clear messages
- Push and create PR
Add AVIF format support
- Add .avif extension detection to ImageType
- Implement AVIF header parsing for dimensions
- Update documentation
- New image format support
- Performance improvements
- Bug fixes
- Documentation improvements
- Packaging for new distributions
- Adding heavy dependencies
- Breaking single-file architecture (unless very compelling reason)
- Platform-specific code (keep cross-platform)
- Packaging Guide - Create packages for your distribution
- Command Reference - Understand all commands