adding copy image from canvas; adding paste image to canvas
This commit is contained in:
@@ -12,7 +12,7 @@ File menu
|
|||||||
- [x] Quit
|
- [x] Quit
|
||||||
|
|
||||||
Clipboard menu
|
Clipboard menu
|
||||||
- [ ] Copy
|
- [x] Copy
|
||||||
- [ ] Paste
|
- [ ] Paste
|
||||||
- [ ] Cut
|
- [ ] Cut
|
||||||
|
|
||||||
|
|||||||
59
src/GUI.py
59
src/GUI.py
@@ -12,6 +12,8 @@ from utils.text_entry import TextEntryHandler
|
|||||||
from utils.file_operations import FileOperationsHandler
|
from utils.file_operations import FileOperationsHandler
|
||||||
from utils.image_properties import ImagePropertiesHandler
|
from utils.image_properties import ImagePropertiesHandler
|
||||||
from utils.filter_params import FilterParamsHandler
|
from utils.filter_params import FilterParamsHandler
|
||||||
|
from ImageManipulation.CopyToClipboard import CopyToClipboard
|
||||||
|
from ImageManipulation.PasteFromClipboard import PasteFromClipboard
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
|
|
||||||
@@ -126,6 +128,8 @@ class GUI:
|
|||||||
filter_menu.add_command(label="Histogram Thresholding", command=lambda: self._applyHistogramThreshold())
|
filter_menu.add_command(label="Histogram Thresholding", command=lambda: self._applyHistogramThreshold())
|
||||||
clipboard_menu = tk.Menu(menu, tearoff=0)
|
clipboard_menu = tk.Menu(menu, tearoff=0)
|
||||||
menu.add_cascade(label="Clipboard", menu=clipboard_menu)
|
menu.add_cascade(label="Clipboard", menu=clipboard_menu)
|
||||||
|
clipboard_menu.add_command(label="Copy Image", accelerator="Ctrl+C", command=lambda: self._copyImage())
|
||||||
|
clipboard_menu.add_command(label="Paste Image", accelerator="Ctrl+V", command=lambda: self._pasteImage())
|
||||||
tools_menu = tk.Menu(menu, tearoff=0)
|
tools_menu = tk.Menu(menu, tearoff=0)
|
||||||
menu.add_cascade(label="Tools", menu=tools_menu)
|
menu.add_cascade(label="Tools", menu=tools_menu)
|
||||||
tools_menu.add_command(label="Zoom In", accelerator="Ctrl+Plus", command=lambda: self._zoomIn())
|
tools_menu.add_command(label="Zoom In", accelerator="Ctrl+Plus", command=lambda: self._zoomIn())
|
||||||
@@ -239,6 +243,8 @@ class GUI:
|
|||||||
root.bind_all('<Control-plus>', lambda event: self._zoomIn())
|
root.bind_all('<Control-plus>', lambda event: self._zoomIn())
|
||||||
root.bind_all('<Control-equal>', lambda event: self._zoomIn()) # Plus key without shift
|
root.bind_all('<Control-equal>', lambda event: self._zoomIn()) # Plus key without shift
|
||||||
root.bind_all('<Control-minus>', lambda event: self._zoomOut())
|
root.bind_all('<Control-minus>', lambda event: self._zoomOut())
|
||||||
|
root.bind_all('<Control-c>', lambda event: self._copyImage())
|
||||||
|
root.bind_all('<Control-v>', lambda event: self._pasteImage())
|
||||||
|
|
||||||
root.mainloop()
|
root.mainloop()
|
||||||
|
|
||||||
@@ -567,6 +573,59 @@ class GUI:
|
|||||||
params = {"scale": 0.8}
|
params = {"scale": 0.8}
|
||||||
self._applyManipulation(manipulation, params)
|
self._applyManipulation(manipulation, params)
|
||||||
|
|
||||||
|
def _copyImage(self) -> None:
|
||||||
|
"""Copy the current image to the system clipboard."""
|
||||||
|
if self._currentImage is None or self._currentImage.getImage() is None:
|
||||||
|
messagebox.showwarning("No Image", "Please load an image first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
manipulation = CopyToClipboard()
|
||||||
|
manipulation.manipulateImage(self._currentImage, {})
|
||||||
|
|
||||||
|
# Check if copy was successful
|
||||||
|
if self._currentImage._clipboard_copy_success:
|
||||||
|
messagebox.showinfo("Copy", "Image copied to clipboard.")
|
||||||
|
else:
|
||||||
|
messagebox.showerror(
|
||||||
|
"Copy Error",
|
||||||
|
"Failed to copy image to clipboard.\n\n"
|
||||||
|
"Windows: Please install pywin32 (pip install pywin32)\n"
|
||||||
|
"Linux: Please install xclip (sudo apt-get install xclip)"
|
||||||
|
)
|
||||||
|
# Clean up the attribute
|
||||||
|
delattr(self._currentImage, '_clipboard_copy_success')
|
||||||
|
|
||||||
|
def _pasteImage(self) -> None:
|
||||||
|
"""Paste an image from the system clipboard."""
|
||||||
|
manipulation = PasteFromClipboard()
|
||||||
|
|
||||||
|
# If no current image exists, create a new ImageContainer
|
||||||
|
if self._currentImage is None or self._currentImage.getImage() is None:
|
||||||
|
self._currentImage = ImageContainer()
|
||||||
|
|
||||||
|
manipulation.manipulateImage(self._currentImage, {})
|
||||||
|
|
||||||
|
# Check if paste was successful
|
||||||
|
if hasattr(self._currentImage, '_clipboard_paste_success'):
|
||||||
|
if self._currentImage._clipboard_paste_success:
|
||||||
|
# Re-render the image
|
||||||
|
self._renderCurrentImage()
|
||||||
|
messagebox.showinfo("Paste", "Image pasted from clipboard.")
|
||||||
|
else:
|
||||||
|
error_msg = getattr(self._currentImage, '_clipboard_paste_error',
|
||||||
|
"Failed to paste image from clipboard.")
|
||||||
|
messagebox.showerror(
|
||||||
|
"Paste Error",
|
||||||
|
f"{error_msg}\n\n"
|
||||||
|
"Windows: Please install pywin32 (pip install pywin32)\n"
|
||||||
|
"Linux: Please install xclip (sudo apt-get install xclip)"
|
||||||
|
)
|
||||||
|
# Clean up the attributes
|
||||||
|
if hasattr(self._currentImage, '_clipboard_paste_success'):
|
||||||
|
delattr(self._currentImage, '_clipboard_paste_success')
|
||||||
|
if hasattr(self._currentImage, '_clipboard_paste_error'):
|
||||||
|
delattr(self._currentImage, '_clipboard_paste_error')
|
||||||
|
|
||||||
def _openCamera(self):
|
def _openCamera(self):
|
||||||
camera = CameraWindow()
|
camera = CameraWindow()
|
||||||
camera.run(self)
|
camera.run(self)
|
||||||
26
src/ImageManipulation/CopyToClipboard.py
Normal file
26
src/ImageManipulation/CopyToClipboard.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from .ImageManipulation import ImageManipulation
|
||||||
|
from ImageContainer import ImageContainer
|
||||||
|
from typing import Any, List
|
||||||
|
from utils.clipboard_utils import copy_image_to_clipboard
|
||||||
|
|
||||||
|
|
||||||
|
class CopyToClipboard(ImageManipulation):
|
||||||
|
"""Image manipulation class for copying images to the system clipboard."""
|
||||||
|
|
||||||
|
def getManipulationName(self) -> str:
|
||||||
|
return "Copy to Clipboard"
|
||||||
|
|
||||||
|
def getParameters(self) -> List[str]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def manipulateImage(self, image: ImageContainer, parameters: Any) -> None:
|
||||||
|
if image is None or image.getImage() is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
pil_img = image.getImage()
|
||||||
|
# Copy image to clipboard (doesn't modify the image)
|
||||||
|
# Store success status for later retrieval
|
||||||
|
success = copy_image_to_clipboard(pil_img)
|
||||||
|
# Add attribute to track clipboard copy success
|
||||||
|
image._clipboard_copy_success = success
|
||||||
|
|
||||||
@@ -36,4 +36,5 @@ def GetImageManipulationList() -> list:
|
|||||||
BackgroundBlur(),
|
BackgroundBlur(),
|
||||||
CameraWindow(),
|
CameraWindow(),
|
||||||
AddText(),
|
AddText(),
|
||||||
|
ZoomImage(),
|
||||||
]
|
]
|
||||||
29
src/ImageManipulation/PasteFromClipboard.py
Normal file
29
src/ImageManipulation/PasteFromClipboard.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from .ImageManipulation import ImageManipulation
|
||||||
|
from ImageContainer import ImageContainer
|
||||||
|
from typing import Any, List, Optional
|
||||||
|
from utils.clipboard_utils import get_image_from_clipboard
|
||||||
|
|
||||||
|
|
||||||
|
class PasteFromClipboard(ImageManipulation):
|
||||||
|
"""Image manipulation class for pasting images from the system clipboard."""
|
||||||
|
|
||||||
|
def getManipulationName(self) -> str:
|
||||||
|
return "Paste from Clipboard"
|
||||||
|
|
||||||
|
def getParameters(self) -> List[str]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def manipulateImage(self, image: ImageContainer, parameters: Any) -> None:
|
||||||
|
# Get image from clipboard
|
||||||
|
clipboard_image = get_image_from_clipboard()
|
||||||
|
|
||||||
|
# Store the result for GUI to check
|
||||||
|
if clipboard_image is None:
|
||||||
|
image._clipboard_paste_success = False
|
||||||
|
image._clipboard_paste_error = "No image found in clipboard."
|
||||||
|
return
|
||||||
|
|
||||||
|
# Replace the current image with the clipboard image (or set it if none exists)
|
||||||
|
image._imageData = clipboard_image
|
||||||
|
image._clipboard_paste_success = True
|
||||||
|
|
||||||
268
src/utils/clipboard_utils.py
Normal file
268
src/utils/clipboard_utils.py
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
"""Utilities for copying images to the system clipboard.
|
||||||
|
Should support both Windows and Linux.
|
||||||
|
This module provides functions to copy and paste images to the clipboard.
|
||||||
|
It uses the win32clipboard module for Windows and xclip for Linux.
|
||||||
|
If xclip is not available, it uses xsel as a fallback.
|
||||||
|
If both xclip and xsel are not available, it returns False.
|
||||||
|
"""
|
||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
|
import io
|
||||||
|
from PIL import Image
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
def copy_image_to_clipboard(image: Image.Image) -> bool:
|
||||||
|
"""Copy a PIL Image to the system clipboard.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image: PIL Image to copy to clipboard
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
if image is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
system = platform.system()
|
||||||
|
|
||||||
|
if system == "Windows":
|
||||||
|
return _copy_to_clipboard_windows(image)
|
||||||
|
elif system == "Linux":
|
||||||
|
return _copy_to_clipboard_linux(image)
|
||||||
|
else:
|
||||||
|
# Unsupported platform
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_to_clipboard_windows(image: Image.Image) -> bool:
|
||||||
|
"""Copy image to clipboard on Windows using win32clipboard."""
|
||||||
|
try:
|
||||||
|
import win32clipboard
|
||||||
|
from win32clipboard import CF_DIB
|
||||||
|
|
||||||
|
# Convert image to RGB if needed
|
||||||
|
if image.mode != "RGB":
|
||||||
|
image = image.convert("RGB")
|
||||||
|
|
||||||
|
# Save image to bytes in BMP format (Windows clipboard format)
|
||||||
|
output = io.BytesIO()
|
||||||
|
image.save(output, "BMP")
|
||||||
|
data = output.getvalue()[14:] # Remove BMP header (14 bytes)
|
||||||
|
output.close()
|
||||||
|
|
||||||
|
# Open clipboard and set data
|
||||||
|
win32clipboard.OpenClipboard()
|
||||||
|
win32clipboard.EmptyClipboard()
|
||||||
|
win32clipboard.SetClipboardData(CF_DIB, data)
|
||||||
|
win32clipboard.CloseClipboard()
|
||||||
|
|
||||||
|
return True
|
||||||
|
except ImportError:
|
||||||
|
# pywin32 not installed, try alternative method
|
||||||
|
return _copy_to_clipboard_windows_alternative(image)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_to_clipboard_windows_alternative(image: Image.Image) -> bool:
|
||||||
|
"""Alternative method for Windows using PowerShell (fallback)."""
|
||||||
|
try:
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Save image to temporary file
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_file:
|
||||||
|
temp_path = tmp_file.name
|
||||||
|
image.save(temp_path, "PNG")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Use PowerShell to copy image to clipboard
|
||||||
|
# Escape backslashes for PowerShell
|
||||||
|
escaped_path = temp_path.replace("\\", "\\\\")
|
||||||
|
ps_command = f'''
|
||||||
|
Add-Type -AssemblyName System.Windows.Forms
|
||||||
|
$image = [System.Drawing.Image]::FromFile("{escaped_path}")
|
||||||
|
[System.Windows.Forms.Clipboard]::SetImage($image)
|
||||||
|
$image.Dispose()
|
||||||
|
'''
|
||||||
|
subprocess.run(
|
||||||
|
["powershell", "-Command", ps_command],
|
||||||
|
check=True,
|
||||||
|
capture_output=True
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
finally:
|
||||||
|
# Clean up temporary file
|
||||||
|
try:
|
||||||
|
os.unlink(temp_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_to_clipboard_linux(image: Image.Image) -> bool:
|
||||||
|
"""Copy image to clipboard on Linux using xclip."""
|
||||||
|
try:
|
||||||
|
# Convert image to PNG bytes
|
||||||
|
output = io.BytesIO()
|
||||||
|
image.save(output, "PNG")
|
||||||
|
data = output.getvalue()
|
||||||
|
output.close()
|
||||||
|
|
||||||
|
# Check if xclip is available
|
||||||
|
try:
|
||||||
|
subprocess.run(["which", "xclip"], check=True, capture_output=True)
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
|
# xclip not found, try xsel as alternative
|
||||||
|
return _copy_to_clipboard_linux_xsel(image, data)
|
||||||
|
|
||||||
|
# Use xclip to copy image to clipboard
|
||||||
|
process = subprocess.Popen(
|
||||||
|
["xclip", "-selection", "clipboard", "-t", "image/png"],
|
||||||
|
stdin=subprocess.PIPE
|
||||||
|
)
|
||||||
|
process.communicate(input=data)
|
||||||
|
|
||||||
|
if process.returncode == 0:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# Try xsel as fallback
|
||||||
|
return _copy_to_clipboard_linux_xsel(image, data)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_to_clipboard_linux_xsel(image: Image.Image, data: Optional[bytes] = None) -> bool:
|
||||||
|
"""Alternative method for Linux using xsel (fallback)."""
|
||||||
|
try:
|
||||||
|
if data is None:
|
||||||
|
output = io.BytesIO()
|
||||||
|
image.save(output, "PNG")
|
||||||
|
data = output.getvalue()
|
||||||
|
output.close()
|
||||||
|
|
||||||
|
# Check if xsel is available
|
||||||
|
try:
|
||||||
|
subprocess.run(["which", "xsel"], check=True, capture_output=True)
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# xsel doesn't support images directly, so this is a fallback that may not work
|
||||||
|
# For now, return False to indicate xsel can't handle images
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_image_from_clipboard() -> Optional[Image.Image]:
|
||||||
|
"""Get a PIL Image from the system clipboard.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PIL Image if successful, None otherwise
|
||||||
|
"""
|
||||||
|
system = platform.system()
|
||||||
|
|
||||||
|
if system == "Windows":
|
||||||
|
return _get_from_clipboard_windows()
|
||||||
|
elif system == "Linux":
|
||||||
|
return _get_from_clipboard_linux()
|
||||||
|
else:
|
||||||
|
# Unsupported platform
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_from_clipboard_windows() -> Optional[Image.Image]:
|
||||||
|
"""Get image from clipboard on Windows using win32clipboard."""
|
||||||
|
try:
|
||||||
|
import win32clipboard
|
||||||
|
from win32clipboard import CF_DIB
|
||||||
|
|
||||||
|
win32clipboard.OpenClipboard()
|
||||||
|
try:
|
||||||
|
if win32clipboard.IsClipboardFormatAvailable(CF_DIB):
|
||||||
|
data = win32clipboard.GetClipboardData(CF_DIB)
|
||||||
|
# Add BMP header (14 bytes)
|
||||||
|
bmp_header = b'BM' + (len(data) + 14).to_bytes(4, 'little') + b'\x00\x00\x00\x00' + (14 + 40).to_bytes(4, 'little')
|
||||||
|
bmp_data = bmp_header + data
|
||||||
|
image = Image.open(io.BytesIO(bmp_data))
|
||||||
|
return image
|
||||||
|
finally:
|
||||||
|
win32clipboard.CloseClipboard()
|
||||||
|
return None
|
||||||
|
except ImportError:
|
||||||
|
# pywin32 not installed, try alternative method
|
||||||
|
return _get_from_clipboard_windows_alternative()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_from_clipboard_windows_alternative() -> Optional[Image.Image]:
|
||||||
|
"""Alternative method for Windows using PowerShell (fallback)."""
|
||||||
|
try:
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Use PowerShell to get image from clipboard and save to temp file
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_file:
|
||||||
|
temp_path = tmp_file.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Escape backslashes for PowerShell
|
||||||
|
escaped_path = temp_path.replace("\\", "\\\\")
|
||||||
|
ps_command = f'''
|
||||||
|
Add-Type -AssemblyName System.Windows.Forms
|
||||||
|
if ([System.Windows.Forms.Clipboard]::ContainsImage()) {{
|
||||||
|
$image = [System.Windows.Forms.Clipboard]::GetImage()
|
||||||
|
$image.Save("{escaped_path}")
|
||||||
|
$image.Dispose()
|
||||||
|
}}
|
||||||
|
'''
|
||||||
|
result = subprocess.run(
|
||||||
|
["powershell", "-Command", ps_command],
|
||||||
|
check=True,
|
||||||
|
capture_output=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if os.path.exists(temp_path) and os.path.getsize(temp_path) > 0:
|
||||||
|
image = Image.open(temp_path)
|
||||||
|
os.unlink(temp_path)
|
||||||
|
return image
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
# Clean up temp file on error
|
||||||
|
try:
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
os.unlink(temp_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_from_clipboard_linux() -> Optional[Image.Image]:
|
||||||
|
"""Get image from clipboard on Linux using xclip."""
|
||||||
|
try:
|
||||||
|
# Check if xclip is available
|
||||||
|
try:
|
||||||
|
subprocess.run(["which", "xclip"], check=True, capture_output=True)
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Use xclip to get image from clipboard
|
||||||
|
process = subprocess.Popen(
|
||||||
|
["xclip", "-selection", "clipboard", "-t", "image/png", "-o"],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
|
stdout, stderr = process.communicate()
|
||||||
|
|
||||||
|
if process.returncode == 0 and len(stdout) > 0:
|
||||||
|
image = Image.open(io.BytesIO(stdout))
|
||||||
|
return image
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
Reference in New Issue
Block a user