Drop Batch - dostosowywanie plików do czytnika ebooków *.cbz

1

Skleciłem sobie małą apkę wspomagającą konwersję obrazków tak, aby się nadawały do wrzucenia w plikach *.cbz do czytnika ebooków.

screenshot-20221209231851.png

Czasem obrazki, z którymi mam do czynienia mają duży rozmiar. Np. wysokość może wynieść 3000px.
Rozdzielczość mojego czytnika to 1440x1920, więc dobrze jest zmniejszyć obrazki, żeby ładowały się szybciej i przy okazji zajmowały mniej miejsca.
Nie potrzebujemy większych obrazków, jeśli nie zamierzamy robić zooma podczas czytania.

Nazwy plików bywają takie: 1_1.jpg, 1_2.jpg, ..., 1_9.jpg, 1_10.jpg, 1_11.jpg.
A to się źle sortuje, jeśli soft nie sortuje plików według wartości liczbowych. Wtedy np. 1_10.jpg jest po 1_1.jpg, zamiast po 1_9.jpg.
Windows 10 dobrze sortuje takie pliki, ale mój czytnik ebooków nie, dlatego zmieniam wszystkie liczby w nazwach plików na "stałą szerokość":
000001_000001.jpg, 000001_000002.jpg, ..., 000001_000009.jpg, 000001_000010.jpg, 000001_000011.jpg

Przez jakiś czas po prostu w każdym folderze uruchamiałem skrypty, które wykonują operacje dostosowujące obrazki dla czytnika ebooków.
Teraz w końcu zebrałem te skrypty w jedną apkę z GUI. Wystarczy przeciągać pliki i katalogi na okienko aplikacji, a ona według zakodowanych kryteriów skonwertuje pliki.
Nie trzeba czekać aż wszystkie pliki z jednego dropa zostaną przetworzone. Można na to samo okienko przeciągać kolejne pliki/foldery jeden za drugim.

Sam szkielet aplikacji może się komuś przydać. Wystarczy podmienić operacje, które będą wykonywane na liście dropniętych plików i już macie apkę dostosowaną do Waszych potrzeb :]

Kod:

# Requires Python 3.5+, PyQt5 and ImageMagick installed!
# pip install PyQt5
# https://imagemagick.org/script/download.php
# ImageMagick should be present in system PATH

import sys, os, re, glob

from PyQt5.QtWidgets import (
	QApplication,
	QMainWindow,
	QLabel,
	QCheckBox,
)

from PyQt5.QtCore import (
	Qt,
	QUrl,
	QRunnable,
	QThreadPool,
)

class Runnable(QRunnable):
	def __init__(self, links, mogrifyChecked, renameChecked, statusLabel):
		super().__init__()
		self.links = links
		self.mogrifyChecked = mogrifyChecked
		self.renameChecked = renameChecked
		self.statusLabel = statusLabel

	def run(self):
		self.unpack_dirs()
		self.process_files()
	
	def unpack_dirs(self):
		result = []
		
		for link in self.links:
			if os.path.isdir(link):
				for imagePath in glob.iglob(os.path.join(link, '**/*.jpg'), recursive=True):
					result.append(imagePath)
				for imagePath in glob.iglob(os.path.join(link, '**/*.png'), recursive=True):
					result.append(imagePath)
				for imagePath in glob.iglob(os.path.join(link, '**/*.jpeg'), recursive=True):
					result.append(imagePath)
			else:
				_, ext = os.path.splitext(link)
				
				if ext.lower() in (".jpg", ".png", ".jpeg"):
					result.append(link)
		
		self.links = result
	
	def process_files(self):
		links_count = len(self.links)
		
		for i, link in enumerate(self.links):
			self.statusLabel.setText("%04d/%04d"%(i + 1, links_count,))
			
			if self.mogrifyChecked:
				cmd = 'magick mogrify -resize "1800x1800>" -colorspace gray "%s"'%(link,)
				os.system(cmd)
			if self.renameChecked:
				os.rename(link, self.get_rename_filename(link))
	
	def get_rename_filename(self, current_path):
		file_path, file_name = os.path.split(current_path)
		
		file_name = re.sub('\d+', self.process_filename_number, file_name)
		
		return os.path.join(file_path, file_name)
	
	def process_filename_number(self, number_re_match):
		return "%06d"%(int(number_re_match.group(0)),)
		
