From 9986b1c829caa12bcea120bb37ebb57ab5e0e874 Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 6 Jul 2017 12:40:03 -0400 Subject: image options to mirror & saturate colours and some friendly docstrings --- freeze.py | 1 + src/command.py | 5 ++++ src/components/image.py | 21 +++++++++++++++- src/components/image.ui | 64 ++++++++++++++++++++++++++++++++++++++++++++++++- src/core.py | 2 +- src/mainwindow.py | 9 ++++++- src/presetmanager.py | 4 ++++ src/preview_thread.py | 18 ++++++++++---- src/video_thread.py | 7 ++++++ 9 files changed, 122 insertions(+), 9 deletions(-) diff --git a/freeze.py b/freeze.py index a81f325..3266f45 100644 --- a/freeze.py +++ b/freeze.py @@ -33,6 +33,7 @@ buildOptions = dict( "PIL.Image", "PIL.ImageQt", "PIL.ImageDraw", + "PIL.ImageEnhance", ], include_files=deps, ) diff --git a/src/command.py b/src/command.py index ee0e48d..be194d8 100644 --- a/src/command.py +++ b/src/command.py @@ -1,3 +1,8 @@ +''' + When using commandline mode, this module's object handles interpreting + the arguments and giving them to Core, which tracks the main program state. + Then it immediately exports a video. +''' from PyQt5 import QtCore import argparse import os diff --git a/src/components/image.py b/src/components/image.py index 4ccfc80..c9da137 100644 --- a/src/components/image.py +++ b/src/components/image.py @@ -1,4 +1,4 @@ -from PIL import Image, ImageDraw +from PIL import Image, ImageDraw, ImageEnhance from PyQt5 import QtGui, QtCore, QtWidgets import os @@ -20,7 +20,9 @@ class Component(Component): page.pushButton_image.clicked.connect(self.pickImage) page.spinBox_scale.valueChanged.connect(self.update) page.spinBox_rotate.valueChanged.connect(self.update) + page.spinBox_color.valueChanged.connect(self.update) page.checkBox_stretch.stateChanged.connect(self.update) + page.checkBox_mirror.stateChanged.connect(self.update) page.spinBox_x.valueChanged.connect(self.update) page.spinBox_y.valueChanged.connect(self.update) @@ -31,9 +33,11 @@ class Component(Component): self.imagePath = self.page.lineEdit_image.text() self.scale = self.page.spinBox_scale.value() self.rotate = self.page.spinBox_rotate.value() + self.color = self.page.spinBox_color.value() self.xPosition = self.page.spinBox_x.value() self.yPosition = self.page.spinBox_y.value() self.stretched = self.page.checkBox_stretch.isChecked() + self.mirror = self.page.checkBox_mirror.isChecked() self.parent.drawPreview() super().update() @@ -56,33 +60,48 @@ class Component(Component): frame = BlankFrame(width, height) if self.imagePath and os.path.exists(self.imagePath): image = Image.open(self.imagePath) + + # Modify image's appearance + if self.color != 100: + image = ImageEnhance.Color(image).enhance( + float(self.color / 100) + ) + if self.mirror: + image = image.transpose(Image.FLIP_LEFT_RIGHT) if self.stretched and image.size != (width, height): image = image.resize((width, height), Image.ANTIALIAS) if self.scale != 100: newHeight = int((image.height / 100) * self.scale) newWidth = int((image.width / 100) * self.scale) image = image.resize((newWidth, newHeight), Image.ANTIALIAS) + + # Paste image at correct position frame.paste(image, box=(self.xPosition, self.yPosition)) if self.rotate != 0: frame = frame.rotate(self.rotate) + return frame def loadPreset(self, pr, presetName=None): super().loadPreset(pr, presetName) self.page.lineEdit_image.setText(pr['image']) self.page.spinBox_scale.setValue(pr['scale']) + self.page.spinBox_color.setValue(pr['color']) self.page.spinBox_rotate.setValue(pr['rotate']) self.page.spinBox_x.setValue(pr['x']) self.page.spinBox_y.setValue(pr['y']) self.page.checkBox_stretch.setChecked(pr['stretched']) + self.page.checkBox_mirror.setChecked(pr['mirror']) def savePreset(self): return { 'preset': self.currentPreset, 'image': self.imagePath, 'scale': self.scale, + 'color': self.color, 'rotate': self.rotate, 'stretched': self.stretched, + 'mirror': self.mirror, 'x': self.xPosition, 'y': self.yPosition, } diff --git a/src/components/image.ui b/src/components/image.ui index 33488f8..e549ed0 100644 --- a/src/components/image.ui +++ b/src/components/image.ui @@ -208,10 +208,17 @@ + + + + Mirror + + + - Rotation + Rotate Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter @@ -290,6 +297,61 @@ + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + Color + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + QAbstractSpinBox::UpDownArrows + + + % + + + 0 + + + 999 + + + 1 + + + 100 + + + + + diff --git a/src/core.py b/src/core.py index 9ea9666..5623039 100644 --- a/src/core.py +++ b/src/core.py @@ -1,5 +1,5 @@ ''' - Home to the Core class which tracks the program state + Home to the Core class which tracks program state. Used by GUI & commandline ''' import sys import os diff --git a/src/mainwindow.py b/src/mainwindow.py index e8a3221..1c6bbc4 100644 --- a/src/mainwindow.py +++ b/src/mainwindow.py @@ -1,3 +1,9 @@ +''' + When using GUI mode, this module's object (the main window) takes + user input to construct a program state (stored in the Core object). + This shows a preview of the video being created and allows for saving + projects and exporting the video at a later time. +''' from PyQt5 import QtCore, QtGui, uic, QtWidgets from PyQt5.QtWidgets import QMenu, QShortcut from queue import Queue @@ -79,6 +85,7 @@ class MainWindow(QtWidgets.QMainWindow): self.previewWorker = preview_thread.Worker(self, self.previewQueue) self.previewWorker.moveToThread(self.previewThread) self.previewWorker.imageCreated.connect(self.showPreviewImage) + self.previewWorker.error.connect(self.cleanUp) self.previewThread.start() self.timer = QtCore.QTimer(self) @@ -296,11 +303,11 @@ class MainWindow(QtWidgets.QMainWindow): QtWidgets.QShortcut("Ctrl+End", self.window, self.moveComponentBottom) QtWidgets.QShortcut("Ctrl+r", self.window, self.removeComponent) + @QtCore.pyqtSlot() def cleanUp(self): self.timer.stop() self.previewThread.quit() self.previewThread.wait() - self.autosave() def updateWindowTitle(self): appName = 'Audio Visualizer' diff --git a/src/presetmanager.py b/src/presetmanager.py index 805b93e..40aa73f 100644 --- a/src/presetmanager.py +++ b/src/presetmanager.py @@ -1,3 +1,7 @@ +''' + Preset manager object handles all interactions with presets, including + the context menu accessed from MainWindow. +''' from PyQt5 import QtCore, QtWidgets import string import os diff --git a/src/preview_thread.py b/src/preview_thread.py index e58f04e..afb5e50 100644 --- a/src/preview_thread.py +++ b/src/preview_thread.py @@ -1,3 +1,7 @@ +''' + Thread that runs to create QImages for MainWindow's preview label. + Processes a queue of component lists. +''' from PyQt5 import QtCore, QtGui, uic from PyQt5.QtCore import pyqtSignal, pyqtSlot from PIL import Image @@ -11,6 +15,7 @@ from copy import copy class Worker(QtCore.QObject): imageCreated = pyqtSignal(['QImage']) + error = pyqtSignal() def __init__(self, parent=None, queue=None): QtCore.QObject.__init__(self) @@ -59,12 +64,15 @@ class Worker(QtCore.QObject): "This is a fatal error." % str(component), detail=str(e), - icon='Warning' + icon='Warning', + parent=None # mainwindow is in a different thread ) - quit(1) - - self._image = ImageQt(frame) - self.imageCreated.emit(QtGui.QImage(self._image)) + from frame import BlankFrame + self.imageCreated.emit(ImageQt(BlankFrame)) + self.error.emit() + break + else: + self.imageCreated.emit(ImageQt(frame)) except Empty: True diff --git a/src/video_thread.py b/src/video_thread.py index aed4d60..d35a37a 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -1,3 +1,10 @@ +''' + Thread created to export a video. It has a slot to begin export using + an input file, output path, and component list. During export multiple + threads are created to render the video as quickly as possible. Signals + are emitted to update MainWindow's progress bar, detail text, and preview. + Export can be cancelled with cancel() + reset() +''' from PyQt5 import QtCore, QtGui, uic from PyQt5.QtCore import pyqtSignal, pyqtSlot from PIL import Image, ImageDraw, ImageFont -- cgit v1.2.3