@agent-webui/ai-desk-harness-gimp 1.0.29-beta1

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.
Files changed (48) hide show
  1. package/README.md +4 -0
  2. package/manifest.json +22 -0
  3. package/package.json +11 -0
  4. package/python/agent-harness/GIMP.md +301 -0
  5. package/python/agent-harness/build/lib/cli_anything/gimp/__init__.py +1 -0
  6. package/python/agent-harness/build/lib/cli_anything/gimp/__main__.py +3 -0
  7. package/python/agent-harness/build/lib/cli_anything/gimp/core/__init__.py +1 -0
  8. package/python/agent-harness/build/lib/cli_anything/gimp/core/canvas.py +193 -0
  9. package/python/agent-harness/build/lib/cli_anything/gimp/core/export.py +479 -0
  10. package/python/agent-harness/build/lib/cli_anything/gimp/core/filters.py +382 -0
  11. package/python/agent-harness/build/lib/cli_anything/gimp/core/layers.py +249 -0
  12. package/python/agent-harness/build/lib/cli_anything/gimp/core/media.py +174 -0
  13. package/python/agent-harness/build/lib/cli_anything/gimp/core/project.py +131 -0
  14. package/python/agent-harness/build/lib/cli_anything/gimp/core/session.py +130 -0
  15. package/python/agent-harness/build/lib/cli_anything/gimp/gimp_cli.py +788 -0
  16. package/python/agent-harness/build/lib/cli_anything/gimp/tests/__init__.py +1 -0
  17. package/python/agent-harness/build/lib/cli_anything/gimp/tests/test_core.py +478 -0
  18. package/python/agent-harness/build/lib/cli_anything/gimp/tests/test_full_e2e.py +578 -0
  19. package/python/agent-harness/build/lib/cli_anything/gimp/utils/__init__.py +1 -0
  20. package/python/agent-harness/build/lib/cli_anything/gimp/utils/gimp_backend.py +208 -0
  21. package/python/agent-harness/build/lib/cli_anything/gimp/utils/repl_skin.py +498 -0
  22. package/python/agent-harness/cli_anything/gimp/README.md +202 -0
  23. package/python/agent-harness/cli_anything/gimp/__init__.py +1 -0
  24. package/python/agent-harness/cli_anything/gimp/__main__.py +3 -0
  25. package/python/agent-harness/cli_anything/gimp/core/__init__.py +1 -0
  26. package/python/agent-harness/cli_anything/gimp/core/canvas.py +193 -0
  27. package/python/agent-harness/cli_anything/gimp/core/export.py +479 -0
  28. package/python/agent-harness/cli_anything/gimp/core/filters.py +382 -0
  29. package/python/agent-harness/cli_anything/gimp/core/layers.py +249 -0
  30. package/python/agent-harness/cli_anything/gimp/core/media.py +174 -0
  31. package/python/agent-harness/cli_anything/gimp/core/project.py +131 -0
  32. package/python/agent-harness/cli_anything/gimp/core/session.py +130 -0
  33. package/python/agent-harness/cli_anything/gimp/gimp_cli.py +788 -0
  34. package/python/agent-harness/cli_anything/gimp/tests/TEST.md +137 -0
  35. package/python/agent-harness/cli_anything/gimp/tests/__init__.py +1 -0
  36. package/python/agent-harness/cli_anything/gimp/tests/test_core.py +478 -0
  37. package/python/agent-harness/cli_anything/gimp/tests/test_full_e2e.py +578 -0
  38. package/python/agent-harness/cli_anything/gimp/utils/__init__.py +1 -0
  39. package/python/agent-harness/cli_anything/gimp/utils/gimp_backend.py +208 -0
  40. package/python/agent-harness/cli_anything/gimp/utils/repl_skin.py +498 -0
  41. package/python/agent-harness/cli_anything_gimp.egg-info/PKG-INFO +236 -0
  42. package/python/agent-harness/cli_anything_gimp.egg-info/SOURCES.txt +25 -0
  43. package/python/agent-harness/cli_anything_gimp.egg-info/dependency_links.txt +1 -0
  44. package/python/agent-harness/cli_anything_gimp.egg-info/entry_points.txt +2 -0
  45. package/python/agent-harness/cli_anything_gimp.egg-info/not-zip-safe +1 -0
  46. package/python/agent-harness/cli_anything_gimp.egg-info/requires.txt +7 -0
  47. package/python/agent-harness/cli_anything_gimp.egg-info/top_level.txt +1 -0
  48. package/python/agent-harness/setup.py +54 -0
