Skip to content

Commit

Permalink
3.0.0 release: Change Windows behavior, using PIL if available (defau…
Browse files Browse the repository at this point in the history
…lting to DIB format). Lots of comment updates & README updates.
  • Loading branch information
Josh Burnett committed Mar 29, 2021
1 parent 78325ab commit dd70c44
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 97 deletions.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2016 joshburnett

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
3 changes: 2 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
include README.md
include README.md
include LICENSE
46 changes: 39 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,54 @@
addcopyfighandler: Add a Ctrl+C handler to matplotlib figures for copying the figure to the clipboard
======================================================================================================

Simply importing this module (after importing matplotlib or pyplot) will add a handler
Importing this module (after importing matplotlib or pyplot) will add a handler
to all subsequently-created matplotlib figures
so that pressing Ctrl+C with a matplotlib figure window selected will copy
the figure to the clipboard as an image.
the figure to the clipboard as an image. The copied image is generated through
matplotlib.pyplot.savefig(), and thus is affected by the relevant rcParams
settings (savefig.dpi, savefig.format, etc.).

Original concept code taken from:
https://stackoverflow.com/questions/31607458/how-to-add-clipboard-support-to-matplotlib-figures
Windows and Linux are currently supported. Pull requests implementing macOS support are welcome.

Uses code & concepts from:
- https://stackoverflow.com/questions/31607458/how-to-add-clipboard-support-to-matplotlib-figures
- https://stackoverflow.com/questions/34322132/copy-image-to-clipboard-in-python3


## Windows-specific behavior:

- addcopyfighandler should work regardless of which graphical backend is being used by matplotlib
(tkagg, gtk3agg, qt5agg, etc.).
- If `matplotlib.rcParams['savefig.format']` is `'svg'`, the figure will be copied to the clipboard
as an SVG.
- If Pillow is installed, all non-SVG format specifiers will be overridden, and the
figure will be copied to the clipboard as a Device-Independant Bitmap.
- If Pillow is not installed, the supported format specifiers are `'png'`, `'jpg'`, `'jpeg'`, and `'svg'`.
All other format specifiers will be overridden, and the figure will be copied to the clipboard as PNG data.


## Linux-specific behavior:

- Requires either Qt or GTK libraries for clipboard interaction. Automatically detects which is being used from
`matplotlib.get_backend()`.
- Qt support requires `PyQt5` or `PySide2`.
- GTK support requires `pycairo`, `PyGObject` and `PIL` or `pillow` to be installed.
- The figure will be copied to the clipboard as a PNG, regardless of `matplotlib.rcParams['savefig.format']`. Alas, SVG output is not currently supported. Pull requests that enable SVG support would be welcomed.


Releases
--------
### 3.0.0: 2021-03-28

- Add Linux support (tested on Ubuntu). Requires PyQt5, PySide2, or PyObject libraries; relevant library chosen based on matplotlib graphical backend in use. No SVG support.
- On Windows, non SVG-formats will now use the Pillow library if installed, storing the figure to the clipboard as a device-indepenent bitmap (as previously handled in v2.0). This is compatible with a wider range of Windows applications.

### 2.1.0: 2020-08-27

- Remove Pillow
- Add support for png, svg, jpg, jpeg fileformat.
- Remove Pillow.
- Add support for png & svg file formats.

### 2.0.0: 2019-04-15
### 2.0.0: 2019-06-07

- Remove Qt requirement. Now use Pillow to grab the figure image, and win32clipboard to manage the Windows clipboard.

Expand Down
141 changes: 67 additions & 74 deletions addcopyfighandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
"""
Monkey-patch plt.figure() to support Ctrl+C for copying to clipboard as an image
Importing this module (after importing matplotlib or pyplot) will add a handler
to all subsequently-created matplotlib figures
so that pressing Ctrl+C with a matplotlib figure window in the foreground will copy
the figure to the clipboard as an image. The copied image is generated through
matplotlib.pyplot.savefig(), and thus is affected by the relevant matplotlib.rcParams
settings (savefig.dpi, savefig.format, etc.).
@authors: Josh Burnett, Sylvain Finot
Modified from code found on Stack Exchange:
https://stackoverflow.com/questions/31607458/how-to-add-clipboard-support-to-matplotlib-figures
Expand All @@ -11,22 +18,29 @@
On Windows:
- addcopyfighandler should work regardless of which graphical backend is being used by matplotlib
(tkagg, gtk3agg, qt4agg, qt5agg, etc.)
- Supported clipboard image file formats are PNG, SVG, JPG, and JPEG.
(tkagg, gtk3agg, qt5agg, etc.)
- If matplotlib.rcParams['savefig.format'] is 'svg,' the figure will be copied to the clipboard
as an SVG.
- If Pillow is installed, all non-SVG format specifiers will be overridden and the
figure will be copied to the clipboard as a Device-Independant Bitmap.
- If Pillow is not installed, the supported format specifiers are 'png,' 'jpg,' 'jpeg,' and 'svg.'
All other format specifiers will be overridden and the figure will be copied to the clipboard as PNG data.
On Linux:
- Requires either Qt or GTK libraries for clipboard interaction. Automatically detects which is being used from
matplotlib.get_backend().
- Qt support requires one of (PyQt4, PyQt5, PySide2).
- Qt support requires PyQt5 or PySide2.
- GTK support requires pycairo, PyGObject and PIL/pillow to be installed.
- The figure will be copied to the clipboard as a PNG, regardless of matplotlib.rcParams['savefig.format'].
"""

