From e65f953bd67a3b11d82a3153f393ea792639c00b Mon Sep 17 00:00:00 2001 From: Kitzy Date: Wed, 14 Jan 2026 12:29:31 -0500 Subject: [PATCH] Fix icon sizing bug where icons were created at 2x requested size - Fixed bitmap creation to use explicit pixel dimensions - Removed NSImage.size setting that caused Retina scaling issues - Added dimension verification tests using sips - All icons now created at exact requested dimensions Fixes #3 --- CHANGELOG.md | 8 ++++++- icongrabber-cli/main.swift | 40 ++++++++++++++++++++------------ tests/run_tests.sh | 47 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cace4b..001db66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Added +### Fixed +- Fixed icon sizing bug where output images were created at 2× the requested dimensions (e.g., 512px request would produce 1024px output) ([#3](https://github.com/macadmins/icongrabber/issues/3)) +- Corrected bitmap creation to use explicit pixel dimensions instead of relying on NSImage.size which was affected by Retina scaling + +### Changed +- Improved test suite with pixel dimension verification using `sips` to ensure icons are created at exact requested sizes +- Added 4 new dimension verification tests covering standard icon sizes (64×64, 256×256, 512×512, 1024×1024) --- diff --git a/icongrabber-cli/main.swift b/icongrabber-cli/main.swift index 54921bd..b331763 100644 --- a/icongrabber-cli/main.swift +++ b/icongrabber-cli/main.swift @@ -234,10 +234,6 @@ func extractIcon(from appPath: String, size: Int) throws -> NSImage { // Get the icon for the application let icon = NSWorkspace.shared.icon(forFile: url.path) - // Set the size - let iconSize = NSSize(width: size, height: size) - icon.size = iconSize - return icon } @@ -245,25 +241,39 @@ func saveIconAsPNG(_ icon: NSImage, to outputPath: String, size: Int) throws { let url = URL(fileURLWithPath: outputPath) let iconSize = NSSize(width: size, height: size) - // Create a bitmap representation - guard let tiffData = icon.tiffRepresentation, - let _ = NSBitmapImageRep(data: tiffData) else { + // Create a bitmap representation with exact pixel dimensions + guard let bitmapRep = NSBitmapImageRep( + bitmapDataPlanes: nil, + pixelsWide: size, + pixelsHigh: size, + bitsPerSample: 8, + samplesPerPixel: 4, + hasAlpha: true, + isPlanar: false, + colorSpaceName: .deviceRGB, + bytesPerRow: 0, + bitsPerPixel: 0 + ) else { throw CLIError.exportFailed("Could not create bitmap representation") } - // Resize to exact dimensions - let resizedImage = NSImage(size: iconSize) - resizedImage.lockFocus() + // Draw the icon into the bitmap context at exact pixel dimensions + NSGraphicsContext.saveGraphicsState() + guard let context = NSGraphicsContext(bitmapImageRep: bitmapRep) else { + throw CLIError.exportFailed("Could not create graphics context") + } + NSGraphicsContext.current = context + + // Draw icon scaled to exact size icon.draw(in: NSRect(origin: .zero, size: iconSize), - from: NSRect(origin: .zero, size: icon.size), + from: NSRect.zero, operation: .copy, fraction: 1.0) - resizedImage.unlockFocus() + + NSGraphicsContext.restoreGraphicsState() // Convert to PNG - guard let resizedTiff = resizedImage.tiffRepresentation, - let resizedBitmap = NSBitmapImageRep(data: resizedTiff), - let pngData = resizedBitmap.representation(using: .png, properties: [:]) else { + guard let pngData = bitmapRep.representation(using: .png, properties: [:]) else { throw CLIError.exportFailed("Could not create PNG data") } diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 3f9695e..19eff39 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -80,6 +80,25 @@ assert_exit_code() { fi } +assert_image_dimensions() { + local file=$1 + local expected_width=$2 + local expected_height=$3 + if [ -f "$file" ]; then + # Use sips to get actual pixel dimensions + local width=$(sips -g pixelWidth "$file" 2>/dev/null | grep pixelWidth | awk '{print $2}') + local height=$(sips -g pixelHeight "$file" 2>/dev/null | grep pixelHeight | awk '{print $2}') + + if [ "$width" = "$expected_width" ] && [ "$height" = "$expected_height" ]; then + pass_test + else + fail_test "Expected ${expected_width}x${expected_height}, got ${width}x${height}" + fi + else + fail_test "File $file does not exist" + fi +} + assert_file_size_under() { local file=$1 local max_kb=$2 @@ -145,16 +164,28 @@ print_test 2 "Custom size extraction (256x256)" $CLI "$TEST_APP" -s 256 -o test_256.png -f assert_file_exists "test_256.png" +# Test 2a: Verify 256x256 dimensions +print_test "2a" "Verify 256x256 pixel dimensions" +assert_image_dimensions "test_256.png" 256 256 + # Test 3: Small size (64x64) print_test 3 "Small icon extraction (64x64)" $CLI "$TEST_APP" -s 64 -o test_64.png -f assert_file_exists "test_64.png" +# Test 3a: Verify 64x64 dimensions +print_test "3a" "Verify 64x64 pixel dimensions" +assert_image_dimensions "test_64.png" 64 64 + # Test 4: Large size (1024x1024) print_test 4 "Large icon extraction (1024x1024)" $CLI "$TEST_APP" -s 1024 -o test_1024.png -f assert_file_exists "test_1024.png" +# Test 4a: Verify 1024x1024 dimensions +print_test "4a" "Verify 1024x1024 pixel dimensions" +assert_image_dimensions "test_1024.png" 1024 1024 + # Test 5: File size validation (should be reasonable) print_test 5 "Output file size validation" assert_file_size "test_basic.png" 1000 @@ -232,6 +263,22 @@ else fail_test "Some size extractions failed" fi +# Test 13a: Verify all standard size dimensions are correct +print_test "13a" "Verify all standard sizes have correct dimensions" +all_correct=true +for size in "${sizes[@]}"; do + width=$(sips -g pixelWidth "test_${size}.png" 2>/dev/null | grep pixelWidth | awk '{print $2}') + height=$(sips -g pixelHeight "test_${size}.png" 2>/dev/null | grep pixelHeight | awk '{print $2}') + if [ "$width" != "$size" ] || [ "$height" != "$size" ]; then + fail_test "Size ${size}: Expected ${size}x${size}, got ${width}x${height}" + all_correct=false + break + fi +done +if [ "$all_correct" = true ]; then + pass_test +fi + # Test 14: Force flag to overwrite without prompt print_test 14 "Force flag overwrites without prompting" $CLI "$TEST_APP" -o test_overwrite.png -s 128