Skip to content

Instantly share code, notes, and snippets.

@aspotton
Forked from initbrain/screenshot.py
Last active January 11, 2023 15:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save aspotton/1888298869c8adf59a577f2fe9d32fc8 to your computer and use it in GitHub Desktop.
Save aspotton/1888298869c8adf59a577f2fe9d32fc8 to your computer and use it in GitHub Desktop.
Python screenshot tool (fullscreen/area selection)
#!/usr/bin/env python
# Python screenshot tool (fullscreen/area selection)
import sys
import os
from io import BytesIO
from PyQt5 import QtCore, QtGui
from PyQt5.QtGui import QPixmap, QScreen
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QSizePolicy, QGroupBox, QSpinBox, QCheckBox, QGridLayout, QPushButton, QHBoxLayout, QVBoxLayout, QFileDialog
from subprocess import getoutput
from io import StringIO
from Xlib import X, display, Xutil
# Documentation for python-xlib here:
# http://python-xlib.sourceforge.net/doc/html/index.html
class XSelect:
def __init__(self, display):
# X display
self.d = display
# Screen
self.screen = self.d.screen()
# Draw on the root window (desktop surface)
self.window = self.screen.root
# If only I could get this working...
#cursor = xobject.cursor.Cursor(self.d, Xcursorfont.crosshair)
#cursor = self.d.create_resource_object('cursor', Xcursorfont.X_cursor)
self.cursor = X.NONE
colormap = self.screen.default_colormap
color = colormap.alloc_color(0, 0, 0)
# Xor it because we'll draw with X.GXxor function
xor_color = color.pixel ^ 0xffffff
self.gc = self.window.create_gc(
line_width = 1,
line_style = X.LineSolid,
fill_style = X.FillOpaqueStippled,
fill_rule = X.WindingRule,
cap_style = X.CapButt,
join_style = X.JoinMiter,
foreground = xor_color,
background = self.screen.black_pixel,
function = X.GXxor,
graphics_exposures = False,
subwindow_mode = X.IncludeInferiors,
)
def get_mouse_selection(self):
started = False
start = dict(x=0, y=0)
end = dict(x=0, y=0)
last = None
drawlimit = 10
i = 0
self.window.grab_pointer(self.d, X.PointerMotionMask|X.ButtonReleaseMask|X.ButtonPressMask,
X.GrabModeAsync, X.GrabModeAsync, X.NONE, self.cursor, X.CurrentTime)
self.window.grab_keyboard(self.d, X.GrabModeAsync, X.GrabModeAsync, X.CurrentTime)
while True:
e = self.d.next_event()
# Window has been destroyed, quit
if e.type == X.DestroyNotify:
break
# Mouse button press
elif e.type == X.ButtonPress:
# Left mouse button?
if e.detail == 1:
start = dict(x=e.root_x, y=e.root_y)
started = True
# Right mouse button?
elif e.detail == 3:
return
# Mouse button release
elif e.type == X.ButtonRelease:
end = dict(x=e.root_x, y=e.root_y)
if last:
self.draw_rectangle(start, last)
break
# Mouse movement
elif e.type == X.MotionNotify and started:
i = i + 1
if i % drawlimit != 0:
continue
if last:
self.draw_rectangle(start, last)
last = None
last = dict(x=e.root_x, y=e.root_y)
self.draw_rectangle(start, last)
self.d.ungrab_keyboard(X.CurrentTime)
self.d.ungrab_pointer(X.CurrentTime)
self.d.sync()
coords = self.get_coords(start, end)
if coords['width'] <= 1 or coords['height'] <= 1:
return
return [coords['start']['x'], coords['start']['y'], coords['width'], coords['height']]
def get_coords(self, start, end):
safe_start = dict(x=0, y=0)
safe_end = dict(x=0, y=0)
if start['x'] > end['x']:
safe_start['x'] = end['x']
safe_end['x'] = start['x']
else:
safe_start['x'] = start['x']
safe_end['x'] = end['x']
if start['y'] > end['y']:
safe_start['y'] = end['y']
safe_end['y'] = start['y']
else:
safe_start['y'] = start['y']
safe_end['y'] = end['y']
return {
'start': {
'x': safe_start['x'],
'y': safe_start['y'],
},
'end': {
'x': safe_end['x'],
'y': safe_end['y'],
},
'width' : safe_end['x'] - safe_start['x'],
'height': safe_end['y'] - safe_start['y'],
}
def draw_rectangle(self, start, end):
coords = self.get_coords(start, end)
self.window.rectangle(self.gc,
coords['start']['x'],
coords['start']['y'],
coords['end']['x'] - coords['start']['x'],
coords['end']['y'] - coords['start']['y']
)
class Screenshot(QWidget):
def __init__(self):
super(Screenshot, self).__init__()
self.screenshotLabel = QLabel()
self.screenshotLabel.setSizePolicy(QSizePolicy.Expanding,
QSizePolicy.Expanding)
self.screenshotLabel.setAlignment(QtCore.Qt.AlignCenter)
self.screenshotLabel.setMinimumSize(240, 160)
self.createOptionsGroupBox()
self.createButtonsLayout()
mainLayout = QVBoxLayout()
mainLayout.addWidget(self.screenshotLabel)
mainLayout.addWidget(self.optionsGroupBox)
mainLayout.addLayout(self.buttonsLayout)
self.setLayout(mainLayout)
self.area = None
self.shootScreen()
self.delaySpinBox.setValue(1)
self.setWindowTitle("Screenshot")
self.resize(300, 200)
def resizeEvent(self, event):
scaledSize = self.originalPixmap.size()
scaledSize.scale(self.screenshotLabel.size(), QtCore.Qt.KeepAspectRatio)
if not self.screenshotLabel.pixmap() or scaledSize != self.screenshotLabel.pixmap().size():
self.updateScreenshotLabel()
def selectArea(self):
self.hide()
xs = XSelect(display.Display())
self.area = xs.get_mouse_selection()
if self.area:
xo, yo, x, y = self.area
self.areaLabel.setText("Area: x%s y%s to x%s y%s" % (xo, yo, x, y))
self.shootScreen()
else:
self.areaLabel.setText("Area: fullscreen")
self.shootScreen()
self.show()
def newScreenshot(self):
if self.hideThisWindowCheckBox.isChecked():
self.hide()
self.newScreenshotButton.setDisabled(True)
QtCore.QTimer.singleShot(self.delaySpinBox.value() * 1000, self.shootScreen)
def saveScreenshot(self):
format = 'png'
initialPath = QtCore.QDir.currentPath() + "/untitled." + format
fileName, fileSuffixSelection = QFileDialog.getSaveFileName(
self,
"Save As",
initialPath,
"{} Files (*.{});;All Files (*)".format(format.upper(), format))
if fileName:
self.originalPixmap.save(str(fileName), format)
def copyToClipboard(self):
if not self.originalPixmap:
return
qi = self.originalPixmap.toImage()
QApplication.clipboard().setImage(qi)
def shootScreen(self):
if self.delaySpinBox.value() != 0:
QApplication.beep()
# Garbage collect any existing image first.
self.originalPixmap = None
screen = QApplication.primaryScreen()
self.originalPixmap = screen.grabWindow(QApplication.desktop().winId())
if self.area is not None:
qi = self.originalPixmap.toImage()
qi = qi.copy(int(self.area[0]), int(self.area[1]), int(self.area[2]), int(self.area[3]))
self.originalPixmap = None
self.originalPixmap = QPixmap.fromImage(qi)
self.updateScreenshotLabel()
self.newScreenshotButton.setDisabled(False)
if self.hideThisWindowCheckBox.isChecked():
self.show()
def updateCheckBox(self):
if self.delaySpinBox.value() == 0:
self.hideThisWindowCheckBox.setDisabled(True)
else:
self.hideThisWindowCheckBox.setDisabled(False)
def createOptionsGroupBox(self):
self.optionsGroupBox = QGroupBox("Options")
self.delaySpinBox = QSpinBox()
self.delaySpinBox.setSuffix(" s")
self.delaySpinBox.setMaximum(60)
self.delaySpinBox.valueChanged.connect(self.updateCheckBox)
self.delaySpinBoxLabel = QLabel("Screenshot Delay:")
self.hideThisWindowCheckBox = QCheckBox("Hide This Window")
self.hideThisWindowCheckBox.setChecked(True)
self.areaLabel = QLabel("Area: fullscreen")
optionsGroupBoxLayout = QGridLayout()
optionsGroupBoxLayout.addWidget(self.delaySpinBoxLabel, 0, 0)
optionsGroupBoxLayout.addWidget(self.delaySpinBox, 0, 1)
optionsGroupBoxLayout.addWidget(self.hideThisWindowCheckBox, 1, 0)
optionsGroupBoxLayout.addWidget(self.areaLabel, 1, 1)
self.optionsGroupBox.setLayout(optionsGroupBoxLayout)
def createButtonsLayout(self):
self.selectAreaButton = self.createButton(
"Select Area",
self.selectArea
)
self.newScreenshotButton = self.createButton(
"New Screenshot",
self.newScreenshot
)
self.copyScreenshotButton = self.createButton(
"Copy to Clipboard",
self.copyToClipboard
)
self.saveScreenshotButton = self.createButton(
"Save Screenshot",
self.saveScreenshot
)
self.quitScreenshotButton = self.createButton(
"Quit",
self.close
)
self.buttonsLayout = QHBoxLayout()
self.buttonsLayout.addStretch()
self.buttonsLayout.addWidget(self.selectAreaButton)
self.buttonsLayout.addWidget(self.newScreenshotButton)
self.buttonsLayout.addWidget(self.copyScreenshotButton)
self.buttonsLayout.addWidget(self.saveScreenshotButton)
self.buttonsLayout.addWidget(self.quitScreenshotButton)
def createButton(self, text, member):
button = QPushButton(text)
button.clicked.connect(member)
return button
def updateScreenshotLabel(self):
self.screenshotLabel.setPixmap(self.originalPixmap.scaled(
self.screenshotLabel.size(), QtCore.Qt.KeepAspectRatio,
QtCore.Qt.SmoothTransformation))
if __name__ == '__main__':
app = QApplication(sys.argv)
screenshot = Screenshot()
screenshot.show()
sys.exit(app.exec_())
@aspotton
Copy link
Author