class DropBatch(QMainWindow):
	def __init__(self):
		super().__init__()
		self.setWindowTitle("Drop Batch")
		self.setWindowFlags(Qt.MSWindowsFixedSizeDialogHint | Qt.WindowStaysOnTopHint)
		self.setStyleSheet("background-color: darkblue;")
		
		self.setAcceptDrops(True)
		#self.resize(350, 200)
		self.setGeometry(20, 50, 350, 200)
		
		self.statusLabel = QLabel("Drag files and folders here!", self)
		self.statusLabel.setStyleSheet("color: white;")
		self.statusLabel.setGeometry(50, 20, 250, 30)
		
		self.mogrifyCheckbox = QCheckBox("Mogrify", self)
		self.mogrifyCheckbox.setStyleSheet("color: white;")
		self.mogrifyCheckbox.setGeometry(50, 100, 250, 25)
		self.mogrifyCheckbox.setChecked(True)
		
		self.renameCheckbox = QCheckBox("Rename", self)
		self.renameCheckbox.setStyleSheet("color: white;")
		self.renameCheckbox.setGeometry(50, 130, 250, 25)
		self.renameCheckbox.setChecked(True)
		
	def dragEnterEvent(self, event):
		if event.mimeData().hasUrls():
			event.accept()
		else:
			event.ignore()
	
	def dragMoveEvent(self, event):
		if event.mimeData().hasUrls():
			event.setDropAction(Qt.CopyAction)
			event.accept()
		else:
			event.ignore()
			
	def dropEvent(self, event):
		if event.mimeData().hasUrls():
			event.setDropAction(Qt.CopyAction)
			event.accept()
			
			links = []
			
			for url in event.mimeData().urls():
				if url.isLocalFile():
					links.append(str(url.toLocalFile()))
				else:
					pass
					#links.append(str(url.toString()))
			
			mogrifyChecked = self.mogrifyCheckbox.isChecked() == True
			renameChecked = self.renameCheckbox.isChecked() == True
			
			pool = QThreadPool.globalInstance()
			runnable = Runnable(links, mogrifyChecked, renameChecked, self.statusLabel)
			pool.start(runnable)
		else:
			event.ignore()

app = QApplication(sys.argv)

window = DropBatch()
window.show()

sys.exit(app.exec_())

1

cmd = 'magick mogrify -resize "1800x1800>" -colorspace gray "%s"'%(link,)

nie ma jakiejś libki do pythona do obrazków, aby nie trzeba bylo zewnetrznego softu miec?

0

@1a2b3c4d5e: Jest: https://pillow.readthedocs.io/en/stable/
Ale raczej na 100% musiałbym więcej kodu napisać :]

1

@Spine:

jako że to dział recenzji, to moje zdanie jest takie:

ty pewnie dopisałbyś kilka linijek kodu - no bo ile resize może potrzebować, a userzy mieliby 10 razy prościej

0

Sorki, leń się we mnie odezwał :/
Masz pełną rację.

Jest jeszcze lepsze rozwiązanie niż Pillow...
Skoro już używam Qt, to mogę użyć czegoś do obrazków z Qt ;)

1

Update: uniezależnienie skryptu od ImageMagick.

screenshot-20221210005608.png

# Requires Python 3.5+ and PyQt5 installed!
# pip install PyQt5

import sys, os, re, glob

from PyQt5.QtWidgets import (
	QApplication,
	QMainWindow,
	QLabel,
	QCheckBox,
)

from PyQt5.QtCore import (
	Qt,
	QUrl,
	QRunnable,
	QThreadPool,
)

from PyQt5.QtGui import (
	QImage,
)