@@ -0,0 +1,174 @@
1
+ """GIMP CLI - Media file analysis module."""
2
+
3
+ import os
4
+ import json
5
+ import subprocess
6
+ from typing import Dict, Any, Optional
7
+
8
+
9
+ def probe_image(path: str) -> Dict[str, Any]:
10
+ """Analyze an image file and return metadata."""
11
+ if not os.path.exists(path):
12
+ raise FileNotFoundError(f"Image file not found: {path}")
13
+
14
+ from PIL import Image
15
+
16
+ info = {
17
+ "path": os.path.abspath(path),
18
+ "filename": os.path.basename(path),
19
+ "file_size": os.path.getsize(path),
20
+ "file_size_human": _human_size(os.path.getsize(path)),
21
+ }
22
+
23
+ try:
24
+ with Image.open(path) as img:
25
+ info["width"] = img.width
26
+ info["height"] = img.height
27
+ info["mode"] = img.mode
28
+ info["format"] = img.format
29
+ info["format_description"] = img.format_description if hasattr(img, 'format_description') else img.format
30
+ info["megapixels"] = f"{img.width * img.height / 1_000_000:.2f}"
31
+
32
+ # DPI info
33
+ dpi = img.info.get("dpi")
34
+ if dpi:
35
+ info["dpi"] = {"x": round(dpi[0]), "y": round(dpi[1])}
36
+
37
+ # Animation info (GIF, APNG)
38
+ info["is_animated"] = getattr(img, "is_animated", False)
39
+ if info["is_animated"]:
40
+ info["n_frames"] = getattr(img, "n_frames", 1)
41
+
42
+ # Color palette
43
+ if img.mode == "P":
44
+ palette = img.getpalette()
45
+ info["palette_colors"] = len(palette) // 3 if palette else 0
46
+
47
+ # EXIF data (basic)
48
+ exif = img.getexif()
49
+ if exif:
50
+ exif_data = {}
51
+ tag_names = {
52
+ 271: "Make", 272: "Model", 274: "Orientation",
53
+ 305: "Software", 306: "DateTime",
54
+ 36867: "DateTimeOriginal", 37378: "ApertureValue",
55
+ 33434: "ExposureTime", 34855: "ISOSpeedRatings",
56
+ }
57
+ for tag_id, name in tag_names.items():
58
+ if tag_id in exif:
59
+ exif_data[name] = str(exif[tag_id])
60
+ if exif_data:
61
+ info["exif"] = exif_data
62
+
63
+ # Image bands/channels
64
+ info["channels"] = len(img.getbands())
65
+ info["bands"] = list(img.getbands())
66
+
67
+ # Bits per pixel estimation
68
+ bits_per_channel = {"1": 1, "L": 8, "P": 8, "RGB": 8, "RGBA": 8,
69
+ "CMYK": 8, "I": 32, "F": 32, "LA": 8}
70
+ bpc = bits_per_channel.get(img.mode, 8)
71
+ info["bits_per_pixel"] = bpc * info["channels"]
72
+
73
+ except Exception as e:
74
+ info["error"] = str(e)
75
+
76
+ return info
77
+
78
+
79
+ def list_media_in_project(project: Dict[str, Any]) -> list:
80
+ """List all media files referenced in the project."""
81
+ media = []
82
+ for i, layer in enumerate(project.get("layers", [])):
83
+ source = layer.get("source")
84
+ if source:
85
+ exists = os.path.exists(source)
86
+ media.append({
87
+ "layer_index": i,
88
+ "layer_name": layer.get("name", f"Layer {i}"),
89
+ "source": source,
90
+ "exists": exists,
91
+ })
92
+ return media
93
+
94
+
95
+ def check_media(project: Dict[str, Any]) -> Dict[str, Any]:
96
+ """Check that all referenced media files exist."""
97
+ media = list_media_in_project(project)
98
+ missing = [m for m in media if not m["exists"]]
99
+ return {
100
+ "total": len(media),
101
+ "found": len(media) - len(missing),
102
+ "missing": len(missing),
103
+ "missing_files": [m["source"] for m in missing],
104
+ "status": "ok" if not missing else "missing_files",
105
+ }
106
+
107
+
108
+ def get_image_histogram(path: str) -> Dict[str, Any]:
109
+ """Get histogram data for an image."""
110
+ if not os.path.exists(path):
111
+ raise FileNotFoundError(f"Image file not found: {path}")
112
+
113
+ from PIL import Image
114
+
115
+ with Image.open(path) as img:
116
+ if img.mode not in ("RGB", "RGBA", "L"):
117
+ img = img.convert("RGB")
118
+
119
+ hist = img.histogram()
120
+
121
+ if img.mode in ("RGB", "RGBA"):
122
+ r_hist = hist[0:256]
123
+ g_hist = hist[256:512]
124
+ b_hist = hist[512:768]
125
+ return {
126
+ "mode": img.mode,
127
+ "channels": {
128
+ "red": {"min": _first_nonzero(r_hist), "max": _last_nonzero(r_hist),
129
+ "mean": _hist_mean(r_hist)},
130
+ "green": {"min": _first_nonzero(g_hist), "max": _last_nonzero(g_hist),
131
+ "mean": _hist_mean(g_hist)},
132
+ "blue": {"min": _first_nonzero(b_hist), "max": _last_nonzero(b_hist),
133
+ "mean": _hist_mean(b_hist)},
134
+ },
135
+ }
136
+ else:
137
+ return {
138
+ "mode": img.mode,
139
+ "channels": {
140
+ "luminance": {"min": _first_nonzero(hist), "max": _last_nonzero(hist),
141
+ "mean": _hist_mean(hist)},
142
+ },
143
+ }
144
+
145
+
146
+ def _human_size(nbytes: int) -> str:
147
+ """Convert byte count to human-readable string."""
148
+ for unit in ("B", "KB", "MB", "GB"):
149
+ if nbytes < 1024:
150
+ return f"{nbytes:.1f} {unit}"
151
+ nbytes /= 1024
152
+ return f"{nbytes:.1f} TB"
153
+
154
+
155
+ def _first_nonzero(hist: list) -> int:
156
+ for i, v in enumerate(hist):
157
+ if v > 0:
158
+ return i
159
+ return 0
160
+
161
+
162
+ def _last_nonzero(hist: list) -> int:
163
+ for i in range(len(hist) - 1, -1, -1):
164
+ if hist[i] > 0:
165
+ return i
166
+ return 0
167
+
168
+
169
+ def _hist_mean(hist: list) -> float:
170
+ total = sum(hist)
171
+ if total == 0:
172
+ return 0.0
173
+ weighted = sum(i * v for i, v in enumerate(hist))
174
+ return round(weighted / total, 1)
@@ -0,0 +1,131 @@
1
+ """GIMP CLI - Core project management module."""
2
+
3
+ import json
4
+ import os
5
+ import copy
6
+ from datetime import datetime
7
+ from typing import Optional, Dict, Any, List
8
+
9
+
10
+ # Default canvas profiles
11
+ PROFILES = {
12
+ "hd1080p": {"width": 1920, "height": 1080, "dpi": 72},
13
+ "hd720p": {"width": 1280, "height": 720, "dpi": 72},
14
+ "4k": {"width": 3840, "height": 2160, "dpi": 72},
15
+ "square1080": {"width": 1080, "height": 1080, "dpi": 72},
16
+ "a4_300dpi": {"width": 2480, "height": 3508, "dpi": 300},
17
+ "a4_150dpi": {"width": 1240, "height": 1754, "dpi": 150},
18
+ "letter_300dpi": {"width": 2550, "height": 3300, "dpi": 300},
19
+ "web_banner": {"width": 1200, "height": 628, "dpi": 72},
20
+ "instagram_post": {"width": 1080, "height": 1080, "dpi": 72},
21
+ "instagram_story": {"width": 1080, "height": 1920, "dpi": 72},
22
+ "twitter_header": {"width": 1500, "height": 500, "dpi": 72},
23
+ "youtube_thumb": {"width": 1280, "height": 720, "dpi": 72},
24
+ "icon_256": {"width": 256, "height": 256, "dpi": 72},
25
+ "icon_512": {"width": 512, "height": 512, "dpi": 72},
26
+ }
27
+
28
+ PROJECT_VERSION = "1.0"
29
+
30
+
31
+ def create_project(
32
+ width: int = 1920,
33
+ height: int = 1080,
34
+ color_mode: str = "RGB",
35
+ background: str = "#ffffff",
36
+ dpi: int = 72,
37
+ name: str = "untitled",
38
+ profile: Optional[str] = None,
39
+ ) -> Dict[str, Any]:
40
+ """Create a new GIMP CLI project."""
41
+ if profile and profile in PROFILES:
42
+ p = PROFILES[profile]
43
+ width = p["width"]
44
+ height = p["height"]
45
+ dpi = p["dpi"]
46
+
47
+ if color_mode not in ("RGB", "RGBA", "L", "LA"):
48
+ raise ValueError(f"Invalid color mode: {color_mode}. Use RGB, RGBA, L, or LA.")
49
+ if width < 1 or height < 1:
50
+ raise ValueError(f"Canvas dimensions must be positive: {width}x{height}")
51
+ if dpi < 1:
52
+ raise ValueError(f"DPI must be positive: {dpi}")
53
+
54
+ project = {
55
+ "version": PROJECT_VERSION,
56
+ "name": name,
57
+ "canvas": {
58
+ "width": width,
59
+ "height": height,
60
+ "color_mode": color_mode,
61
+ "background": background,
62
+ "dpi": dpi,
63
+ },
64
+ "layers": [],
65
+ "selection": None,
66
+ "guides": [],
67
+ "metadata": {
68
+ "created": datetime.now().isoformat(),
69
+ "modified": datetime.now().isoformat(),
70
+ "software": "gimp-cli 1.0",
71
+ },
72
+ }
73
+ return project
74
+
75
+
76
+ def open_project(path: str) -> Dict[str, Any]:
77
+ """Open a .gimp-cli.json project file."""
78
+ if not os.path.exists(path):
79
+ raise FileNotFoundError(f"Project file not found: {path}")
80
+ with open(path, "r") as f:
81
+ project = json.load(f)
82
+ if "version" not in project or "canvas" not in project:
83
+ raise ValueError(f"Invalid project file: {path}")
84
+ return project
85
+
86
+
87
+ def save_project(project: Dict[str, Any], path: str) -> str:
88
+ """Save project to a .gimp-cli.json file."""
89
+ project["metadata"]["modified"] = datetime.now().isoformat()
90
+ with open(path, "w") as f:
91
+ json.dump(project, f, indent=2, default=str)
92
+ return path
93
+
94
+
95
+ def get_project_info(project: Dict[str, Any]) -> Dict[str, Any]:
96
+ """Get summary information about the project."""
97
+ canvas = project["canvas"]
98
+ layers = project.get("layers", [])
99
+ return {
100
+ "name": project.get("name", "untitled"),
101
+ "version": project.get("version", "unknown"),
102
+ "canvas": {
103
+ "width": canvas["width"],
104
+ "height": canvas["height"],
105
+ "color_mode": canvas.get("color_mode", "RGB"),
106
+ "background": canvas.get("background", "#ffffff"),
107
+ "dpi": canvas.get("dpi", 72),
108
+ },
109
+ "layer_count": len(layers),
110
+ "layers": [
111
+ {
112
+ "id": l.get("id", i),
113
+ "name": l.get("name", f"Layer {i}"),
114
+ "type": l.get("type", "image"),
115
+ "visible": l.get("visible", True),
116
+ "opacity": l.get("opacity", 1.0),
117
+ "blend_mode": l.get("blend_mode", "normal"),
118
+ "filter_count": len(l.get("filters", [])),
119
+ }
120
+ for i, l in enumerate(layers)
121
+ ],
122
+ "metadata": project.get("metadata", {}),
123
+ }
124
+
125
+
126
+ def list_profiles() -> List[Dict[str, Any]]:
127
+ """List all available canvas profiles."""
128
+ result = []
129
+ for name, p in PROFILES.items():
130
+ result.append({"name": name, "width": p["width"], "height": p["height"], "dpi": p["dpi"]})
131
+ return result
@@ -0,0 +1,130 @@
1
+ """GIMP CLI - Session management with undo/redo."""
2
+
3
+ import json
4
+ import os
5
+ import copy
6
+ from typing import Dict, Any, Optional, List
7
+ from datetime import datetime
8
+
9
+
10
+ class Session:
11
+ """Manages project state with undo/redo history."""
12
+
13
+ MAX_UNDO = 50
14
+
15
+ def __init__(self):
16
+ self.project: Optional[Dict[str, Any]] = None
17
+ self.project_path: Optional[str] = None
18
+ self._undo_stack: List[Dict[str, Any]] = []
19
+ self._redo_stack: List[Dict[str, Any]] = []
20
+ self._modified: bool = False
21
+
22
+ def has_project(self) -> bool:
23
+ return self.project is not None
24
+
25
+ def get_project(self) -> Dict[str, Any]:
26
+ if self.project is None:
27
+ raise RuntimeError("No project loaded. Use 'project new' or 'project open' first.")
28
+ return self.project
29
+
30
+ def set_project(self, project: Dict[str, Any], path: Optional[str] = None) -> None:
31
+ self.project = project
32
+ self.project_path = path
33
+ self._undo_stack.clear()
34
+ self._redo_stack.clear()
35
+ self._modified = False
36
+
37
+ def snapshot(self, description: str = "") -> None:
38
+ """Save current state to undo stack before a mutation."""
39
+ if self.project is None:
40
+ return
41
+ state = {
42
+ "project": copy.deepcopy(self.project),
43
+ "description": description,
44
+ "timestamp": datetime.now().isoformat(),
45
+ }
46
+ self._undo_stack.append(state)
47
+ if len(self._undo_stack) > self.MAX_UNDO:
48
+ self._undo_stack.pop(0)
49
+ self._redo_stack.clear()
50
+ self._modified = True
51
+
52
+ def undo(self) -> Optional[str]:
53
+ """Undo the last operation. Returns description of undone action."""
54
+ if not self._undo_stack:
55
+ raise RuntimeError("Nothing to undo.")
56
+ if self.project is None:
57
+ raise RuntimeError("No project loaded.")
58
+
59
+ # Save current state to redo stack
60
+ self._redo_stack.append({
61
+ "project": copy.deepcopy(self.project),
62
+ "description": "redo point",
63
+ "timestamp": datetime.now().isoformat(),
64
+ })
65
+
66
+ # Restore previous state
67
+ state = self._undo_stack.pop()
68
+ self.project = state["project"]
69
+ self._modified = True
70
+ return state.get("description", "")
71
+
72
+ def redo(self) -> Optional[str]:
73
+ """Redo the last undone operation."""
74
+ if not self._redo_stack:
75
+ raise RuntimeError("Nothing to redo.")
76
+ if self.project is None:
77
+ raise RuntimeError("No project loaded.")
78
+
79
+ # Save current state to undo stack
80
+ self._undo_stack.append({
81
+ "project": copy.deepcopy(self.project),
82
+ "description": "undo point",
83
+ "timestamp": datetime.now().isoformat(),
84
+ })
85
+
86
+ # Restore redo state
87
+ state = self._redo_stack.pop()
88
+ self.project = state["project"]
89
+ self._modified = True
90
+ return state.get("description", "")
91
+
92
+ def status(self) -> Dict[str, Any]:
93
+ """Get session status."""
94
+ return {
95
+ "has_project": self.project is not None,
96
+ "project_path": self.project_path,
97
+ "modified": self._modified,
98
+ "undo_count": len(self._undo_stack),
99
+ "redo_count": len(self._redo_stack),
100
+ "project_name": self.project.get("name", "untitled") if self.project else None,
101
+ }
102
+
103
+ def save_session(self, path: Optional[str] = None) -> str:
104
+ """Save the session state (project + undo history) to disk."""
105
+ if self.project is None:
106
+ raise RuntimeError("No project to save.")
107
+
108
+ save_path = path or self.project_path
109
+ if not save_path:
110
+ raise ValueError("No save path specified.")
111
+
112
+ # Save project
113
+ self.project["metadata"]["modified"] = datetime.now().isoformat()
114
+ with open(save_path, "w") as f:
115
+ json.dump(self.project, f, indent=2, default=str)
116
+
117
+ self.project_path = save_path
118
+ self._modified = False
119
+ return save_path
120
+
121
+ def list_history(self) -> List[Dict[str, str]]:
122
+ """List undo history."""
123
+ result = []
124
+ for i, state in enumerate(reversed(self._undo_stack)):
125
+ result.append({
126
+ "index": i,
127
+ "description": state.get("description", ""),
128
+ "timestamp": state.get("timestamp", ""),
129
+ })
130
+ return result