diff --git a/README.md b/README.md index 7086713..1899d7e 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ File menu - [x] Quit Clipboard menu -- [ ] Copy +- [x] Copy - [ ] Paste - [ ] Cut diff --git a/src/GUI.py b/src/GUI.py index 052b5ef..0bc652f 100644 --- a/src/GUI.py +++ b/src/GUI.py @@ -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('', lambda event: self._zoomIn()) root.bind_all('', lambda event: self._zoomIn()) # Plus key without shift root.bind_all('', lambda event: self._zoomOut()) + root.bind_all('', lambda event: self._copyImage()) + root.bind_all('', 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) \ No newline at end of file diff --git a/src/ImageManipulation/CopyToClipboard.py b/src/ImageManipulation/CopyToClipboard.py new file mode 100644 index 0000000..32022a3 --- /dev/null +++ b/src/ImageManipulation/CopyToClipboard.py @@ -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 + diff --git a/src/ImageManipulation/ManipulationList.py b/src/ImageManipulation/ManipulationList.py index 92bd74d..6408463 100644 --- a/src/ImageManipulation/ManipulationList.py +++ b/src/ImageManipulation/ManipulationList.py @@ -36,4 +36,5 @@ def GetImageManipulationList() -> list: BackgroundBlur(), CameraWindow(), AddText(), + ZoomImage(), ] \ No newline at end of file diff --git a/src/ImageManipulation/PasteFromClipboard.py b/src/ImageManipulation/PasteFromClipboard.py new file mode 100644 index 0000000..82d6bb1 --- /dev/null +++ b/src/ImageManipulation/PasteFromClipboard.py @@ -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 + diff --git a/src/utils/clipboard_utils.py b/src/utils/clipboard_utils.py new file mode 100644 index 0000000..78798a1 --- /dev/null +++ b/src/utils/clipboard_utils.py @@ -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 +