class Runnable(QRunnable):
	def __init__(self, links, resizeChecked, renameChecked, statusLabel):
		super().__init__()
		self.links = links
		self.resizeChecked = resizeChecked
		self.renameChecked = renameChecked
		self.statusLabel = statusLabel

	def run(self):
		self.unpack_dirs()
		self.process_files()
	
	def unpack_dirs(self):
		result = []
		
		for link in self.links:
			if os.path.isdir(link):
				for imagePath in glob.iglob(os.path.join(link, '**/*.jpg'), recursive=True):
					result.append(imagePath)
				for imagePath in glob.iglob(os.path.join(link, '**/*.png'), recursive=True):
					result.append(imagePath)
				for imagePath in glob.iglob(os.path.join(link, '**/*.jpeg'), recursive=True):
					result.append(imagePath)
			else:
				_, ext = os.path.splitext(link)
				
				if ext.lower() in (".jpg", ".png", ".jpeg"):
					result.append(link)
		
		self.links = result
	
	def process_files(self):
		links_count = len(self.links)
		
		for i, link in enumerate(self.links):
			self.statusLabel.setText("%04d/%04d"%(i + 1, links_count,))
			
			if self.resizeChecked:
				self.resize_image(link)
			if self.renameChecked:
				os.rename(link, self.get_rename_filename(link))
	
	def resize_image(self, imagePath):
		img = QImage(imagePath)
		img = img.scaled(1800, 1800, Qt.KeepAspectRatio, Qt.SmoothTransformation)
		img = img.convertToFormat(QImage.Format_Grayscale8)
		#img = img.convertToFormat(QImage.Format_Grayscale16)
		img.save(imagePath, quality = 88)
	
	def get_rename_filename(self, current_path):
		file_path, file_name = os.path.split(current_path)
		
		file_name = re.sub('\d+', self.process_filename_number, file_name)
		
		return os.path.join(file_path, file_name)
	
	def process_filename_number(self, number_re_match):
		return "%06d"%(int(number_re_match.group(0)),)
		
class DropBatch(QMainWindow):
	def __init__(self):
		super().__init__()
		self.setWindowTitle("Drop Batch")
		self.setWindowFlags(Qt.MSWindowsFixedSizeDialogHint | Qt.WindowStaysOnTopHint)
		self.setStyleSheet("background-color: darkblue;")
		
		self.setAcceptDrops(True)
		#self.resize(350, 200)
		self.setGeometry(20, 50, 350, 200)
		
		self.statusLabel = QLabel("Drag files and folders here!", self)
		self.statusLabel.setStyleSheet("color: white;")
		self.statusLabel.setGeometry(50, 20, 250, 30)
		
		self.resizeCheckbox = QCheckBox("Resize", self)
		self.resizeCheckbox.setStyleSheet("color: white;")
		self.resizeCheckbox.setGeometry(50, 100, 250, 25)
		self.resizeCheckbox.setChecked(True)
		
		self.renameCheckbox = QCheckBox("Rename", self)
		self.renameCheckbox.setStyleSheet("color: white;")
		self.renameCheckbox.setGeometry(50, 130, 250, 25)
		self.renameCheckbox.setChecked(True)
		
	def dragEnterEvent(self, event):
		if event.mimeData().hasUrls():
			event.accept()
		else:
			event.ignore()
	
	def dragMoveEvent(self, event):
		if event.mimeData().hasUrls():
			event.setDropAction(Qt.CopyAction)
			event.accept()
		else:
			event.ignore()
			
	def dropEvent(self, event):
		if event.mimeData().hasUrls():
			event.setDropAction(Qt.CopyAction)
			event.accept()
			
			links = []
			
			for url in event.mimeData().urls():
				if url.isLocalFile():
					links.append(str(url.toLocalFile()))
				else:
					pass
					#links.append(str(url.toString()))
			
			resizeChecked = self.resizeCheckbox.isChecked() == True
			renameChecked = self.renameCheckbox.isChecked() == True
			
			pool = QThreadPool.globalInstance()
			runnable = Runnable(links, resizeChecked, renameChecked, self.statusLabel)
			pool.start(runnable)
		else:
			event.ignore()

app = QApplication(sys.argv)

window = DropBatch()
window.show()

sys.exit(app.exec_())

0

Update: wrzuciłem projekt na GitHuba => https://github.com/JasonSpine/DropBatch

Dodałem kilka ustawień do GUI oraz dość istotne ostrzeżenie dla użytkownika.

screenshot-20221212235400.png

1 użytkowników online, w tym zalogowanych: 0, gości: 1