imgstat 1.0.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/bin/imgstat +108 -0
- package/build_deb.sh +48 -0
- package/imagestat +46 -3
- package/install.sh +68 -0
- package/lib/analyze.sh +93 -0
- package/lib/inspect.sh +33 -0
- package/lib/remote.sh +59 -0
- package/lib/rename.sh +70 -0
- package/lib/scan.sh +15 -0
- package/lib/ui.sh +83 -0
- package/lib/utils.sh +64 -0
- package/package.json +10 -22
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# imgstat
|
|
2
|
+
|
|
3
|
+
imgstat is a CLI tool that embeds image dimensions directly into filenames, or analyzes remote imagery, to give AI context without needing external parsers.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
imgstat handles renaming smoothly and idempotently—it will never re-append dimensions to a file that already has them. When dealing with remote imagery from URLs or scanning your codebase, it securely generates dimension reports without leaving permanent downloads on your machine. For AI integration, the `analyze` mode seamlessly builds an `.agent/rules/image_dimensions.md` file, giving your local language models instant, zero-config context about the images used in your project.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Run `imgstat` with no arguments to get an interactive menu. You will be prompted to select the mode you want to use.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
imgstat
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Contribution Rules
|
|
18
|
+
|
|
19
|
+
Keep the tool small. If you are considering adding a feature, ask: **does this help AI understand images faster?** If the answer is not clearly yes, it probably does not belong here.
|
|
20
|
+
|
|
21
|
+
**Every file in `lib/` must have one clear responsibility.** If you find yourself writing image discovery logic inside `rename.sh`, stop and move it to `scan.sh`.
|
|
22
|
+
|
|
23
|
+
**No feature should require memorizing new flags.** If it can be handled by a mode or an interactive prompt, prefer that.
|
|
24
|
+
|
|
25
|
+
**Remote mode must never leave files on disk.** The `trap` cleanup is non-negotiable.
|
|
26
|
+
|
|
27
|
+
**Dry-run must work for any operation that touches files.** This is a safety contract with users.
|
package/bin/imgstat
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
LIB_DIR="$DIR/../lib"
|
|
7
|
+
|
|
8
|
+
source "$LIB_DIR/utils.sh"
|
|
9
|
+
|
|
10
|
+
# Ensure identify is available
|
|
11
|
+
require_command identify
|
|
12
|
+
|
|
13
|
+
# Parse global flags and modes
|
|
14
|
+
MODE=""
|
|
15
|
+
TARGET=""
|
|
16
|
+
DRY_RUN="false"
|
|
17
|
+
YES_FLAG="false"
|
|
18
|
+
|
|
19
|
+
# Simple parser
|
|
20
|
+
while [[ $# -gt 0 ]]; do
|
|
21
|
+
case $1 in
|
|
22
|
+
inspect|rename|remote|analyze)
|
|
23
|
+
if [[ -n "$MODE" ]]; then
|
|
24
|
+
echo "Error: Multiple modes specified ($MODE and $1)."
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
MODE="$1"
|
|
28
|
+
shift
|
|
29
|
+
;;
|
|
30
|
+
--dry-run)
|
|
31
|
+
DRY_RUN="true"
|
|
32
|
+
shift
|
|
33
|
+
;;
|
|
34
|
+
--yes|-y)
|
|
35
|
+
YES_FLAG="true"
|
|
36
|
+
shift
|
|
37
|
+
;;
|
|
38
|
+
--help|-h)
|
|
39
|
+
echo "imgstat — Embed image dimensions directly into filenames."
|
|
40
|
+
echo "Usage: imgstat [mode] [target] [options]"
|
|
41
|
+
echo ""
|
|
42
|
+
echo "Modes:"
|
|
43
|
+
echo " (none) Interactive menu"
|
|
44
|
+
echo " inspect Print dimensions of local directory images"
|
|
45
|
+
echo " rename Rename local files (e.g. image-800x600.jpg)"
|
|
46
|
+
echo " remote Print dimensions of images from a URL"
|
|
47
|
+
echo " analyze Scan codebase for URLs, write to .agent/rules/image_dimensions.md"
|
|
48
|
+
echo ""
|
|
49
|
+
echo "Options:"
|
|
50
|
+
echo " --dry-run Show what would happen without making changes"
|
|
51
|
+
echo " --yes, -y Skip confirmation prompts (for rename)"
|
|
52
|
+
echo " --help, -h Show this help message"
|
|
53
|
+
exit 0
|
|
54
|
+
;;
|
|
55
|
+
-*)
|
|
56
|
+
echo "Unknown flag: $1"
|
|
57
|
+
exit 1
|
|
58
|
+
;;
|
|
59
|
+
*)
|
|
60
|
+
if [[ -z "$TARGET" ]]; then
|
|
61
|
+
TARGET="$1"
|
|
62
|
+
else
|
|
63
|
+
echo "Error: Unexpected argument: $1"
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|
|
66
|
+
shift
|
|
67
|
+
;;
|
|
68
|
+
esac
|
|
69
|
+
done
|
|
70
|
+
|
|
71
|
+
if [[ -z "$MODE" ]]; then
|
|
72
|
+
if [[ -n "$TARGET" ]]; then
|
|
73
|
+
echo "Error: A target was provided without a mode."
|
|
74
|
+
echo "Usage: imgstat [mode] [target] [--dry-run] [--yes]"
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
77
|
+
source "$LIB_DIR/ui.sh"
|
|
78
|
+
cmd_ui
|
|
79
|
+
exit 0
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# Fallback target if none provided
|
|
83
|
+
if [[ "$MODE" == "inspect" || "$MODE" == "rename" || "$MODE" == "analyze" ]]; then
|
|
84
|
+
TARGET="${TARGET:-./}"
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
case "$MODE" in
|
|
88
|
+
inspect)
|
|
89
|
+
source "$LIB_DIR/inspect.sh"
|
|
90
|
+
cmd_inspect "$TARGET"
|
|
91
|
+
;;
|
|
92
|
+
rename)
|
|
93
|
+
source "$LIB_DIR/rename.sh"
|
|
94
|
+
cmd_rename "$TARGET" "$DRY_RUN" "$YES_FLAG"
|
|
95
|
+
;;
|
|
96
|
+
remote)
|
|
97
|
+
if [[ -z "$TARGET" ]]; then
|
|
98
|
+
echo "Error: URL target required for remote mode."
|
|
99
|
+
exit 1
|
|
100
|
+
fi
|
|
101
|
+
source "$LIB_DIR/remote.sh"
|
|
102
|
+
cmd_remote "$TARGET" "$DRY_RUN"
|
|
103
|
+
;;
|
|
104
|
+
analyze)
|
|
105
|
+
source "$LIB_DIR/analyze.sh"
|
|
106
|
+
cmd_analyze "$TARGET"
|
|
107
|
+
;;
|
|
108
|
+
esac
|
package/build_deb.sh
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
PACKAGE_NAME="imagestat"
|
|
3
|
+
VERSION="1.0"
|
|
4
|
+
ARCH="all"
|
|
5
|
+
DEB_DIR="${PACKAGE_NAME}_${VERSION}_${ARCH}"
|
|
6
|
+
|
|
7
|
+
echo "Building .deb package for $PACKAGE_NAME..."
|
|
8
|
+
|
|
9
|
+
# Create directory structure
|
|
10
|
+
mkdir -p "$DEB_DIR/usr/local/bin"
|
|
11
|
+
mkdir -p "$DEB_DIR/DEBIAN"
|
|
12
|
+
|
|
13
|
+
# Determine source file (handle rename)
|
|
14
|
+
if [ -f "imagestat" ]; then
|
|
15
|
+
cp imagestat "$DEB_DIR/usr/local/bin/$PACKAGE_NAME"
|
|
16
|
+
elif [ -f "imgstat.sh" ]; then
|
|
17
|
+
cp imgstat.sh "$DEB_DIR/usr/local/bin/$PACKAGE_NAME"
|
|
18
|
+
else
|
|
19
|
+
echo "Error: Source script not found (looked for 'imagestat' and 'imgstat.sh')."
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
chmod 755 "$DEB_DIR/usr/local/bin/$PACKAGE_NAME"
|
|
24
|
+
|
|
25
|
+
# Create control file
|
|
26
|
+
cat > "$DEB_DIR/DEBIAN/control" <<EOF
|
|
27
|
+
Package: $PACKAGE_NAME
|
|
28
|
+
Version: $VERSION
|
|
29
|
+
Section: utils
|
|
30
|
+
Priority: optional
|
|
31
|
+
Architecture: $ARCH
|
|
32
|
+
Allowed-Architectures: all
|
|
33
|
+
Maintainer: User <user@example.com>
|
|
34
|
+
Depends: imagemagick, wget
|
|
35
|
+
Description: Image dimensions scanner and renamer
|
|
36
|
+
Recursively scans directories for images and renames them to include their
|
|
37
|
+
dimensions (e.g., image-800x600.jpg). Ignores heavy directories like node_modules.
|
|
38
|
+
EOF
|
|
39
|
+
|
|
40
|
+
# Build package
|
|
41
|
+
dpkg-deb --build "$DEB_DIR"
|
|
42
|
+
|
|
43
|
+
# Cleanup
|
|
44
|
+
mv "${DEB_DIR}.deb" "${PACKAGE_NAME}.deb" 2>/dev/null || true
|
|
45
|
+
rm -rf "$DEB_DIR"
|
|
46
|
+
|
|
47
|
+
echo "Build complete: ${PACKAGE_NAME}.deb"
|
|
48
|
+
echo "Install with: sudo apt install ./$PACKAGE_NAME.deb"
|
package/imagestat
CHANGED
|
@@ -26,11 +26,54 @@ if [ ! -d "$TARGET_DIR" ]; then
|
|
|
26
26
|
mkdir -p "$TARGET_DIR"
|
|
27
27
|
fi
|
|
28
28
|
|
|
29
|
+
# Download images if URL is provided
|
|
29
30
|
# Download images if URL is provided
|
|
30
31
|
if [ -n "$URL" ]; then
|
|
31
|
-
echo "Downloading images from $URL to
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
echo "Downloading images from $URL to temp folder..."
|
|
33
|
+
TEMP_DIR=$(mktemp -d)
|
|
34
|
+
|
|
35
|
+
# Download with wget (no extension restriction to catch all images)
|
|
36
|
+
# -nd: no directories (flatten)
|
|
37
|
+
# -r: recursive (useful if URL is a directory, but works for single file too)
|
|
38
|
+
# -l 1: limit recursion depth to 1 just in case (optional, but safer)
|
|
39
|
+
# --no-parent: don't go up
|
|
40
|
+
wget -nd -H -P "$TEMP_DIR" -e robots=off --user-agent="Mozilla/5.0" "$URL" 2>/dev/null
|
|
41
|
+
|
|
42
|
+
echo "Processing downloaded files..."
|
|
43
|
+
shopt -s nullglob
|
|
44
|
+
for f in "$TEMP_DIR"/*; do
|
|
45
|
+
if [ -f "$f" ]; then
|
|
46
|
+
# Identify format and dimensions (take first frame for animated gifs etc)
|
|
47
|
+
# Use -ping to be faster? identification doesn't need full read usually
|
|
48
|
+
ID_OUTPUT=$(identify -format "%m %w %h" "$f[0]" 2>/dev/null)
|
|
49
|
+
|
|
50
|
+
if [ -n "$ID_OUTPUT" ]; then
|
|
51
|
+
read -r TYPE W H <<< "$ID_OUTPUT"
|
|
52
|
+
|
|
53
|
+
# Normalize extension
|
|
54
|
+
EXT=$(echo "$TYPE" | tr '[:upper:]' '[:lower:]')
|
|
55
|
+
case "$EXT" in
|
|
56
|
+
jpeg) EXT="jpg" ;;
|
|
57
|
+
esac
|
|
58
|
+
|
|
59
|
+
# Get clean basename (remove query params)
|
|
60
|
+
BASENAME=$(basename "$f")
|
|
61
|
+
CLEAN_NAME=$(echo "$BASENAME" | sed 's/[?=&].*//')
|
|
62
|
+
# Remove existing extension if it matches common ones
|
|
63
|
+
CLEAN_NAME="${CLEAN_NAME%.*}"
|
|
64
|
+
|
|
65
|
+
# Construct new filename
|
|
66
|
+
NEW_FILENAME="${CLEAN_NAME}-${W}x${H}.${EXT}"
|
|
67
|
+
NEW_PATH="$TARGET_DIR/$NEW_FILENAME"
|
|
68
|
+
|
|
69
|
+
# Move to target
|
|
70
|
+
mv -n "$f" "$NEW_PATH"
|
|
71
|
+
echo "Saved: $NEW_FILENAME ($W x $H)"
|
|
72
|
+
fi
|
|
73
|
+
fi
|
|
74
|
+
done
|
|
75
|
+
rm -rf "$TEMP_DIR"
|
|
76
|
+
echo "Download processing complete."
|
|
34
77
|
fi
|
|
35
78
|
|
|
36
79
|
# Export function so xargs can use it
|
package/install.sh
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
APP_NAME="imagestat"
|
|
4
|
+
INSTALL_DIR="/usr/local/bin"
|
|
5
|
+
TARGET_PATH="$INSTALL_DIR/$APP_NAME"
|
|
6
|
+
SOURCE_FILE=""
|
|
7
|
+
|
|
8
|
+
# Colors
|
|
9
|
+
GREEN='\033[0;32m'
|
|
10
|
+
RED='\033[0;31m'
|
|
11
|
+
YELLOW='\033[1;33m'
|
|
12
|
+
NC='\033[0m' # No Color
|
|
13
|
+
|
|
14
|
+
echo -e "${GREEN}Installing $APP_NAME...${NC}"
|
|
15
|
+
|
|
16
|
+
# Check sudo
|
|
17
|
+
if [ "$EUID" -ne 0 ]; then
|
|
18
|
+
echo -e "${YELLOW}Please run as root (sudo) to install globally.${NC}"
|
|
19
|
+
echo "Try: sudo ./install.sh"
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
# Locate source file
|
|
24
|
+
if [ -f "imagestat" ]; then
|
|
25
|
+
SOURCE_FILE="imagestat"
|
|
26
|
+
elif [ -f "imgstat.sh" ]; then
|
|
27
|
+
SOURCE_FILE="imgstat.sh"
|
|
28
|
+
else
|
|
29
|
+
echo "Local script not found. Downloading from GitHub..."
|
|
30
|
+
REMOTE_URL="https://raw.githubusercontent.com/isaac0yen/imgstat/main/imagestat"
|
|
31
|
+
if command -v curl &> /dev/null; then
|
|
32
|
+
curl -fsSL "$REMOTE_URL" -o imagestat
|
|
33
|
+
elif command -v wget &> /dev/null; then
|
|
34
|
+
wget -q "$REMOTE_URL" -O imagestat
|
|
35
|
+
else
|
|
36
|
+
echo -e "${RED}Error: Neither curl nor wget found. Cannot download script.${NC}"
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if [ ! -f "imagestat" ]; then
|
|
41
|
+
echo -e "${RED}Error: Failed to download script.${NC}"
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
SOURCE_FILE="imagestat"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Check dependencies
|
|
48
|
+
MISSING_DEPS=0
|
|
49
|
+
if ! command -v identify &> /dev/null; then
|
|
50
|
+
echo -e "${RED}Error: 'identify' (ImageMagick) is not installed.${NC}"
|
|
51
|
+
MISSING_DEPS=1
|
|
52
|
+
fi
|
|
53
|
+
if ! command -v wget &> /dev/null; then
|
|
54
|
+
echo -e "${RED}Error: 'wget' is not installed.${NC}"
|
|
55
|
+
MISSING_DEPS=1
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
if [ $MISSING_DEPS -eq 1 ]; then
|
|
59
|
+
echo -e "${YELLOW}Please install missing dependencies first.${NC}"
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
echo "Installing $SOURCE_FILE to $TARGET_PATH..."
|
|
64
|
+
cp "$SOURCE_FILE" "$TARGET_PATH"
|
|
65
|
+
chmod +x "$TARGET_PATH"
|
|
66
|
+
|
|
67
|
+
echo -e "${GREEN}Success! $APP_NAME has been installed.${NC}"
|
|
68
|
+
echo -e "Usage: ${GREEN}$APP_NAME [-u|--url <url>] [directory]${NC}"
|
package/lib/analyze.sh
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utils.sh"
|
|
4
|
+
|
|
5
|
+
cmd_analyze() {
|
|
6
|
+
local dir="$1"
|
|
7
|
+
echo "Analyzing codebase for remote image references in $dir..."
|
|
8
|
+
|
|
9
|
+
# Scan common code files for URLs
|
|
10
|
+
# We extract http/https links specifically in src, href, url() contexts
|
|
11
|
+
local urls=()
|
|
12
|
+
|
|
13
|
+
# Read all matching files, use grep to find URLs, and sort uniquely
|
|
14
|
+
# This uses a basic grep regex to find common image URL patterns.
|
|
15
|
+
mapfile -t urls < <(find "$dir" \
|
|
16
|
+
-type d \( -name "node_modules" -o -name ".git" -o -name "dist" -o -name "build" \) -prune -o \
|
|
17
|
+
-type f \( -name "*.html" -o -name "*.jsx" -o -name "*.tsx" -o -name "*.js" -o -name "*.ts" -o -name "*.vue" -o -name "*.css" -o -name "*.scss" \) \
|
|
18
|
+
-exec grep -oE "https?://[^\"')[:space:]]+" {} + 2>/dev/null | sort -u)
|
|
19
|
+
|
|
20
|
+
if [[ ${#urls[@]} -eq 0 ]]; then
|
|
21
|
+
echo "No remote URLs found in code files."
|
|
22
|
+
return 0
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
echo "Found ${#urls[@]} unique URL(s). Fetching dimensions..."
|
|
26
|
+
|
|
27
|
+
# Prepare secure fetch directory
|
|
28
|
+
local TEMP_DIR
|
|
29
|
+
TEMP_DIR=$(mktemp -d)
|
|
30
|
+
trap 'rm -rf "$TEMP_DIR"' EXIT
|
|
31
|
+
|
|
32
|
+
# Output file setup
|
|
33
|
+
local rules_dir="$dir/.agent/rules"
|
|
34
|
+
local rules_file="$rules_dir/image_dimensions.md"
|
|
35
|
+
|
|
36
|
+
mkdir -p "$rules_dir"
|
|
37
|
+
|
|
38
|
+
# Write header for the agent
|
|
39
|
+
cat << 'EOF' > "$rules_file"
|
|
40
|
+
# Codebase Remote Images
|
|
41
|
+
|
|
42
|
+
> [!NOTE]
|
|
43
|
+
> This file is strictly auto-generated by \`imgstat\`.
|
|
44
|
+
> It maps remote image URLs found in the codebase to their exact physical dimensions.
|
|
45
|
+
> AI assistants should use this dictionary when asked about layout constraints, native sizes, or aspect ratios of referenced images.
|
|
46
|
+
|
|
47
|
+
| Documented URL | Detected Size (W x H) |
|
|
48
|
+
|---|---|
|
|
49
|
+
EOF
|
|
50
|
+
|
|
51
|
+
local processed=0
|
|
52
|
+
|
|
53
|
+
for url in "${urls[@]}"; do
|
|
54
|
+
# Skip obvious non-images if possible, but Cloudinary etc don't have extensions
|
|
55
|
+
# so we attempt a shallow fetch for all to be safe.
|
|
56
|
+
|
|
57
|
+
# 1. Fetch direct URL
|
|
58
|
+
wget -q --content-disposition -P "$TEMP_DIR" "$url" || true
|
|
59
|
+
|
|
60
|
+
local all_files=( "$TEMP_DIR"/* )
|
|
61
|
+
local found_images=()
|
|
62
|
+
|
|
63
|
+
for file in "${all_files[@]}"; do
|
|
64
|
+
if [[ ! -f "$file" ]]; then continue; fi
|
|
65
|
+
local mimetype
|
|
66
|
+
mimetype=$(file -b --mime-type "$file" 2>/dev/null || true)
|
|
67
|
+
if [[ "$mimetype" == image/* ]]; then
|
|
68
|
+
found_images+=("$file")
|
|
69
|
+
fi
|
|
70
|
+
done
|
|
71
|
+
|
|
72
|
+
# Process only the first valid image found for the URL
|
|
73
|
+
if [[ ${#found_images[@]} -gt 0 ]]; then
|
|
74
|
+
local file="${found_images[0]}"
|
|
75
|
+
local dim
|
|
76
|
+
dim=$(get_dimensions "$file" || true)
|
|
77
|
+
|
|
78
|
+
if [[ -n "$dim" ]]; then
|
|
79
|
+
local w="${dim% *}"
|
|
80
|
+
local h="${dim#* }"
|
|
81
|
+
echo "| \`$url\` | **${w}x${h}** |" >> "$rules_file"
|
|
82
|
+
processed=$((processed + 1))
|
|
83
|
+
fi
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
# Clean temp dir contents for the next URL to avoid collision
|
|
87
|
+
rm -rf "${TEMP_DIR:?}"/*
|
|
88
|
+
done
|
|
89
|
+
|
|
90
|
+
echo "Analysis complete!"
|
|
91
|
+
echo "Documented $processed valid image reference(s) into: $rules_file"
|
|
92
|
+
echo "This file can now be read by autonomous coding agents."
|
|
93
|
+
}
|
package/lib/inspect.sh
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utils.sh"
|
|
4
|
+
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/scan.sh"
|
|
5
|
+
|
|
6
|
+
cmd_inspect() {
|
|
7
|
+
local dir="$1"
|
|
8
|
+
echo "Inspecting directory: $dir"
|
|
9
|
+
|
|
10
|
+
local files
|
|
11
|
+
mapfile -t files < <(scan_images "$dir")
|
|
12
|
+
|
|
13
|
+
if [[ ${#files[@]} -eq 0 ]]; then
|
|
14
|
+
echo "No images found in $dir."
|
|
15
|
+
return 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
printf "%-50s | %-15s\n" "Filename" "Dimensions"
|
|
19
|
+
printf "%.s-" {1..70}
|
|
20
|
+
echo
|
|
21
|
+
|
|
22
|
+
for file in "${files[@]}"; do
|
|
23
|
+
local dim
|
|
24
|
+
dim=$(get_dimensions "$file" || true)
|
|
25
|
+
if [[ -n "$dim" ]]; then
|
|
26
|
+
local w="${dim% *}"
|
|
27
|
+
local h="${dim#* }"
|
|
28
|
+
printf "%-50s | %s x %s\n" "$(basename "$file")" "$w" "$h"
|
|
29
|
+
else
|
|
30
|
+
printf "%-50s | %-15s\n" "$(basename "$file")" "Error reading"
|
|
31
|
+
fi
|
|
32
|
+
done
|
|
33
|
+
}
|
package/lib/remote.sh
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utils.sh"
|
|
4
|
+
|
|
5
|
+
cmd_remote() {
|
|
6
|
+
local url="$1"
|
|
7
|
+
local dry_run="$2"
|
|
8
|
+
|
|
9
|
+
if [[ "$dry_run" == "true" ]]; then
|
|
10
|
+
echo "[Dry Run] Would fetch $url to extract dimensions. Nothing will be downloaded."
|
|
11
|
+
return 0
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
echo "Fetching images from $url..."
|
|
15
|
+
|
|
16
|
+
local TEMP_DIR
|
|
17
|
+
TEMP_DIR=$(mktemp -d)
|
|
18
|
+
trap 'rm -rf "$TEMP_DIR"' EXIT
|
|
19
|
+
|
|
20
|
+
# 1. Fetch direct URL (handles direct images without extensions like Cloudinary)
|
|
21
|
+
wget -q --content-disposition -P "$TEMP_DIR" "$url" || true
|
|
22
|
+
|
|
23
|
+
# 2. Shallow scrape for linked images (handles HTML pages)
|
|
24
|
+
wget -q -nd -r -l 1 -A jpeg,jpg,bmp,gif,png,webp,avif -P "$TEMP_DIR" "$url" || true
|
|
25
|
+
|
|
26
|
+
local all_files=( "$TEMP_DIR"/* )
|
|
27
|
+
local found_images=()
|
|
28
|
+
|
|
29
|
+
# Filter only actual images
|
|
30
|
+
for file in "${all_files[@]}"; do
|
|
31
|
+
if [[ ! -f "$file" ]]; then continue; fi
|
|
32
|
+
local mimetype
|
|
33
|
+
mimetype=$(file -b --mime-type "$file" 2>/dev/null || true)
|
|
34
|
+
if [[ "$mimetype" == image/* ]]; then
|
|
35
|
+
found_images+=("$file")
|
|
36
|
+
fi
|
|
37
|
+
done
|
|
38
|
+
|
|
39
|
+
if [[ ${#found_images[@]} -eq 0 ]]; then
|
|
40
|
+
echo "No images found at $url."
|
|
41
|
+
return 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
printf "%-50s | %-15s\n" "Filename" "Dimensions"
|
|
45
|
+
printf "%.s-" {1..70}
|
|
46
|
+
echo
|
|
47
|
+
|
|
48
|
+
for file in "${found_images[@]}"; do
|
|
49
|
+
local dim
|
|
50
|
+
dim=$(get_dimensions "$file" || true)
|
|
51
|
+
if [[ -n "$dim" ]]; then
|
|
52
|
+
local w="${dim% *}"
|
|
53
|
+
local h="${dim#* }"
|
|
54
|
+
printf "%-50s | %s x %s\n" "$(basename "$file")" "$w" "$h"
|
|
55
|
+
else
|
|
56
|
+
printf "%-50s | %-15s\n" "$(basename "$file")" "Error reading"
|
|
57
|
+
fi
|
|
58
|
+
done
|
|
59
|
+
}
|
package/lib/rename.sh
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utils.sh"
|
|
4
|
+
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/scan.sh"
|
|
5
|
+
|
|
6
|
+
cmd_rename() {
|
|
7
|
+
local dir="$1"
|
|
8
|
+
local dry_run="$2" # true or false
|
|
9
|
+
local auto_yes="$3" # true or false
|
|
10
|
+
|
|
11
|
+
local files
|
|
12
|
+
mapfile -t files < <(scan_images "$dir")
|
|
13
|
+
|
|
14
|
+
if [[ ${#files[@]} -eq 0 ]]; then
|
|
15
|
+
echo "No images found in $dir."
|
|
16
|
+
return 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
local to_rename=()
|
|
20
|
+
local new_names=()
|
|
21
|
+
|
|
22
|
+
for file in "${files[@]}"; do
|
|
23
|
+
local dim
|
|
24
|
+
dim=$(get_dimensions "$file" || true)
|
|
25
|
+
if [[ -z "$dim" ]]; then
|
|
26
|
+
continue
|
|
27
|
+
fi
|
|
28
|
+
local w="${dim% *}"
|
|
29
|
+
local h="${dim#* }"
|
|
30
|
+
|
|
31
|
+
local new_filename
|
|
32
|
+
new_filename=$(format_filename "$file" "$w" "$h")
|
|
33
|
+
|
|
34
|
+
if [[ -n "$new_filename" ]]; then
|
|
35
|
+
to_rename+=("$file")
|
|
36
|
+
new_names+=("$new_filename")
|
|
37
|
+
fi
|
|
38
|
+
done
|
|
39
|
+
|
|
40
|
+
if [[ ${#to_rename[@]} -eq 0 ]]; then
|
|
41
|
+
echo "All images already have dimensions in their filenames. Nothing to do."
|
|
42
|
+
return 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
echo "Found ${#to_rename[@]} file(s) to rename:"
|
|
46
|
+
for i in "${!to_rename[@]}"; do
|
|
47
|
+
echo " $(basename "${to_rename[$i]}") -> $(basename "${new_names[$i]}")"
|
|
48
|
+
done
|
|
49
|
+
|
|
50
|
+
if [[ "$dry_run" == "true" ]]; then
|
|
51
|
+
echo
|
|
52
|
+
echo "[Dry Run] No files were changed."
|
|
53
|
+
return 0
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
if [[ "$auto_yes" != "true" ]]; then
|
|
57
|
+
echo
|
|
58
|
+
read -r -p "Proceed with renaming? [y/N] " response
|
|
59
|
+
if [[ ! "$response" =~ ^([yY][eE][sS]|[yY])$ ]]; then
|
|
60
|
+
echo "Aborted."
|
|
61
|
+
return 1
|
|
62
|
+
fi
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
for i in "${!to_rename[@]}"; do
|
|
66
|
+
mv "${to_rename[$i]}" "${new_names[$i]}"
|
|
67
|
+
done
|
|
68
|
+
|
|
69
|
+
echo "Renaming complete."
|
|
70
|
+
}
|
package/lib/scan.sh
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Recursively find images, ignoring node_modules and .git
|
|
4
|
+
# Usage: scan_images "directory"
|
|
5
|
+
scan_images() {
|
|
6
|
+
local dir="$1"
|
|
7
|
+
if [[ ! -d "$dir" ]]; then
|
|
8
|
+
echo "Error: Directory '$dir' does not exist." >&2
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
# GNU find syntax for ignoring dirs and matching extensions case-insensitively
|
|
13
|
+
find "$dir" -type d \( -name "node_modules" -o -name ".git" \) -prune \
|
|
14
|
+
-o -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.avif" \) -print
|
|
15
|
+
}
|
package/lib/ui.sh
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/inspect.sh"
|
|
4
|
+
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/rename.sh"
|
|
5
|
+
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/remote.sh"
|
|
6
|
+
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/analyze.sh"
|
|
7
|
+
|
|
8
|
+
cmd_ui() {
|
|
9
|
+
local options=("Inspect local directory" "Rename local files" "Analyze remote URL" "Analyze codebase (.agent/rules)" "Quit")
|
|
10
|
+
local selected=0
|
|
11
|
+
|
|
12
|
+
# Hide cursor
|
|
13
|
+
tput civis
|
|
14
|
+
|
|
15
|
+
while true; do
|
|
16
|
+
echo -ne "\033[1;36mimgstat\033[0m: The tool for embedding image dimensions in filenames.\n"
|
|
17
|
+
echo "Select an operation mode (Use Up/Down arrows and Enter):"
|
|
18
|
+
|
|
19
|
+
for i in "${!options[@]}"; do
|
|
20
|
+
if [[ $i -eq $selected ]]; then
|
|
21
|
+
echo -e "\033[1;32m> ${options[$i]}\033[0m"
|
|
22
|
+
else
|
|
23
|
+
echo " ${options[$i]}"
|
|
24
|
+
fi
|
|
25
|
+
done
|
|
26
|
+
|
|
27
|
+
read -rsn1 key || true
|
|
28
|
+
if [[ $key == $'\x1b' ]]; then
|
|
29
|
+
read -rsn2 -t 0.1 seq || true
|
|
30
|
+
case "$seq" in
|
|
31
|
+
"[A") # Up arrow
|
|
32
|
+
((selected--)) || true
|
|
33
|
+
if [[ $selected -lt 0 ]]; then selected=$((${#options[@]} - 1)); fi
|
|
34
|
+
;;
|
|
35
|
+
"[B") # Down arrow
|
|
36
|
+
((selected++)) || true
|
|
37
|
+
if [[ $selected -ge ${#options[@]} ]]; then selected=0; fi
|
|
38
|
+
;;
|
|
39
|
+
esac
|
|
40
|
+
elif [[ $key == "" ]]; then # Enter key
|
|
41
|
+
# Clear menu lines before proceeding
|
|
42
|
+
tput cuu $((${#options[@]} + 2))
|
|
43
|
+
tput ed
|
|
44
|
+
break
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Go up and clear to redraw
|
|
48
|
+
tput cuu $((${#options[@]} + 2))
|
|
49
|
+
tput ed
|
|
50
|
+
done
|
|
51
|
+
|
|
52
|
+
# Restore cursor
|
|
53
|
+
tput cnorm
|
|
54
|
+
|
|
55
|
+
case "$selected" in
|
|
56
|
+
0)
|
|
57
|
+
read -e -p "Enter directory to inspect [./]: " dir
|
|
58
|
+
dir="${dir:-./}"
|
|
59
|
+
cmd_inspect "$dir"
|
|
60
|
+
;;
|
|
61
|
+
1)
|
|
62
|
+
read -e -p "Enter directory to rename [./]: " dir
|
|
63
|
+
dir="${dir:-./}"
|
|
64
|
+
cmd_rename "$dir" "false" "false"
|
|
65
|
+
;;
|
|
66
|
+
2)
|
|
67
|
+
read -e -p "Enter URL to analyze: " url
|
|
68
|
+
if [[ -z "$url" ]]; then
|
|
69
|
+
echo "URL cannot be empty."
|
|
70
|
+
else
|
|
71
|
+
cmd_remote "$url" "false"
|
|
72
|
+
fi
|
|
73
|
+
;;
|
|
74
|
+
3)
|
|
75
|
+
read -e -p "Enter codebase directory to analyze [./]: " dir
|
|
76
|
+
dir="${dir:-./}"
|
|
77
|
+
cmd_analyze "$dir"
|
|
78
|
+
;;
|
|
79
|
+
4)
|
|
80
|
+
echo "Exiting."
|
|
81
|
+
;;
|
|
82
|
+
esac
|
|
83
|
+
}
|
package/lib/utils.sh
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Check if required command is available
|
|
4
|
+
require_command() {
|
|
5
|
+
local cmd="$1"
|
|
6
|
+
if ! command -v "$cmd" &> /dev/null; then
|
|
7
|
+
echo "Error: Required command '$cmd' is not installed." >&2
|
|
8
|
+
exit 1
|
|
9
|
+
fi
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
# Get Image Dimensions using ImageMagick 'identify'
|
|
13
|
+
# Usage: get_dimensions "file.jpg"
|
|
14
|
+
# Outputs: width height
|
|
15
|
+
get_dimensions() {
|
|
16
|
+
local file="$1"
|
|
17
|
+
# -ping is faster because it doesn't read the whole image data if possible
|
|
18
|
+
if ! dim=$(identify -ping -format "%w %h" "$file" 2>/dev/null); then
|
|
19
|
+
return 1
|
|
20
|
+
fi
|
|
21
|
+
echo "$dim"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Generate the target filename if it doesn't already have the dimensions
|
|
25
|
+
# Usage: format_filename "file.jpg" "800" "600"
|
|
26
|
+
# Outputs: target_file.jpg (or nothing if it shouldn't be renamed)
|
|
27
|
+
format_filename() {
|
|
28
|
+
local file="$1"
|
|
29
|
+
local w="$2"
|
|
30
|
+
local h="$3"
|
|
31
|
+
|
|
32
|
+
local filename
|
|
33
|
+
filename=$(basename -- "$file")
|
|
34
|
+
local ext="${filename##*.}"
|
|
35
|
+
local name="${filename%.*}"
|
|
36
|
+
|
|
37
|
+
# If file has no extension
|
|
38
|
+
if [[ "$name" == "$filename" ]]; then
|
|
39
|
+
ext=""
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
local dim_suffix="${w}x${h}"
|
|
43
|
+
|
|
44
|
+
# Check if already ends with -WxH
|
|
45
|
+
if [[ "$name" == *-*x* ]]; then
|
|
46
|
+
local current_suffix="${name##*-}"
|
|
47
|
+
if [[ "$current_suffix" == "$dim_suffix" ]]; then
|
|
48
|
+
# Already correct
|
|
49
|
+
return 0
|
|
50
|
+
fi
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
local new_name
|
|
54
|
+
if [[ -n "$ext" ]]; then
|
|
55
|
+
new_name="${name}-${dim_suffix}.${ext}"
|
|
56
|
+
else
|
|
57
|
+
new_name="${name}-${dim_suffix}"
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
local dirname
|
|
61
|
+
dirname=$(dirname -- "$file")
|
|
62
|
+
|
|
63
|
+
echo "$dirname/$new_name"
|
|
64
|
+
}
|
package/package.json
CHANGED
|
@@ -1,30 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imgstat",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"imagestat": "./imagestat"
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Embeds image dimensions directly into filenames for natural AI context.",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
7
|
},
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"url": "git+https://github.com/isaac0yen/imgstat.git"
|
|
8
|
+
"bin": {
|
|
9
|
+
"imgstat": "./bin/imgstat"
|
|
12
10
|
},
|
|
11
|
+
"license": "MIT",
|
|
13
12
|
"keywords": [
|
|
13
|
+
"cli",
|
|
14
14
|
"image",
|
|
15
|
-
"resize",
|
|
16
|
-
"rename",
|
|
17
15
|
"dimensions",
|
|
18
|
-
"
|
|
19
|
-
"bash"
|
|
20
|
-
],
|
|
21
|
-
"author": "Isaac Oyeniyi",
|
|
22
|
-
"license": "MIT",
|
|
23
|
-
"bugs": {
|
|
24
|
-
"url": "https://github.com/isaac0yen/imgstat/issues"
|
|
25
|
-
},
|
|
26
|
-
"homepage": "https://github.com/isaac0yen/imgstat#readme",
|
|
27
|
-
"files": [
|
|
28
|
-
"imagestat"
|
|
16
|
+
"ai"
|
|
29
17
|
]
|
|
30
|
-
}
|
|
18
|
+
}
|