diff --git a/.gitignore b/.gitignore index 27cd58f..dd6195d 100644 --- a/.gitignore +++ b/.gitignore @@ -108,7 +108,7 @@ celerybeat.pid .env .venv env/ -venv/ +venv*/ ENV/ env.bak/ venv.bak/ @@ -145,6 +145,7 @@ cython_debug/ *.cbz *.pdf +settings.json *.bat .idea/ diff --git a/mangle.pyw b/mangle.pyw index 9bc73ae..69a4e8c 100755 --- a/mangle.pyw +++ b/mangle.pyw @@ -15,15 +15,25 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +## TODO # Port to Python3, PyQt5 # TODO ## +# X run 2to3 +# X change imports to PyQt5 and update PyQt method calls +# X test main functionality: +# X build comic with images and generate all formats for at least 2 types of Kindle +# X test load/save .mngl files and other menu bar buttons. +# - remove unused imports +# - remove print/ debug +# - test all functions import sys -from PyQt4 import QtGui +from PyQt5 import QtGui, QtCore, uic, QtWidgets +from PyQt5.QtWidgets import QMainWindow, QApplication +from PyQt5.QtCore import * from mangle.book import MainWindowBook - -application = QtGui.QApplication(sys.argv) +application = QtWidgets.QApplication(sys.argv) filename = sys.argv[1] if len(sys.argv) > 1 else None window = MainWindowBook(filename) window.show() diff --git a/mangle/about.py b/mangle/about.py index 1cb0b77..cac74ca 100644 --- a/mangle/about.py +++ b/mangle/about.py @@ -14,12 +14,13 @@ # along with this program. If not, see . -from PyQt4 import QtGui, uic +#from PyQt4 import QtGui, uic +from PyQt5 import QtWidgets, uic +# from PyQt5.QtCore import * -import util +from . import util - -class DialogAbout(QtGui.QDialog): +class DialogAbout(QtWidgets.QDialog): def __init__(self, parent): - QtGui.QDialog.__init__(self, parent) + QtWidgets.QDialog.__init__(self, parent) uic.loadUi(util.buildResPath('mangle/ui/about.ui'), self) diff --git a/mangle/book.py b/mangle/book.py index e4d545a..953710d 100644 --- a/mangle/book.py +++ b/mangle/book.py @@ -19,28 +19,33 @@ import tempfile from zipfile import ZipFile -from PyQt4 import QtGui, QtCore, QtXml, uic +# from PyQt4 import QtGui, QtCore, QtXml, uic +from PyQt5 import QtGui, QtCore, uic, QtXml, QtWidgets +from PyQt5.QtCore import * -from about import DialogAbout -from convert import DialogConvert -from image import ImageFlags -from options import DialogOptions -import util +from .about import DialogAbout +from .convert import DialogConvert +from .image import ImageFlags +from .options import DialogOptions +from . import util -# Sort function use to sort files in a natural order, by lowering -# characters, and manage multi levels of integers (tome 1/ page 1.jpg, etc etc) +# Sort function used to sort files in a natural order, by lowering +# characters, and managing multi levels of integers (tome 1/ page 1.jpg, etc etc) # cf: See http://www.codinghorror.com/blog/archives/001018.html def natural_key(string_): l = [] - for s in re.split(r'(\d+)', string_): - # QString do not have isdigit, so convert it if need - if isinstance(s, QtCore.QString): - s = unicode(s) - if s.isdigit(): - l.append(int(s)) - else: - l.append(s.lower()) + print(f"string_ => {string_}") + print(f"type(string_) => {type(string_)}") + if string_: + for s in re.split(r'(\d+)', string_): + # str does not have isdigit, so convert it if needed + if isinstance(s, str): + s = str(s) + if s.isdigit(): + l.append(int(s)) + else: + l.append(s.lower()) return l @@ -80,14 +85,18 @@ def save(self, filename): root.appendChild(itemImg) itemImg.setAttribute('filename', filenameImg) - textXml = document.toString(4).toUtf8() + textXml = document.toString(4) + + if len(filename) == 2: + filename = filename[0] # filename received is a tuple: get the first item try: - fileXml = open(unicode(filename), 'w') + fileXml = open(str(filename), 'w') fileXml.write(textXml) fileXml.close() - except IOError: - raise RuntimeError('Cannot create book file %s' % filename) + except IOError as ioe: + print(f"IOError caught =>\n{ioe}") + raise RuntimeError(f'Can\'t save file {filename}') self.filename = filename self.modified = False @@ -95,20 +104,20 @@ def save(self, filename): def load(self, filename): try: - fileXml = open(unicode(filename), 'r') + fileXml = open(str(filename), 'r') textXml = fileXml.read() fileXml.close() except IOError: - raise RuntimeError('Cannot open book file %s' % filename) + raise RuntimeError(f'Can\'t open Mangle file {filename}') document = QtXml.QDomDocument() - if not document.setContent(QtCore.QString.fromUtf8(textXml)): - raise RuntimeError('Error parsing book file %s' % filename) + if not document.setContent(textXml): + raise RuntimeError(f'Error parsing Mangle file {filename}') root = document.documentElement() if root.tagName() != 'book': - raise RuntimeError('Unexpected book format in file %s' % filename) + raise RuntimeError(f'Unexpected Mangle format in file {filename}') self.title = root.attribute('title', 'Untitled') self.overwrite = root.attribute('overwrite', 'true' if Book.DefaultOverwrite else 'false') == 'true' @@ -123,15 +132,15 @@ def load(self, filename): if items is None: return - for i in xrange(0, len(items)): + for i in range(0, len(items)): item = items.at(i).toElement() if item.hasAttribute('filename'): self.images.append(item.attribute('filename')) -class MainWindowBook(QtGui.QMainWindow): +class MainWindowBook(QtWidgets.QMainWindow): def __init__(self, filename=None): - QtGui.QMainWindow.__init__(self) + QtWidgets.QMainWindow.__init__(self) uic.loadUi(util.buildResPath('mangle/ui/book.ui'), self) self.listWidgetFiles.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) @@ -174,7 +183,7 @@ def dropEvent(self, event): filename = url.toLocalFile() if self.isImageFile(filename): filenames.append(filename) - elif os.path.isdir(unicode(filename)): + elif os.path.isdir(str(filename)): directories.append(filename) self.addImageDirs(directories) @@ -191,12 +200,14 @@ def onFileOpen(self): if not self.saveIfNeeded(): return - filename = QtGui.QFileDialog.getOpenFileName( + filename = QtWidgets.QFileDialog.getOpenFileName( parent=self, caption='Select a book file to open', filter='Mangle files (*.mngl);;All files (*.*)' ) - if not filename.isNull(): + filename = filename[0] # first item is filename (second is filter) + + if filename: self.loadBook(self.cleanupBookFile(filename)) @@ -209,7 +220,7 @@ def onFileSaveAs(self): def onFilesContextMenu(self, point): - menu = QtGui.QMenu(self) + menu = QtWidgets.QMenu(self) menu.addAction(self.menu_Add.menuAction()) if len(self.listWidgetFiles.selectedItems()) > 0: @@ -225,11 +236,13 @@ def onFilesDoubleClick(self, item): def onBookAddFiles(self): - filenames = QtGui.QFileDialog.getOpenFileNames( + filenames = QtWidgets.QFileDialog.getOpenFileNames( parent=self, caption='Select image file(s) to add', filter='Image files (*.jpeg *.jpg *.gif *.png);;Comic files (*.cbz)' ) + filenames = filenames[0] # get first tuple item (second is file filter) + if(self.containsCbzFile(filenames)): self.addCBZFiles(filenames) else: @@ -237,9 +250,9 @@ def onBookAddFiles(self): def onBookAddDirectory(self): - directory = QtGui.QFileDialog.getExistingDirectory(self, 'Select an image directory to add') - if not directory.isNull(): - self.book.title = os.path.basename(os.path.normpath(unicode(directory))) + directory = QtWidgets.QFileDialog.getExistingDirectory(self, 'Select an image directory to add') + if directory: + self.book.title = os.path.basename(os.path.normpath(str(directory))) self.addImageDirs([directory]) @@ -257,24 +270,24 @@ def onBookRemove(self): def onBookOptions(self): dialog = DialogOptions(self, self.book) - if dialog.exec_() == QtGui.QDialog.Accepted: + if dialog.exec_() == QtWidgets.QDialog.Accepted: self.book.titleSet = True def onBookExport(self): if len(self.book.images) == 0: - QtGui.QMessageBox.warning(self, 'Mangle', 'This book has no images to export') + QtWidgets.QMessageBox.warning(self, 'Mangle', 'This book has no images to export') return if not self.book.titleSet: # if self.book.title is None: dialog = DialogOptions(self, self.book) - if dialog.exec_() == QtGui.QDialog.Rejected: + if dialog.exec_() == QtWidgets.QDialog.Rejected: return else: self.book.titleSet = True - directory = QtGui.QFileDialog.getExistingDirectory(self, 'Select a directory to export book to') - if not directory.isNull(): + directory = QtWidgets.QFileDialog.getExistingDirectory(self, 'Select a directory to export book to') + if directory: dialog = DialogConvert(self, self.book, directory) dialog.exec_() @@ -293,40 +306,40 @@ def saveIfNeeded(self): if not self.book.modified: return True - result = QtGui.QMessageBox.question( + result = QtWidgets.QMessageBox.question( self, 'Mangle', 'Save changes to the current book?', - QtGui.QMessageBox.Yes | QtGui.QMessageBox.No | QtGui.QMessageBox.Cancel, - QtGui.QMessageBox.Yes + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, + QtWidgets.QMessageBox.Yes ) return ( - result == QtGui.QMessageBox.No or - result == QtGui.QMessageBox.Yes and self.saveBook() + result == QtWidgets.QMessageBox.No or + result == QtWidgets.QMessageBox.Yes and self.saveBook() ) def saveBook(self, browse=False): if self.book.title is None: - QtGui.QMessageBox.warning(self, 'Mangle', 'You must specify a title for this book before saving') + QtWidgets.QMessageBox.warning(self, 'Mangle', 'You must specify a title for this book before saving') return False filename = self.book.filename if filename is None or browse: - filename = QtGui.QFileDialog.getSaveFileName( + filename = QtWidgets.QFileDialog.getSaveFileName( parent=self, caption='Select a book file to save as', filter='Mangle files (*.mngl);;All files (*.*)' ) - if filename.isNull(): + if not filename: return False filename = self.cleanupBookFile(filename) try: self.book.save(filename) - except RuntimeError, error: - QtGui.QMessageBox.critical(self, 'Mangle', str(error)) + except RuntimeError as error: + QtWidgets.QMessageBox.critical(self, 'Mangle', str(error)) return False return True @@ -335,8 +348,8 @@ def saveBook(self, browse=False): def loadBook(self, filename): try: self.book.load(filename) - except RuntimeError, error: - QtGui.QMessageBox.critical(self, 'Mangle', str(error)) + except RuntimeError as error: + QtWidgets.QMessageBox.critical(self, 'Mangle', str(error)) else: self.listWidgetFiles.clear() for image in self.book.images: @@ -380,13 +393,15 @@ def removeImageFiles(self): def addImageFiles(self, filenames): filenamesListed = [] - for i in xrange(0, self.listWidgetFiles.count()): + for i in range(0, self.listWidgetFiles.count()): filenamesListed.append(self.listWidgetFiles.item(i).text()) - + # Get files but in a natural sorted order - for filename in sorted(filenames, key=natural_key): + sorted_filenames = sorted(filenames, key=natural_key) + + for filename in sorted_filenames: if filename not in filenamesListed: - filename = QtCore.QString(filename) + filename = str(filename) self.listWidgetFiles.addItem(filename) self.book.images.append(filename) self.book.modified = True @@ -396,12 +411,12 @@ def addImageDirs(self, directories): filenames = [] for directory in directories: - for root, _, subfiles in os.walk(unicode(directory)): + for root, _, subfiles in os.walk(str(directory)): for filename in subfiles: path = os.path.join(root, filename) if self.isImageFile(path): filenames.append(path) - + self.addImageFiles(filenames) @@ -411,7 +426,7 @@ def addCBZFiles(self, filenames): filenames.sort() filenamesListed = [] - for i in xrange(0, self.listWidgetFiles.count()): + for i in range(0, self.listWidgetFiles.count()): filenamesListed.append(self.listWidgetFiles.item(i).text()) for filename in filenames: @@ -426,7 +441,7 @@ def addCBZFiles(self, filenames): pass # the dir exists so we are going to extract the images only. else: cbzFile.extract(f, path) - if os.path.isdir(unicode(path)): # Add the directories + if os.path.isdir(str(path)): # Add the directories directories.append(path) self.addImageDirs(directories) # Add the files @@ -434,7 +449,7 @@ def addCBZFiles(self, filenames): def isImageFile(self, filename): imageExts = ['.jpeg', '.jpg', '.gif', '.png'] - filename = unicode(filename) + filename = str(filename) return ( os.path.isfile(filename) and os.path.splitext(filename)[1].lower() in imageExts @@ -443,7 +458,7 @@ def isImageFile(self, filename): def containsCbzFile(self, filenames): cbzExts = ['.cbz'] for filename in filenames: - filename = unicode(filename) + filename = str(filename) result = ( os.path.isfile(filename) and os.path.splitext(filename)[1].lower() in cbzExts @@ -453,6 +468,6 @@ def containsCbzFile(self, filenames): return False def cleanupBookFile(self, filename): - if len(os.path.splitext(unicode(filename))[1]) == 0: + if len(os.path.splitext(str(filename))[1]) == 0: filename += '.mngl' return filename diff --git a/mangle/convert.py b/mangle/convert.py index 8c9c92e..6a97a8e 100644 --- a/mangle/convert.py +++ b/mangle/convert.py @@ -16,21 +16,23 @@ import os import shutil -from PyQt4 import QtGui, QtCore +#from PyQt4 import QtGui, QtCore +from PyQt5 import QtGui, QtCore, QtWidgets +from PyQt5.QtCore import * -from image import ImageFlags -import cbz -import image -import pdfimage +from .image import ImageFlags +from . import cbz +from . import image +from . import pdfimage -class DialogConvert(QtGui.QProgressDialog): +class DialogConvert(QtWidgets.QProgressDialog): def __init__(self, parent, book, directory): - QtGui.QProgressDialog.__init__(self) + QtWidgets.QProgressDialog.__init__(self) self.book = book - self.bookPath = os.path.join(unicode(directory), unicode(self.book.title)) + self.bookPath = os.path.join(str(directory), str(self.book.title)) self.timer = None self.setWindowTitle('Exporting book...') @@ -82,19 +84,19 @@ def onTimer(self): index = self.value() pages_split = self.increment target = os.path.join(self.bookPath, '%05d.png' % (index + pages_split)) - source = unicode(self.book.images[index]) + source = str(self.book.images[index]) if index == 0: try: if not os.path.isdir(self.bookPath): os.makedirs(self.bookPath) except OSError: - QtGui.QMessageBox.critical(self, 'Mangle', 'Cannot create directory %s' % self.bookPath) + QtWidgets.QMessageBox.critical(self, 'Mangle', 'Cannot create directory %s' % self.bookPath) self.close() return try: - base = os.path.join(self.bookPath, unicode(self.book.title)) + base = os.path.join(self.bookPath, str(self.book.title)) mangaName = base + '.manga' if self.book.overwrite or not os.path.isfile(mangaName): @@ -105,12 +107,12 @@ def onTimer(self): mangaSaveName = base + '.manga_save' if self.book.overwrite or not os.path.isfile(mangaSaveName): mangaSave = open(base + '.manga_save', 'w') - saveData = u'LAST=/mnt/us/pictures/%s/%s' % (self.book.title, os.path.split(target)[1]) - mangaSave.write(saveData.encode('utf-8')) + saveData = 'LAST=/mnt/us/pictures/%s/%s' % (self.book.title, os.path.split(target)[1]) + mangaSave.write(saveData) mangaSave.close() except IOError: - QtGui.QMessageBox.critical(self, 'Mangle', 'Cannot write manga file(s) to directory %s' % self.bookPath) + QtWidgets.QMessageBox.critical(self, 'Mangle', 'Cannot write manga file(s) to directory %s' % self.bookPath) self.close() return False @@ -155,15 +157,15 @@ def onTimer(self): # Convert page self.convertAndSave(source, target, device, flags, archive, pdf) - except RuntimeError, error: - result = QtGui.QMessageBox.critical( + except RuntimeError as error: + result = QtWidgets.QMessageBox.critical( self, 'Mangle', str(error), - QtGui.QMessageBox.Abort | QtGui.QMessageBox.Ignore, - QtGui.QMessageBox.Ignore + QtWidgets.QMessageBox.Abort | QtWidgets.QMessageBox.Ignore, + QtWidgets.QMessageBox.Ignore ) - if result == QtGui.QMessageBox.Abort: + if result == QtWidgets.QMessageBox.Abort: self.close() return diff --git a/mangle/image.py b/mangle/image.py index 2fe18b4..7977e7b 100644 --- a/mangle/image.py +++ b/mangle/image.py @@ -105,9 +105,9 @@ def func_wrapper(*args, **kwargs): return args[0] return func_wrapper - -@protect_bad_image + +@protect_bad_image def splitLeft(image): widthImg, heightImg = image.size @@ -125,7 +125,7 @@ def splitRight(image): def quantizeImage(image, palette): colors = len(palette) / 3 if colors < 256: - palette = palette + palette[:3] * (256 - colors) + palette = palette + palette[:3] * int(256 - colors) palImg = Image.new('P', (1, 1)) palImg.putpalette(palette) @@ -226,7 +226,7 @@ def loadImage(source): return Image.open(source) except IOError: raise RuntimeError('Cannot read image file %s' % source) - + def saveImage(image, target): try: @@ -243,7 +243,7 @@ def isSplitable(source): try: widthImg, heightImg = image.size return widthImg > heightImg - except IOError: + except IOError: raise RuntimeError('Cannot read image file %s' % source) @@ -281,5 +281,5 @@ def convertImage(source, target, device, flags): image = frameImage(image, tuple(palette[:3]), tuple(palette[-3:]), size) if flags & ImageFlags.Quantize: image = quantizeImage(image, palette) - + saveImage(image, target) diff --git a/mangle/options.py b/mangle/options.py index 8829463..2b06506 100644 --- a/mangle/options.py +++ b/mangle/options.py @@ -14,15 +14,16 @@ # along with this program. If not, see . -from PyQt4 import QtGui, uic +#from PyQt4 import QtGui, uic +from PyQt5 import QtGui, uic, QtWidgets -from image import ImageFlags -import util +from .image import ImageFlags +from . import util -class DialogOptions(QtGui.QDialog): +class DialogOptions(QtWidgets.QDialog): def __init__(self, parent, book): - QtGui.QDialog.__init__(self, parent) + QtWidgets.QDialog.__init__(self, parent) uic.loadUi(util.buildResPath('mangle/ui/options.ui'), self) self.accepted.connect(self.onAccept) diff --git a/mangle/pdfimage.py b/mangle/pdfimage.py index 908be05..3f9b157 100644 --- a/mangle/pdfimage.py +++ b/mangle/pdfimage.py @@ -18,7 +18,7 @@ from reportlab.pdfgen import canvas -from image import KindleData +from .image import KindleData class PDFImage(object): diff --git a/requirements.txt b/requirements.txt index 7a2c36e..ef571f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ -py2exe-py2==0.6.9; platform_system=='Windows' -PyQt4 @ https://download.lfd.uci.edu/pythonlibs/w4tscw6k/cp27/PyQt4-4.11.4-cp27-cp27m-win32.whl; platform_system=='Windows' -reportlab==3.5.59 +PyQt5==5.15.4 +reportlab==3.5.68