adding copy image from canvas; adding paste image to canvas

This commit is contained in:
vb
2025-11-07 14:51:01 +01:00
parent eaa6b6f4d7
commit e6599a1cd7
6 changed files with 384 additions and 1 deletions

View File

@@ -12,7 +12,7 @@ File menu
- [x] Quit
Clipboard menu
- [ ] Copy
- [x] Copy
- [ ] Paste
- [ ] Cut

View File

@@ -12,6 +12,8 @@ from utils.text_entry import TextEntryHandler
from utils.file_operations import FileOperationsHandler
from utils.image_properties import ImagePropertiesHandler
from utils.filter_params import FilterParamsHandler
from ImageManipulation.CopyToClipboard import CopyToClipboard
from ImageManipulation.PasteFromClipboard import PasteFromClipboard
from functools import partial
@@ -126,6 +128,8 @@ class GUI:
filter_menu.add_command(label="Histogram Thresholding", command=lambda: self._applyHistogramThreshold())
clipboard_menu = tk.Menu(menu, tearoff=0)
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)
menu.add_cascade(label="Tools", menu=tools_menu)
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-equal>', lambda event: self._zoomIn()) # Plus key without shift
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()
@@ -567,6 +573,59 @@ class GUI:
params = {"scale": 0.8}
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):
camera = CameraWindow()
camera.run(self)

View 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

View File

@@ -36,4 +36,5 @@ def GetImageManipulationList() -> list:
BackgroundBlur(),
CameraWindow(),
AddText(),
ZoomImage(),
]

View 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

View 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