import platform
import matplotlib.pyplot as plt
from io import BytesIO

__version__ = (3, 0, 0)
__version__ = '3.0.0'
__version_info__ = tuple(int(i) if i.isdigit() else i for i in __version__.split('.'))

oldfig = plt.figure

ostype = platform.system().lower()
Expand Down Expand Up @@ -57,40 +71,54 @@ def copyfig(fig=None, format=None, *args, **kwargs):
"""

# Determined available values by digging into windows API
format_map = {"png": "PNG",
"svg": "image/svg+xml",
"jpg": "JFIF",
"jpeg": "JFIF"}
format_map = {'png': 'PNG',
'svg': 'image/svg+xml',
'jpg': 'JFIF',
'jpeg': 'JFIF',
}

# If no format is passed to savefig get the default one
if format is None:
format = plt.rcParams["savefig.format"]
format = plt.rcParams['savefig.format']
format = format.lower()

if format not in format_map:
raise ValueError(f"Format {format} is not supported "
f"(supported formats: {', '.join(list(format_map.keys()))})")
format = 'png'

if fig is None:
# Find the figure window that has UI focus right now (not necessarily
# the same as plt.gcf() when in interactive mode)
fig_window_text = GetWindowText(GetForegroundWindow())
for i in plt.get_fignums():
if plt.figure(i).canvas.get_window_title() == fig_window_text:
if plt.figure(i).canvas.manager.get_window_title() == fig_window_text:
fig = plt.figure(i)
break

if fig is None:
raise AttributeError("No figure found!")
raise AttributeError('No figure found!')

# Store the image in a buffer using savefig(). This has the
# advantage of applying all the default savefig parameters
# such as resolution and background color, which would be ignored
# if you simply grab the canvas.
format_id = win32clipboard.RegisterClipboardFormat(format_map[format])
# if we simply grab the canvas as displayed.
with BytesIO() as buf:
fig.savefig(buf, format=format, *args, **kwargs)
data = buf.getvalue()

if format != 'svg':
try:
from PIL import Image
im = Image.open(buf)
with BytesIO() as output:
im.convert("RGB").save(output, "BMP")
data = output.getvalue()[14:] # The file header off-set of BMP is 14 bytes
format_id = win32clipboard.CF_DIB # DIB = device independent bitmap

except ImportError:
data = buf.getvalue()
format_id = win32clipboard.RegisterClipboardFormat(format_map[format])
else:
data = buf.getvalue()
format_id = win32clipboard.RegisterClipboardFormat(format_map[format])

win32clipboard.OpenClipboard()
win32clipboard.EmptyClipboard()
Expand All @@ -99,20 +127,16 @@ def copyfig(fig=None, format=None, *args, **kwargs):

elif ostype == 'linux':
backend = plt.get_backend()
if backend in ['Qt5Agg', 'Qt4Agg']:
if backend == 'Qt4Agg':
from PyQt4.QtGui import QApplication, QImage
clipboard = QApplication.clipboard
if backend == 'Qt5Agg':
try:
from PySide2.QtGui import QGuiApplication, QImage
from PySide2.QtWidgets import QApplication
except ImportError:
from PyQt5.QtGui import QGuiApplication, QImage
from PyQt5.QtWidgets import QApplication
clipboard = QGuiApplication.clipboard

def copyfig(fig=None, format=None, *args, **kwargs):
if backend == 'Qt5Agg':
try:
from PySide2.QtGui import QGuiApplication, QImage
from PySide2.QtWidgets import QApplication
except ImportError:
from PyQt5.QtGui import QGuiApplication, QImage
from PyQt5.QtWidgets import QApplication
clipboard = QGuiApplication.clipboard

def copyfig(fig=None, *args, **kwargs):
"""
Parameters
----------
Expand All @@ -130,37 +154,29 @@ def copyfig(fig=None, format=None, *args, **kwargs):
If no figure is found
"""

if format is None:
format = plt.rcParams["savefig.format"]
format = format.lower()

formats = ['png', 'jpg', 'jpeg', 'tiff']
if format not in formats:
format = 'png'

if fig is None:
# Find the figure window that has UI focus right now (not necessarily
# the same as plt.gcf() when in interactive mode)
fig_window_text = QApplication.activeWindow().windowTitle()
for i in plt.get_fignums():
if plt.figure(i).canvas.get_window_title() == fig_window_text:
if plt.figure(i).canvas.manager.get_window_title() == fig_window_text:
fig = plt.figure(i)
break

if fig is None:
raise AttributeError("No figure found!")
raise AttributeError('No figure found!')

# Store the image in a buffer using savefig(). This has the
# advantage of applying all the default savefig parameters
# such as resolution and background color, which would be ignored
# if you simply grab the canvas.
# if we simply grab the canvas as displayed.
with BytesIO() as buf:
fig.savefig(buf, format=format, *args, **kwargs)
fig.savefig(buf, format='png', *args, **kwargs)
clipboard().setImage(QImage.fromData(buf.getvalue()))

elif backend == 'GTK3Agg':
import gi
gi.require_version("Gtk", "3.0")
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository.Gtk import Clipboard
from gi.repository import GLib, GdkPixbuf, Gdk
Expand All @@ -169,15 +185,12 @@ def copyfig(fig=None, format=None, *args, **kwargs):

clipboard = Clipboard.get(Gdk.SELECTION_CLIPBOARD)

def copyfig(fig=None, format=None, *args, **kwargs):
def copyfig(fig=None, *args, **kwargs):
"""
Parameters
----------
fig : matplotlib figure, optional
If None, get the figure that has UI focus
format : type of image to be pasted to the clipboard ('png', 'jpg', 'jpeg', 'tiff')
If None, uses matplotlib.rcParams["savefig.format"]
If resulting format is not in ('png', 'jpg', 'jpeg', 'tiff'), will override to PNG.
*args : arguments that are passed to savefig
**kwargs : keywords arguments that are passed to savefig
Expand All @@ -187,20 +200,6 @@ def copyfig(fig=None, format=None, *args, **kwargs):
If no figure is found
"""

if format is None:
format = plt.rcParams["savefig.format"]
format = format.lower()

format_map = {
'png': 'PNG',
'jpg': 'JPEG',
'jpeg': 'JPEG',
'tiff': 'TIFF',
}

if format not in format_map:
format = 'png'

if fig is None:
# Find the figure window that has UI focus right now (not necessarily
# the same as plt.gcf() when in interactive mode)
Expand All @@ -214,33 +213,27 @@ def copyfig(fig=None, format=None, *args, **kwargs):
)

for i in plt.get_fignums():
if plt.figure(i).canvas.get_window_title() == fig_window_text:
if plt.figure(i).canvas.manager.get_window_title() == fig_window_text:
fig = plt.figure(i)
break

# Store the image in a buffer using savefig(). This has the
# advantage of applying all the default savefig parameters
# such as resolution and background color, which would be ignored
# if you simply grab the canvas.
# if we simply grab the canvas as displayed.
with BytesIO() as buf:
fig.savefig(buf, format=format, *args, **kwargs)
im = Image.open(buf, formats=[format_map[format]])
im = Image.open(buf, formats=['PNG'])

w, h = im.size
data = GLib.Bytes.new(im.tobytes())
if im.mode == 'RGBA':
pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(data, GdkPixbuf.Colorspace.RGB,
True, 8, w, h, w * 4)
elif im.mode == 'RGB':
pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(data, GdkPixbuf.Colorspace.RGB,
False, 8, w, h, w * 3)
else:
raise ValueError(f'Unsupported image format ({im.mode}). Must be RGB or RGBA.')
pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(data, GdkPixbuf.Colorspace.RGB,
True, 8, w, h, w * 4)

clipboard.set_image(pixbuf)
clipboard.store()
else:
raise ValueError(f'Unsupported matplotlib backend ({backend}). On Linux must be Qt4Agg, Qt5Agg, or GTK3Agg.')
raise ValueError(f'Unsupported matplotlib backend ({backend}). On Linux must be Qt5Agg, or GTK3Agg.')

else:
raise ValueError(f'addcopyfighandler: Supported OSes are Windows and Linux. Current OS: {ostype}')
Expand Down
3 changes: 2 additions & 1 deletion generate_source_dist.bat
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
python setup.py sdist --formats=zip bdist_wheel --universal
python setup.py sdist --formats=gztar
python setup.py bdist_wheel --universal
Loading

0 comments on commit dd70c44

Please sign in to comment.