← back to Resize It

scripts/resize_image.py

183 lines

#!/usr/bin/env python3
"""
Image resizing script for Resize It skill
Resizes images to 24" width at 150 DPI while maintaining aspect ratio
"""

import sys
import json
from pathlib import Path
from PIL import Image
import argparse


def calculate_dimensions(target_width_inches=24, dpi=150):
    """Calculate target pixel dimensions"""
    target_width_px = int(target_width_inches * dpi)
    return target_width_px


def analyze_resize_quality(original_width, original_height, target_width):
    """Analyze if resize will degrade quality"""
    scale_factor = target_width / original_width

    result = {
        "original_width": original_width,
        "original_height": original_height,
        "target_width": target_width,
        "target_height": int(original_height * scale_factor),
        "scale_factor": scale_factor,
        "is_upscaling": scale_factor > 1,
        "quality_warning": None
    }

    if scale_factor > 1:
        upscale_percentage = (scale_factor - 1) * 100
        result["quality_warning"] = (
            f"⚠️  WARNING: Image will be upscaled by {upscale_percentage:.1f}%. "
            f"This may result in quality degradation and pixelation. "
            f"Original: {original_width}x{original_height}px, "
            f"Target: {result['target_width']}x{result['target_height']}px"
        )
    elif scale_factor < 0.5:
        downscale_percentage = (1 - scale_factor) * 100
        result["quality_warning"] = (
            f"ℹ️  INFO: Image will be significantly downscaled by {downscale_percentage:.1f}%. "
            f"Some detail will be lost. "
            f"Original: {original_width}x{original_height}px, "
            f"Target: {result['target_width']}x{result['target_height']}px"
        )

    return result


def resize_image(input_path, output_path=None, target_width_inches=24, dpi=150, quality=95):
    """
    Resize image to specified width in inches at given DPI

    Args:
        input_path: Path to input image
        output_path: Path for output image (optional, will generate if not provided)
        target_width_inches: Target width in inches (default 24")
        dpi: Dots per inch (default 150)
        quality: JPEG quality 1-100 (default 95)

    Returns:
        dict with result information
    """
    try:
        # Open image
        img = Image.open(input_path)
        original_format = img.format
        original_width, original_height = img.size

        # Calculate target dimensions
        target_width_px = calculate_dimensions(target_width_inches, dpi)

        # Analyze quality impact
        analysis = analyze_resize_quality(original_width, original_height, target_width_px)

        # Calculate target height maintaining aspect ratio
        aspect_ratio = original_height / original_width
        target_height_px = int(target_width_px * aspect_ratio)

        # Perform resize using high-quality resampling
        # Try newer API first, fall back to older API
        try:
            resample_method = Image.Resampling.LANCZOS
        except AttributeError:
            resample_method = Image.LANCZOS

        resized_img = img.resize((target_width_px, target_height_px), resample_method)

        # Generate output path if not provided
        if output_path is None:
            input_path_obj = Path(input_path)
            output_path = str(input_path_obj.parent / f"{input_path_obj.stem}_24x{target_height_px//dpi}in_150dpi{input_path_obj.suffix}")

        # Save with DPI metadata
        save_kwargs = {'dpi': (dpi, dpi)}

        # Add format-specific options
        if original_format in ['JPEG', 'JPG'] or output_path.lower().endswith(('.jpg', '.jpeg')):
            save_kwargs['quality'] = quality
            save_kwargs['optimize'] = True
        elif original_format == 'PNG' or output_path.lower().endswith('.png'):
            save_kwargs['optimize'] = True

        resized_img.save(output_path, **save_kwargs)

        # Build result
        result = {
            "success": True,
            "input_path": str(input_path),
            "output_path": str(output_path),
            "original_dimensions": {"width": original_width, "height": original_height},
            "output_dimensions": {"width": target_width_px, "height": target_height_px},
            "physical_dimensions": {
                "width_inches": target_width_inches,
                "height_inches": target_height_px / dpi,
                "dpi": dpi
            },
            "scale_factor": analysis["scale_factor"],
            "quality_warning": analysis["quality_warning"]
        }

        return result

    except Exception as e:
        return {
            "success": False,
            "error": str(e),
            "input_path": str(input_path)
        }


def main():
    parser = argparse.ArgumentParser(
        description='Resize image to 24" width at 150 DPI maintaining aspect ratio'
    )
    parser.add_argument('input', help='Input image path')
    parser.add_argument('-o', '--output', help='Output image path (optional)')
    parser.add_argument('-w', '--width', type=float, default=24,
                        help='Target width in inches (default: 24)')
    parser.add_argument('-d', '--dpi', type=int, default=150,
                        help='DPI resolution (default: 150)')
    parser.add_argument('-q', '--quality', type=int, default=95,
                        help='JPEG quality 1-100 (default: 95)')
    parser.add_argument('--json', action='store_true',
                        help='Output result as JSON')

    args = parser.parse_args()

    # Perform resize
    result = resize_image(
        args.input,
        args.output,
        args.width,
        args.dpi,
        args.quality
    )

    if args.json:
        print(json.dumps(result, indent=2))
    else:
        if result["success"]:
            print(f"✅ Image resized successfully!")
            print(f"   Input: {result['input_path']}")
            print(f"   Output: {result['output_path']}")
            print(f"   Original: {result['original_dimensions']['width']}x{result['original_dimensions']['height']}px")
            print(f"   Resized: {result['output_dimensions']['width']}x{result['output_dimensions']['height']}px")
            print(f"   Physical: {result['physical_dimensions']['width_inches']:.1f}\" x {result['physical_dimensions']['height_inches']:.1f}\" @ {result['physical_dimensions']['dpi']} DPI")
            print(f"   Scale: {result['scale_factor']:.2f}x")

            if result.get('quality_warning'):
                print(f"\n{result['quality_warning']}")
        else:
            print(f"❌ Error: {result['error']}")
            sys.exit(1)


if __name__ == '__main__':
    main()