Updates

  • No longer shells out in order to get the screen area selection from the user
  • Added a "copy to clipboard" button
  • Updated to use PyQt5 and Python 3

@SpongebobSquamirez
Copy link

What platforms does this work on? I saw a comment on the code you forked from suggesting it didn't work on Windows.

@aspotton
Copy link
Author

What platforms does this work on? I saw a comment on the code you forked from suggesting it didn't work on Windows.

AFAIK it doesn't work on Windows due to how it uses the X server to get the mouse position, but I haven't tried it on anything other than Linux/Ubuntu.

@lucguislain
Copy link

lucguislain commented Jan 8, 2023

Hi Adam,

I was looking for a piece of python code to take screenshots and I found yours.
When I saw the comment "If only I could get this working..." about changing the cursor when grabbing an area, I went looking for a solution and found one here.

After, I added these few lines of code instead of self.cursor = X.None.
from Xlib import X, display, Xcursorfont

and

    `# Create font cursor
    font = display.open_font('cursor')
    self.cursor = font.create_glyph_cursor(font, Xcursorfont.crosshair, Xcursorfont.crosshair+1, (65535, 65535, 65535), (0, 0, 0))`

then in get_mouse_selection function
self.window.grab_pointer(self.d, X.PointerMotionMask|X.ButtonReleaseMask|X.ButtonPressMask, X.GrabModeAsync, X.GrabModeAsync, X.NONE, self.cursor, X.CurrentTime)

I hope this will help someone.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment