nc1709 1.15.4__py3-none-any.whl
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.
- nc1709/__init__.py +13 -0
- nc1709/agent/__init__.py +36 -0
- nc1709/agent/core.py +505 -0
- nc1709/agent/mcp_bridge.py +245 -0
- nc1709/agent/permissions.py +298 -0
- nc1709/agent/tools/__init__.py +21 -0
- nc1709/agent/tools/base.py +440 -0
- nc1709/agent/tools/bash_tool.py +367 -0
- nc1709/agent/tools/file_tools.py +454 -0
- nc1709/agent/tools/notebook_tools.py +516 -0
- nc1709/agent/tools/search_tools.py +322 -0
- nc1709/agent/tools/task_tool.py +284 -0
- nc1709/agent/tools/web_tools.py +555 -0
- nc1709/agents/__init__.py +17 -0
- nc1709/agents/auto_fix.py +506 -0
- nc1709/agents/test_generator.py +507 -0
- nc1709/checkpoints.py +372 -0
- nc1709/cli.py +3380 -0
- nc1709/cli_ui.py +1080 -0
- nc1709/cognitive/__init__.py +149 -0
- nc1709/cognitive/anticipation.py +594 -0
- nc1709/cognitive/context_engine.py +1046 -0
- nc1709/cognitive/council.py +824 -0
- nc1709/cognitive/learning.py +761 -0
- nc1709/cognitive/router.py +583 -0
- nc1709/cognitive/system.py +519 -0
- nc1709/config.py +155 -0
- nc1709/custom_commands.py +300 -0
- nc1709/executor.py +333 -0
- nc1709/file_controller.py +354 -0
- nc1709/git_integration.py +308 -0
- nc1709/github_integration.py +477 -0
- nc1709/image_input.py +446 -0
- nc1709/linting.py +519 -0
- nc1709/llm_adapter.py +667 -0
- nc1709/logger.py +192 -0
- nc1709/mcp/__init__.py +18 -0
- nc1709/mcp/client.py +370 -0
- nc1709/mcp/manager.py +407 -0
- nc1709/mcp/protocol.py +210 -0
- nc1709/mcp/server.py +473 -0
- nc1709/memory/__init__.py +20 -0
- nc1709/memory/embeddings.py +325 -0
- nc1709/memory/indexer.py +474 -0
- nc1709/memory/sessions.py +432 -0
- nc1709/memory/vector_store.py +451 -0
- nc1709/models/__init__.py +86 -0
- nc1709/models/detector.py +377 -0
- nc1709/models/formats.py +315 -0
- nc1709/models/manager.py +438 -0
- nc1709/models/registry.py +497 -0
- nc1709/performance/__init__.py +343 -0
- nc1709/performance/cache.py +705 -0
- nc1709/performance/pipeline.py +611 -0
- nc1709/performance/tiering.py +543 -0
- nc1709/plan_mode.py +362 -0
- nc1709/plugins/__init__.py +17 -0
- nc1709/plugins/agents/__init__.py +18 -0
- nc1709/plugins/agents/django_agent.py +912 -0
- nc1709/plugins/agents/docker_agent.py +623 -0
- nc1709/plugins/agents/fastapi_agent.py +887 -0
- nc1709/plugins/agents/git_agent.py +731 -0
- nc1709/plugins/agents/nextjs_agent.py +867 -0
- nc1709/plugins/base.py +359 -0
- nc1709/plugins/manager.py +411 -0
- nc1709/plugins/registry.py +337 -0
- nc1709/progress.py +443 -0
- nc1709/prompts/__init__.py +22 -0
- nc1709/prompts/agent_system.py +180 -0
- nc1709/prompts/task_prompts.py +340 -0
- nc1709/prompts/unified_prompt.py +133 -0
- nc1709/reasoning_engine.py +541 -0
- nc1709/remote_client.py +266 -0
- nc1709/shell_completions.py +349 -0
- nc1709/slash_commands.py +649 -0
- nc1709/task_classifier.py +408 -0
- nc1709/version_check.py +177 -0
- nc1709/web/__init__.py +8 -0
- nc1709/web/server.py +950 -0
- nc1709/web/templates/index.html +1127 -0
- nc1709-1.15.4.dist-info/METADATA +858 -0
- nc1709-1.15.4.dist-info/RECORD +86 -0
- nc1709-1.15.4.dist-info/WHEEL +5 -0
- nc1709-1.15.4.dist-info/entry_points.txt +2 -0
- nc1709-1.15.4.dist-info/licenses/LICENSE +9 -0
- nc1709-1.15.4.dist-info/top_level.txt +1 -0
nc1709/image_input.py
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Image Input Support for NC1709
|
|
3
|
+
|
|
4
|
+
Provides multi-modal capabilities for image/screenshot input:
|
|
5
|
+
- Read and encode images for LLM API calls
|
|
6
|
+
- Support for PNG, JPG, GIF, WebP formats
|
|
7
|
+
- Screenshot capture (macOS support)
|
|
8
|
+
- Clipboard image paste support
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import base64
|
|
13
|
+
import mimetypes
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional, List, Dict, Any, Tuple
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
import subprocess
|
|
18
|
+
import tempfile
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Supported image formats
|
|
22
|
+
SUPPORTED_IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'}
|
|
23
|
+
SUPPORTED_MIME_TYPES = {
|
|
24
|
+
'.png': 'image/png',
|
|
25
|
+
'.jpg': 'image/jpeg',
|
|
26
|
+
'.jpeg': 'image/jpeg',
|
|
27
|
+
'.gif': 'image/gif',
|
|
28
|
+
'.webp': 'image/webp',
|
|
29
|
+
'.bmp': 'image/bmp',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class ImageData:
|
|
35
|
+
"""Represents an image for multi-modal input"""
|
|
36
|
+
path: str
|
|
37
|
+
base64_data: str
|
|
38
|
+
mime_type: str
|
|
39
|
+
width: Optional[int] = None
|
|
40
|
+
height: Optional[int] = None
|
|
41
|
+
size_bytes: int = 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_image_file(file_path: str) -> bool:
|
|
45
|
+
"""Check if a file is a supported image format"""
|
|
46
|
+
path = Path(file_path)
|
|
47
|
+
return path.suffix.lower() in SUPPORTED_IMAGE_EXTENSIONS
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_image_mime_type(file_path: str) -> str:
|
|
51
|
+
"""Get MIME type for an image file"""
|
|
52
|
+
path = Path(file_path)
|
|
53
|
+
ext = path.suffix.lower()
|
|
54
|
+
return SUPPORTED_MIME_TYPES.get(ext, 'image/png')
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def encode_image_to_base64(file_path: str) -> Optional[str]:
|
|
58
|
+
"""
|
|
59
|
+
Read an image file and encode it to base64.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
file_path: Path to the image file
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Base64 encoded string, or None if failed
|
|
66
|
+
"""
|
|
67
|
+
path = Path(file_path).expanduser()
|
|
68
|
+
|
|
69
|
+
if not path.exists():
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
if not is_image_file(str(path)):
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
with open(path, 'rb') as f:
|
|
77
|
+
image_data = f.read()
|
|
78
|
+
return base64.b64encode(image_data).decode('utf-8')
|
|
79
|
+
except Exception:
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def load_image(file_path: str) -> Optional[ImageData]:
|
|
84
|
+
"""
|
|
85
|
+
Load an image file and prepare it for multi-modal input.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
file_path: Path to the image file
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
ImageData object, or None if failed
|
|
92
|
+
"""
|
|
93
|
+
path = Path(file_path).expanduser()
|
|
94
|
+
|
|
95
|
+
if not path.exists():
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
if not is_image_file(str(path)):
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
base64_data = encode_image_to_base64(str(path))
|
|
102
|
+
if not base64_data:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
# Get file size
|
|
106
|
+
size_bytes = path.stat().st_size
|
|
107
|
+
|
|
108
|
+
# Try to get image dimensions (optional, requires PIL)
|
|
109
|
+
width, height = None, None
|
|
110
|
+
try:
|
|
111
|
+
from PIL import Image
|
|
112
|
+
with Image.open(path) as img:
|
|
113
|
+
width, height = img.size
|
|
114
|
+
except ImportError:
|
|
115
|
+
pass
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
return ImageData(
|
|
120
|
+
path=str(path.absolute()),
|
|
121
|
+
base64_data=base64_data,
|
|
122
|
+
mime_type=get_image_mime_type(str(path)),
|
|
123
|
+
width=width,
|
|
124
|
+
height=height,
|
|
125
|
+
size_bytes=size_bytes
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def capture_screenshot(output_path: Optional[str] = None) -> Optional[str]:
|
|
130
|
+
"""
|
|
131
|
+
Capture a screenshot (macOS only for now).
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
output_path: Optional path to save screenshot (uses temp file if not provided)
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Path to the saved screenshot, or None if failed
|
|
138
|
+
"""
|
|
139
|
+
import platform
|
|
140
|
+
|
|
141
|
+
if platform.system() != 'Darwin':
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
if output_path is None:
|
|
145
|
+
# Create a temporary file
|
|
146
|
+
fd, output_path = tempfile.mkstemp(suffix='.png', prefix='nc1709_screenshot_')
|
|
147
|
+
os.close(fd)
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
# Use macOS screencapture command
|
|
151
|
+
# -i for interactive selection, -x for no sound
|
|
152
|
+
result = subprocess.run(
|
|
153
|
+
['screencapture', '-i', '-x', output_path],
|
|
154
|
+
capture_output=True,
|
|
155
|
+
timeout=60
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if result.returncode == 0 and Path(output_path).exists():
|
|
159
|
+
return output_path
|
|
160
|
+
else:
|
|
161
|
+
# User cancelled or error
|
|
162
|
+
if Path(output_path).exists():
|
|
163
|
+
os.remove(output_path)
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
except subprocess.TimeoutExpired:
|
|
167
|
+
if Path(output_path).exists():
|
|
168
|
+
os.remove(output_path)
|
|
169
|
+
return None
|
|
170
|
+
except Exception:
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def get_clipboard_image() -> Optional[str]:
|
|
175
|
+
"""
|
|
176
|
+
Get image from clipboard (macOS only for now).
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Path to temporary file containing the image, or None if no image in clipboard
|
|
180
|
+
"""
|
|
181
|
+
import platform
|
|
182
|
+
|
|
183
|
+
if platform.system() != 'Darwin':
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
# Create temp file for clipboard image
|
|
188
|
+
fd, temp_path = tempfile.mkstemp(suffix='.png', prefix='nc1709_clipboard_')
|
|
189
|
+
os.close(fd)
|
|
190
|
+
|
|
191
|
+
# Use pngpaste or osascript to get clipboard image
|
|
192
|
+
# First try pngpaste if available
|
|
193
|
+
result = subprocess.run(
|
|
194
|
+
['which', 'pngpaste'],
|
|
195
|
+
capture_output=True
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if result.returncode == 0:
|
|
199
|
+
# pngpaste is available
|
|
200
|
+
result = subprocess.run(
|
|
201
|
+
['pngpaste', temp_path],
|
|
202
|
+
capture_output=True,
|
|
203
|
+
timeout=10
|
|
204
|
+
)
|
|
205
|
+
if result.returncode == 0 and Path(temp_path).exists():
|
|
206
|
+
return temp_path
|
|
207
|
+
else:
|
|
208
|
+
# Fallback to osascript
|
|
209
|
+
script = '''
|
|
210
|
+
tell application "System Events"
|
|
211
|
+
set clipboardData to the clipboard as «class PNGf»
|
|
212
|
+
end tell
|
|
213
|
+
'''
|
|
214
|
+
# This is more complex, skip for now
|
|
215
|
+
pass
|
|
216
|
+
|
|
217
|
+
# Cleanup if failed
|
|
218
|
+
if Path(temp_path).exists():
|
|
219
|
+
os.remove(temp_path)
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
except Exception:
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def format_image_for_api(image: ImageData, api_type: str = "anthropic") -> Dict[str, Any]:
|
|
227
|
+
"""
|
|
228
|
+
Format image data for API request.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
image: ImageData object
|
|
232
|
+
api_type: API type ("anthropic", "openai")
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Dict formatted for the API
|
|
236
|
+
"""
|
|
237
|
+
if api_type == "anthropic":
|
|
238
|
+
return {
|
|
239
|
+
"type": "image",
|
|
240
|
+
"source": {
|
|
241
|
+
"type": "base64",
|
|
242
|
+
"media_type": image.mime_type,
|
|
243
|
+
"data": image.base64_data
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
elif api_type == "openai":
|
|
247
|
+
return {
|
|
248
|
+
"type": "image_url",
|
|
249
|
+
"image_url": {
|
|
250
|
+
"url": f"data:{image.mime_type};base64,{image.base64_data}"
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else:
|
|
254
|
+
raise ValueError(f"Unknown API type: {api_type}")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def extract_image_references(text: str) -> List[str]:
|
|
258
|
+
"""
|
|
259
|
+
Extract image file references from text.
|
|
260
|
+
|
|
261
|
+
Supports formats like:
|
|
262
|
+
- @image:path/to/image.png
|
|
263
|
+
- [image: path/to/image.jpg]
|
|
264
|
+
- {{image: /absolute/path.png}}
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
text: Input text
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
List of image paths found
|
|
271
|
+
"""
|
|
272
|
+
import re
|
|
273
|
+
|
|
274
|
+
patterns = [
|
|
275
|
+
r'@image:([^\s]+)',
|
|
276
|
+
r'\[image:\s*([^\]]+)\]',
|
|
277
|
+
r'\{\{image:\s*([^}]+)\}\}',
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
image_paths = []
|
|
281
|
+
for pattern in patterns:
|
|
282
|
+
matches = re.findall(pattern, text, re.IGNORECASE)
|
|
283
|
+
image_paths.extend([m.strip() for m in matches])
|
|
284
|
+
|
|
285
|
+
return image_paths
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def process_prompt_with_images(
|
|
289
|
+
prompt: str,
|
|
290
|
+
additional_images: Optional[List[str]] = None
|
|
291
|
+
) -> Tuple[str, List[ImageData]]:
|
|
292
|
+
"""
|
|
293
|
+
Process a prompt and extract/load any referenced images.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
prompt: User prompt text
|
|
297
|
+
additional_images: Optional list of additional image paths
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Tuple of (cleaned prompt, list of ImageData)
|
|
301
|
+
"""
|
|
302
|
+
import re
|
|
303
|
+
|
|
304
|
+
images = []
|
|
305
|
+
cleaned_prompt = prompt
|
|
306
|
+
|
|
307
|
+
# Extract image references from prompt
|
|
308
|
+
image_paths = extract_image_references(prompt)
|
|
309
|
+
|
|
310
|
+
# Add additional images
|
|
311
|
+
if additional_images:
|
|
312
|
+
image_paths.extend(additional_images)
|
|
313
|
+
|
|
314
|
+
# Load each image
|
|
315
|
+
for path in image_paths:
|
|
316
|
+
image = load_image(path)
|
|
317
|
+
if image:
|
|
318
|
+
images.append(image)
|
|
319
|
+
else:
|
|
320
|
+
# Keep the reference but note it failed
|
|
321
|
+
print(f"Warning: Could not load image: {path}")
|
|
322
|
+
|
|
323
|
+
# Clean the prompt by removing image references
|
|
324
|
+
patterns = [
|
|
325
|
+
r'@image:[^\s]+',
|
|
326
|
+
r'\[image:\s*[^\]]+\]',
|
|
327
|
+
r'\{\{image:\s*[^}]+\}\}',
|
|
328
|
+
]
|
|
329
|
+
for pattern in patterns:
|
|
330
|
+
cleaned_prompt = re.sub(pattern, '', cleaned_prompt, flags=re.IGNORECASE)
|
|
331
|
+
|
|
332
|
+
# Clean up extra whitespace
|
|
333
|
+
cleaned_prompt = ' '.join(cleaned_prompt.split())
|
|
334
|
+
|
|
335
|
+
return cleaned_prompt, images
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def get_image_info(file_path: str) -> Optional[Dict[str, Any]]:
|
|
339
|
+
"""
|
|
340
|
+
Get information about an image file.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
file_path: Path to the image
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Dict with image info, or None if failed
|
|
347
|
+
"""
|
|
348
|
+
path = Path(file_path).expanduser()
|
|
349
|
+
|
|
350
|
+
if not path.exists() or not is_image_file(str(path)):
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
info = {
|
|
354
|
+
"path": str(path.absolute()),
|
|
355
|
+
"name": path.name,
|
|
356
|
+
"format": path.suffix.lower()[1:],
|
|
357
|
+
"size_bytes": path.stat().st_size,
|
|
358
|
+
"size_human": format_file_size(path.stat().st_size),
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
# Try to get dimensions
|
|
362
|
+
try:
|
|
363
|
+
from PIL import Image
|
|
364
|
+
with Image.open(path) as img:
|
|
365
|
+
info["width"] = img.width
|
|
366
|
+
info["height"] = img.height
|
|
367
|
+
info["mode"] = img.mode
|
|
368
|
+
except ImportError:
|
|
369
|
+
pass
|
|
370
|
+
except Exception:
|
|
371
|
+
pass
|
|
372
|
+
|
|
373
|
+
return info
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def format_file_size(size_bytes: int) -> str:
|
|
377
|
+
"""Format file size in human-readable format"""
|
|
378
|
+
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
379
|
+
if size_bytes < 1024:
|
|
380
|
+
return f"{size_bytes:.1f} {unit}"
|
|
381
|
+
size_bytes /= 1024
|
|
382
|
+
return f"{size_bytes:.1f} TB"
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class ImageInputHandler:
|
|
386
|
+
"""
|
|
387
|
+
Handler for image input in the CLI.
|
|
388
|
+
|
|
389
|
+
Provides methods for:
|
|
390
|
+
- Loading images from paths
|
|
391
|
+
- Capturing screenshots
|
|
392
|
+
- Managing image context for prompts
|
|
393
|
+
"""
|
|
394
|
+
|
|
395
|
+
def __init__(self):
|
|
396
|
+
self._pending_images: List[ImageData] = []
|
|
397
|
+
|
|
398
|
+
def add_image(self, path: str) -> bool:
|
|
399
|
+
"""Add an image to the pending list"""
|
|
400
|
+
image = load_image(path)
|
|
401
|
+
if image:
|
|
402
|
+
self._pending_images.append(image)
|
|
403
|
+
return True
|
|
404
|
+
return False
|
|
405
|
+
|
|
406
|
+
def add_screenshot(self) -> bool:
|
|
407
|
+
"""Capture and add a screenshot"""
|
|
408
|
+
path = capture_screenshot()
|
|
409
|
+
if path:
|
|
410
|
+
return self.add_image(path)
|
|
411
|
+
return False
|
|
412
|
+
|
|
413
|
+
def add_clipboard(self) -> bool:
|
|
414
|
+
"""Add image from clipboard"""
|
|
415
|
+
path = get_clipboard_image()
|
|
416
|
+
if path:
|
|
417
|
+
return self.add_image(path)
|
|
418
|
+
return False
|
|
419
|
+
|
|
420
|
+
def get_pending_images(self) -> List[ImageData]:
|
|
421
|
+
"""Get all pending images"""
|
|
422
|
+
return self._pending_images.copy()
|
|
423
|
+
|
|
424
|
+
def clear_pending(self) -> None:
|
|
425
|
+
"""Clear pending images"""
|
|
426
|
+
self._pending_images = []
|
|
427
|
+
|
|
428
|
+
def has_pending_images(self) -> bool:
|
|
429
|
+
"""Check if there are pending images"""
|
|
430
|
+
return len(self._pending_images) > 0
|
|
431
|
+
|
|
432
|
+
def format_for_api(self, api_type: str = "anthropic") -> List[Dict[str, Any]]:
|
|
433
|
+
"""Format pending images for API request"""
|
|
434
|
+
return [format_image_for_api(img, api_type) for img in self._pending_images]
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
# Global image handler
|
|
438
|
+
_image_handler: Optional[ImageInputHandler] = None
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def get_image_handler() -> ImageInputHandler:
|
|
442
|
+
"""Get or create the global image handler"""
|
|
443
|
+
global _image_handler
|
|
444
|
+
if _image_handler is None:
|
|
445
|
+
_image_handler = ImageInputHandler()
|
|
446
|
+
return _image_handler
|