1
0
mirror of https://github.com/ciromattia/kcc synced 2026-04-18 23:19:00 +00:00

Compare commits

...

26 Commits

Author SHA1 Message Date
Alex Xu
3e88dabd1a bump 8.0.1 2025-07-03 12:43:54 -07:00
Alex Xu
3b7d949128 only slugify cbz subfolders if sort matters (#1010)
* only slugify if sort matters

* add comments

* make conditions more granular

* fix

* shorten
2025-07-03 12:39:13 -07:00
Alex Xu
68186285bd only use 7zz on macos (#1012) 2025-07-03 12:36:12 -07:00
Alex Xu
0abf620698 Create FUNDING.yml 2025-07-03 11:42:46 -07:00
Alex Xu
69d3bf3278 simplify removeNonImages (#1009) 2025-07-02 17:28:03 -07:00
Alex Xu
793992f408 bump to 8.0.0 2025-07-02 10:18:30 -07:00
Alex Xu
f41d5327e0 remove non images early (#1007) 2025-07-02 10:17:54 -07:00
Alex Xu
6f960aa1d0 bump mozjpeg 2025-07-01 08:12:32 -07:00
Alex Xu
17c0a73f9f upgrade 7z to 7zz (#1005) 2025-07-01 08:12:01 -07:00
Adrian
1fa5a5b19b Improved color detection (#1003)
* Improved color detection

* use pure python

---------

Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
2025-06-30 17:18:04 -07:00
Alex Xu
e8d05c16aa Update README.md 2025-06-29 14:03:43 -07:00
Alex Xu
74187b0d77 bump to 7.6.0 2025-06-29 09:37:59 -07:00
Alex Xu
f39e0caad0 exclude pkg_resources (#1001) 2025-06-29 09:34:54 -07:00
Alex Xu
6299c45790 fix flatpak kindlegen detection (7.5.0 regression) (#1000)
* fix flatpak kindlegen detection

* fix shared
2025-06-29 09:28:47 -07:00
Alex Xu
c7ebb230c2 add L comment 2025-06-27 11:45:31 -07:00
Alex Xu
3e4b729a30 always convert to L
even if workImg.color = False, it could still have color pixels, which can cause problems with quantization
2025-06-27 07:19:22 -07:00
Adrian
16a1d9b45f Fix quantization for colored images (#991) 2025-06-26 07:36:22 -07:00
Alex Xu
b7aef324aa Prevent selecting Kindle as output directory (#990)
* merge conflicts

* fix

* call it is

* fix imports
2025-06-25 18:16:42 -07:00
Alex Xu
1a42730ea0 next-folder (#988) 2025-06-25 14:55:17 -07:00
Adrian
217a18b7b5 Save images in GIF when output is set to MOBI and forcepng is used (#981)
* Save images in GIF when output is set to MOBI and forcepng option is used

* Save images in GIF when output is set to EPUB, kindle profile is used and forcepng option is set

* media-type="image/gif"

---------

Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
2025-06-24 22:14:00 -07:00
Silver0006
2ecbf7d2e9 Replaced add folders with output directory button. (#977)
* Replaced Add Folders with output directory button.

I took the code from #969 and replaced the selectDir() function with it. Then replaced the button and linked it to selectDir()

* Fixed merge error in gui

* Fixed bug

Missing () at the end of checkState on line 852

* small fixes

* fix checkbox not saving

* rename selectDir to selectOutputDirectory

---------

Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
2025-06-24 18:58:24 -07:00
Alex Xu
a1cf9c5c7d Update README.md 2025-06-21 22:24:57 -07:00
Alex Xu
32020d6b07 display-block (#986) 2025-06-20 17:28:42 -07:00
Adrian
221f964f14 Improve code readability (#984) 2025-06-20 10:12:24 -07:00
Adrian
e9f0310b94 Fixes to grayscale pages in forcecolor mode (#978)
* Fix bit depth of non-color pages in forcecolor mode

* Optimization for JPEG non-color pages in forcecolor mode
2025-06-18 15:12:12 -07:00
Adrian
2fa90c9f59 Fix png bit depth (#976) 2025-06-18 15:11:59 -07:00
14 changed files with 341 additions and 257 deletions

15
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: eink_dude
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -27,7 +27,7 @@ on underpowered ereaders with small storage capacities.
KCC avoids many common formatting issues (some of which occur [even on the Kindle Store](https://github.com/ciromattia/kcc/wiki/Kindle-Store-bad-formatting)), such as: KCC avoids many common formatting issues (some of which occur [even on the Kindle Store](https://github.com/ciromattia/kcc/wiki/Kindle-Store-bad-formatting)), such as:
1) faded black levels causing unneccessarily low contrast, which is hard to see and can cause eyestrain. 1) faded black levels causing unneccessarily low contrast, which is hard to see and can cause eyestrain.
2) unneccessary margins at the bottom of the screen 2) unneccessary margins at the bottom of the screen
3) Not utilizing the full 1860x2480 resolution of the 10" Kindle Scribe (feature in progress) 3) Not utilizing the full 1860x2480 resolution of the 10" Kindle Scribe
4) incorrect page turn direction for manga that's read right to left 4) incorrect page turn direction for manga that's read right to left
5) unaligned two page spreads in landscape, where pages are shifted over by 1 5) unaligned two page spreads in landscape, where pages are shifted over by 1
@@ -101,6 +101,8 @@ For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.co
## FAQ ## FAQ
- All options have additional information in tooltips if you hover over the option. - All options have additional information in tooltips if you hover over the option.
- To get the converted book onto your Kindle/Kobo, just drag and drop the mobi/kepub into the documents folder on your Kindle/Kobo via USB - To get the converted book onto your Kindle/Kobo, just drag and drop the mobi/kepub into the documents folder on your Kindle/Kobo via USB
- Colors inverted?
- Disable Kindle dark mode
- Cannot connect Kindle Scribe or 2024+ Kindle to macOS - Cannot connect Kindle Scribe or 2024+ Kindle to macOS
- Use official MTP [Amazon USB File Transfer app](https://www.amazon.com/gp/help/customer/display.html/ref=hp_Connect_USB_MTP?nodeId=TCUBEdEkbIhK07ysFu) - Use official MTP [Amazon USB File Transfer app](https://www.amazon.com/gp/help/customer/display.html/ref=hp_Connect_USB_MTP?nodeId=TCUBEdEkbIhK07ysFu)
(no login required). Works much better than previously recommended Android File Transfer. Cannot run simutaneously with other transfer apps. (no login required). Works much better than previously recommended Android File Transfer. Cannot run simutaneously with other transfer apps.

View File

@@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>519</width> <width>566</width>
<height>572</height> <height>573</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@@ -137,27 +137,62 @@
<property name="bottomMargin"> <property name="bottomMargin">
<number>0</number> <number>0</number>
</property> </property>
<item row="0" column="0"> <item row="1" column="3">
<widget class="QPushButton" name="directoryButton"> <widget class="QPushButton" name="convertButton">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>0</width> <width>0</width>
<height>30</height> <height>30</height>
</size> </size>
</property> </property>
<property name="font">
<font>
<bold>true</bold>
</font>
</property>
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Add directory containing JPG, PNG or GIF files to queue.&lt;br/&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;CBR, CBZ and CB7 files inside will not be processed!&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Shift+Click to select the output directory for this list.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
<property name="text"> <property name="text">
<string>Add image folder</string> <string>Convert</string>
</property> </property>
<property name="icon"> <property name="icon">
<iconset resource="KCC.qrc"> <iconset resource="KCC.qrc">
<normaloff>:/Other/icons/folder_new.png</normaloff>:/Other/icons/folder_new.png</iconset> <normaloff>:/Other/icons/convert.png</normaloff>:/Other/icons/convert.png</iconset>
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="3"> <item row="0" column="3">
<widget class="QPushButton" name="clearButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<property name="text">
<string>Clear list</string>
</property>
<property name="icon">
<iconset resource="KCC.qrc">
<normaloff>:/Other/icons/clear.png</normaloff>:/Other/icons/clear.png</iconset>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="deviceBox">
<property name="minimumSize">
<size>
<width>0</width>
<height>28</height>
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Target device.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="fileButton"> <widget class="QPushButton" name="fileButton">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
@@ -177,20 +212,46 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="0" column="5">
<widget class="QComboBox" name="deviceBox"> <widget class="QPushButton" name="defaultOutputFolderButton">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>0</width> <width>0</width>
<height>28</height> <height>30</height>
</size> </size>
</property> </property>
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Target device.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Use this to select the default output directory.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="KCC.qrc">
<normaloff>:/Other/icons/folder_new.png</normaloff>:/Other/icons/folder_new.png</iconset>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="3"> <item row="0" column="4">
<widget class="QCheckBox" name="defaultOutputFolderBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - next to source&lt;br/&gt;&lt;/span&gt;Place output files next to source files&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - folder next to source&lt;br/&gt;&lt;/span&gt;Place output files in a folder next to source files&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Custom&lt;br/&gt;&lt;/span&gt;Place output files in custom directory specified by right button&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Output Folder</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="4" colspan="2">
<widget class="QComboBox" name="formatBox"> <widget class="QComboBox" name="formatBox">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
@@ -203,55 +264,14 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="2">
<widget class="QPushButton" name="convertButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<property name="font">
<font>
<bold>true</bold>
</font>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Shift+Click to select the output directory.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Convert</string>
</property>
<property name="icon">
<iconset resource="KCC.qrc">
<normaloff>:/Other/icons/convert.png</normaloff>:/Other/icons/convert.png</iconset>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="clearButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<property name="text">
<string>Clear list</string>
</property>
<property name="icon">
<iconset resource="KCC.qrc">
<normaloff>:/Other/icons/clear.png</normaloff>:/Other/icons/clear.png</iconset>
</property>
</widget>
</item>
</layout> </layout>
<zorder>directoryButton</zorder>
<zorder>clearButton</zorder> <zorder>clearButton</zorder>
<zorder>fileButton</zorder>
<zorder>deviceBox</zorder> <zorder>deviceBox</zorder>
<zorder>convertButton</zorder> <zorder>convertButton</zorder>
<zorder>formatBox</zorder> <zorder>formatBox</zorder>
<zorder>defaultOutputFolderButton</zorder>
<zorder>fileButton</zorder>
<zorder>defaultOutputFolderBox</zorder>
</widget> </widget>
</item> </item>
<item row="1" column="0" colspan="2"> <item row="1" column="0" colspan="2">
@@ -813,8 +833,6 @@
<tabstops> <tabstops>
<tabstop>convertButton</tabstop> <tabstop>convertButton</tabstop>
<tabstop>clearButton</tabstop> <tabstop>clearButton</tabstop>
<tabstop>directoryButton</tabstop>
<tabstop>fileButton</tabstop>
<tabstop>deviceBox</tabstop> <tabstop>deviceBox</tabstop>
<tabstop>formatBox</tabstop> <tabstop>formatBox</tabstop>
<tabstop>mangaBox</tabstop> <tabstop>mangaBox</tabstop>

View File

@@ -11,7 +11,7 @@ a = Analysis(['kcc-c2e.py'],
hiddenimports=['_cffi_backend'], hiddenimports=['_cffi_backend'],
hookspath=[], hookspath=[],
runtime_hooks=[], runtime_hooks=[],
excludes=[], excludes=['pkg_resources'],
win_no_prefer_redirects=False, win_no_prefer_redirects=False,
win_private_assemblies=False, win_private_assemblies=False,
cipher=block_cipher, cipher=block_cipher,

View File

@@ -11,7 +11,7 @@ a = Analysis(['kcc-c2p.py'],
hiddenimports=['_cffi_backend'], hiddenimports=['_cffi_backend'],
hookspath=[], hookspath=[],
runtime_hooks=[], runtime_hooks=[],
excludes=[], excludes=['pkg_resources'],
win_no_prefer_redirects=False, win_no_prefer_redirects=False,
win_private_assemblies=False, win_private_assemblies=False,
cipher=block_cipher, cipher=block_cipher,

View File

@@ -11,7 +11,7 @@ a = Analysis(['kcc.py'],
hiddenimports=['_cffi_backend'], hiddenimports=['_cffi_backend'],
hookspath=[], hookspath=[],
runtime_hooks=[], runtime_hooks=[],
excludes=[], excludes=['pkg_resources'],
win_no_prefer_redirects=False, win_no_prefer_redirects=False,
win_private_assemblies=False, win_private_assemblies=False,
cipher=block_cipher, cipher=block_cipher,

View File

@@ -16,6 +16,9 @@
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER # OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE. # PERFORMANCE OF THIS SOFTWARE.
import itertools
from pathlib import Path
from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings) from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings)
from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices) from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices)
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QApplication, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog) from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QApplication, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog)
@@ -27,7 +30,7 @@ import sys
from urllib.parse import unquote from urllib.parse import unquote
from time import sleep from time import sleep
from shutil import move, rmtree from shutil import move, rmtree
from subprocess import STDOUT, PIPE from subprocess import STDOUT, PIPE, CalledProcessError
import requests import requests
from xml.sax.saxutils import escape from xml.sax.saxutils import escape
@@ -37,10 +40,10 @@ from packaging.version import Version
from raven import Client from raven import Client
from tempfile import gettempdir from tempfile import gettempdir
from .shared import HTMLStripper, available_archive_tools, sanitizeTrace, walkLevel, subprocess_run from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run
from .comicarchive import SEVENZIP, available_archive_tools
from . import __version__ from . import __version__
from . import comic2ebook from . import comic2ebook
from . import image
from . import metadata from . import metadata
from . import kindle from . import kindle
from . import KCC_ui from . import KCC_ui
@@ -487,17 +490,33 @@ class SystemTrayIcon(QSystemTrayIcon):
class KCCGUI(KCC_ui.Ui_mainWindow): class KCCGUI(KCC_ui.Ui_mainWindow):
def selectDir(self): def selectDefaultOutputFolder(self):
if self.needClean: dname = QFileDialog.getExistingDirectory(MW, 'Select default output folder', self.defaultOutputFolder)
self.needClean = False if self.is_directory_on_kindle(dname):
GUI.jobList.clear() return
dname = QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath)
if dname != '': if dname != '':
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
dname = dname.replace('/', '\\') dname = dname.replace('/', '\\')
self.lastPath = os.path.abspath(os.path.join(dname, os.pardir)) GUI.defaultOutputFolder = dname
GUI.jobList.addItem(dname)
GUI.jobList.scrollToBottom() def is_directory_on_kindle(self, dname):
path = Path(dname)
for parent in itertools.chain([path], path.parents):
if parent.name == 'documents' and parent.parent.joinpath('system').joinpath('thumbnails').is_dir():
self.addMessage("Cannot select Kindle as output directory", 'error')
return True
def selectOutputFolder(self):
dname = QFileDialog.getExistingDirectory(MW, 'Select output directory', self.lastPath)
if self.is_directory_on_kindle(dname):
return
if dname != '':
if sys.platform.startswith('win'):
dname = dname.replace('/', '\\')
GUI.targetDirectory = dname
else:
GUI.targetDirectory = ''
return GUI.targetDirectory
def selectFile(self): def selectFile(self):
if self.needClean: if self.needClean:
@@ -585,7 +604,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.editorButton.setEnabled(status) GUI.editorButton.setEnabled(status)
GUI.wikiButton.setEnabled(status) GUI.wikiButton.setEnabled(status)
GUI.deviceBox.setEnabled(status) GUI.deviceBox.setEnabled(status)
GUI.directoryButton.setEnabled(status) GUI.defaultOutputFolderButton.setEnabled(status)
GUI.clearButton.setEnabled(status) GUI.clearButton.setEnabled(status)
GUI.fileButton.setEnabled(status) GUI.fileButton.setEnabled(status)
GUI.formatBox.setEnabled(status) GUI.formatBox.setEnabled(status)
@@ -782,13 +801,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.worker.sync() self.worker.sync()
else: else:
if QApplication.keyboardModifiers() == Qt.KeyboardModifier.ShiftModifier: if QApplication.keyboardModifiers() == Qt.KeyboardModifier.ShiftModifier:
dname = QFileDialog.getExistingDirectory(MW, 'Select output directory', self.lastPath) if not self.selectOutputFolder():
if dname != '': return
if sys.platform.startswith('win'): elif GUI.defaultOutputFolderBox.isChecked():
dname = dname.replace('/', '\\') self.targetDirectory = self.defaultOutputFolder
GUI.targetDirectory = dname
else:
GUI.targetDirectory = ''
else: else:
GUI.targetDirectory = '' GUI.targetDirectory = ''
self.progress.start() self.progress.start()
@@ -799,6 +815,12 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.addMessage('No files selected! Please choose files to convert.', 'error') self.addMessage('No files selected! Please choose files to convert.', 'error')
self.needClean = True self.needClean = True
return return
if GUI.defaultOutputFolderBox.checkState() == Qt.CheckState.PartiallyChecked:
parent = Path(self.jobList.item(0).text()).parent
target_path = parent.joinpath(f"{parent.name}")
if not target_path.exists():
target_path.mkdir()
self.targetDirectory = str(target_path)
if self.currentMode > 2 and (GUI.widthBox.value() == 0 or GUI.heightBox.value() == 0): if self.currentMode > 2 and (GUI.widthBox.value() == 0 or GUI.heightBox.value() == 0):
GUI.jobList.clear() GUI.jobList.clear()
self.addMessage('Target resolution is not set!', 'error') self.addMessage('Target resolution is not set!', 'error')
@@ -830,6 +852,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
event.ignore() event.ignore()
self.settings.setValue('settingsVersion', __version__) self.settings.setValue('settingsVersion', __version__)
self.settings.setValue('lastPath', self.lastPath) self.settings.setValue('lastPath', self.lastPath)
self.settings.setValue('defaultOutputFolder', self.defaultOutputFolder)
self.settings.setValue('lastDevice', GUI.deviceBox.currentIndex()) self.settings.setValue('lastDevice', GUI.deviceBox.currentIndex())
self.settings.setValue('currentFormat', GUI.formatBox.currentIndex()) self.settings.setValue('currentFormat', GUI.formatBox.currentIndex())
self.settings.setValue('startNumber', self.startNumber + 1) self.settings.setValue('startNumber', self.startNumber + 1)
@@ -856,6 +879,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'deleteBox': GUI.deleteBox.checkState().value, 'deleteBox': GUI.deleteBox.checkState().value,
'spreadShiftBox': GUI.spreadShiftBox.checkState().value, 'spreadShiftBox': GUI.spreadShiftBox.checkState().value,
'fileFusionBox': GUI.fileFusionBox.checkState().value, 'fileFusionBox': GUI.fileFusionBox.checkState().value,
'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState().value,
'noRotateBox': GUI.noRotateBox.checkState().value, 'noRotateBox': GUI.noRotateBox.checkState().value,
'maximizeStrips': GUI.maximizeStrips.checkState().value, 'maximizeStrips': GUI.maximizeStrips.checkState().value,
'gammaSlider': float(self.gammaValue) * 100, 'gammaSlider': float(self.gammaValue) * 100,
@@ -921,7 +945,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.addMessage('Your <a href="https://www.amazon.com/b?node=23496309011">KindleGen</a>' self.addMessage('Your <a href="https://www.amazon.com/b?node=23496309011">KindleGen</a>'
' is outdated! MOBI conversion might fail.', 'warning') ' is outdated! MOBI conversion might fail.', 'warning')
break break
except FileNotFoundError: except (FileNotFoundError, CalledProcessError):
self.kindleGen = False self.kindleGen = False
if startup: if startup:
self.display_kindlegen_missing() self.display_kindlegen_missing()
@@ -937,6 +961,9 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.settings = QSettings('ciromattia', 'kcc') self.settings = QSettings('ciromattia', 'kcc')
self.settingsVersion = self.settings.value('settingsVersion', '', type=str) self.settingsVersion = self.settings.value('settingsVersion', '', type=str)
self.lastPath = self.settings.value('lastPath', '', type=str) self.lastPath = self.settings.value('lastPath', '', type=str)
self.defaultOutputFolder = str(self.settings.value('defaultOutputFolder', '', type=str))
if not os.path.exists(self.defaultOutputFolder):
self.defaultOutputFolder = ''
self.lastDevice = self.settings.value('lastDevice', 0, type=int) self.lastDevice = self.settings.value('lastDevice', 0, type=int)
self.currentFormat = self.settings.value('currentFormat', 0, type=int) self.currentFormat = self.settings.value('currentFormat', 0, type=int)
self.startNumber = self.settings.value('startNumber', 0, type=int) self.startNumber = self.settings.value('startNumber', 0, type=int)
@@ -965,7 +992,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if self.windowSize == '0x0': if self.windowSize == '0x0':
MW.resize(500, 500) MW.resize(500, 500)
elif sys.platform.startswith('darwin'): elif sys.platform.startswith('darwin'):
for element in ['editorButton', 'wikiButton', 'directoryButton', 'clearButton', 'fileButton', 'deviceBox', for element in ['editorButton', 'wikiButton', 'defaultOutputFolderButton', 'clearButton', 'fileButton', 'deviceBox',
'convertButton', 'formatBox']: 'convertButton', 'formatBox']:
getattr(GUI, element).setMinimumSize(QSize(0, 0)) getattr(GUI, element).setMinimumSize(QSize(0, 0))
GUI.gridLayout.setContentsMargins(-1, -1, -1, -1) GUI.gridLayout.setContentsMargins(-1, -1, -1, -1)
@@ -1133,21 +1160,20 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.addMessage('<b>Welcome!</b>', 'info') self.addMessage('<b>Welcome!</b>', 'info')
self.addMessage('<b>Tip:</b> Hover mouse over options to see additional information in tooltips.', 'info') self.addMessage('<b>Tip:</b> Hover mouse over options to see additional information in tooltips.', 'info')
self.addMessage('<b>Tip:</b> You can drag and drop image folders or comic files/archives into this window to convert.', 'info') self.addMessage('<b>Tip:</b> You can drag and drop image folders or comic files/archives into this window to convert.', 'info')
self.addMessage('<b>Tip:</b> Shift clicking the Convert button lets you select a custom output directory', 'info')
if self.startNumber < 5: if self.startNumber < 5:
self.addMessage('Since you are a new user of <b>KCC</b> please see few ' self.addMessage('Since you are a new user of <b>KCC</b> please see few '
'<a href="https://github.com/ciromattia/kcc/wiki/Important-tips">important tips</a>.', '<a href="https://github.com/ciromattia/kcc/wiki/Important-tips">important tips</a>.',
'info') 'info')
self.tar = 'tar' in available_archive_tools() self.tar = 'tar' in available_archive_tools()
self.sevenzip = '7z' in available_archive_tools() self.sevenzip = SEVENZIP in available_archive_tools()
if not any([self.tar, self.sevenzip]): if not any([self.tar, self.sevenzip]):
self.addMessage('<a href="https://github.com/ciromattia/kcc#7-zip">Install 7z (link)</a>' self.addMessage('<a href="https://github.com/ciromattia/kcc#7-zip">Install 7z (link)</a>'
' to enable CBZ/CBR/ZIP/etc processing.', 'warning') ' to enable CBZ/CBR/ZIP/etc processing.', 'warning')
self.detectKindleGen(True) self.detectKindleGen(True)
APP.messageFromOtherInstance.connect(self.handleMessage) APP.messageFromOtherInstance.connect(self.handleMessage)
GUI.directoryButton.clicked.connect(self.selectDir) GUI.defaultOutputFolderButton.clicked.connect(self.selectDefaultOutputFolder)
GUI.clearButton.clicked.connect(self.clearJobs) GUI.clearButton.clicked.connect(self.clearJobs)
GUI.fileButton.clicked.connect(self.selectFile) GUI.fileButton.clicked.connect(self.selectFile)
GUI.editorButton.clicked.connect(self.selectFileMetaEditor) GUI.editorButton.clicked.connect(self.selectFileMetaEditor)

View File

@@ -26,7 +26,7 @@ class Ui_mainWindow(object):
def setupUi(self, mainWindow): def setupUi(self, mainWindow):
if not mainWindow.objectName(): if not mainWindow.objectName():
mainWindow.setObjectName(u"mainWindow") mainWindow.setObjectName(u"mainWindow")
mainWindow.resize(519, 572) mainWindow.resize(566, 573)
icon = QIcon() icon = QIcon()
icon.addFile(u":/Icon/icons/comic2ebook.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon.addFile(u":/Icon/icons/comic2ebook.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
mainWindow.setWindowIcon(icon) mainWindow.setWindowIcon(icon)
@@ -90,63 +90,75 @@ class Ui_mainWindow(object):
self.gridLayout_4 = QGridLayout(self.buttonWidget) self.gridLayout_4 = QGridLayout(self.buttonWidget)
self.gridLayout_4.setObjectName(u"gridLayout_4") self.gridLayout_4.setObjectName(u"gridLayout_4")
self.gridLayout_4.setContentsMargins(0, 0, 0, 0) self.gridLayout_4.setContentsMargins(0, 0, 0, 0)
self.directoryButton = QPushButton(self.buttonWidget)
self.directoryButton.setObjectName(u"directoryButton")
self.directoryButton.setMinimumSize(QSize(0, 30))
icon4 = QIcon()
icon4.addFile(u":/Other/icons/folder_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.directoryButton.setIcon(icon4)
self.gridLayout_4.addWidget(self.directoryButton, 0, 0, 1, 1)
self.fileButton = QPushButton(self.buttonWidget)
self.fileButton.setObjectName(u"fileButton")
self.fileButton.setMinimumSize(QSize(0, 30))
icon5 = QIcon()
icon5.addFile(u":/Other/icons/document_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.fileButton.setIcon(icon5)
self.gridLayout_4.addWidget(self.fileButton, 0, 3, 1, 1)
self.deviceBox = QComboBox(self.buttonWidget)
self.deviceBox.setObjectName(u"deviceBox")
self.deviceBox.setMinimumSize(QSize(0, 28))
self.gridLayout_4.addWidget(self.deviceBox, 1, 0, 1, 1)
self.formatBox = QComboBox(self.buttonWidget)
self.formatBox.setObjectName(u"formatBox")
self.formatBox.setMinimumSize(QSize(0, 28))
self.gridLayout_4.addWidget(self.formatBox, 1, 3, 1, 1)
self.convertButton = QPushButton(self.buttonWidget) self.convertButton = QPushButton(self.buttonWidget)
self.convertButton.setObjectName(u"convertButton") self.convertButton.setObjectName(u"convertButton")
self.convertButton.setMinimumSize(QSize(0, 30)) self.convertButton.setMinimumSize(QSize(0, 30))
font = QFont() font = QFont()
font.setBold(True) font.setBold(True)
self.convertButton.setFont(font) self.convertButton.setFont(font)
icon6 = QIcon() icon4 = QIcon()
icon6.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon4.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.convertButton.setIcon(icon6) self.convertButton.setIcon(icon4)
self.gridLayout_4.addWidget(self.convertButton, 1, 2, 1, 1) self.gridLayout_4.addWidget(self.convertButton, 1, 3, 1, 1)
self.clearButton = QPushButton(self.buttonWidget) self.clearButton = QPushButton(self.buttonWidget)
self.clearButton.setObjectName(u"clearButton") self.clearButton.setObjectName(u"clearButton")
self.clearButton.setMinimumSize(QSize(0, 30)) self.clearButton.setMinimumSize(QSize(0, 30))
icon5 = QIcon()
icon5.addFile(u":/Other/icons/clear.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.clearButton.setIcon(icon5)
self.gridLayout_4.addWidget(self.clearButton, 0, 3, 1, 1)
self.deviceBox = QComboBox(self.buttonWidget)
self.deviceBox.setObjectName(u"deviceBox")
self.deviceBox.setMinimumSize(QSize(0, 28))
self.gridLayout_4.addWidget(self.deviceBox, 1, 1, 1, 1)
self.fileButton = QPushButton(self.buttonWidget)
self.fileButton.setObjectName(u"fileButton")
self.fileButton.setMinimumSize(QSize(0, 30))
icon6 = QIcon()
icon6.addFile(u":/Other/icons/document_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.fileButton.setIcon(icon6)
self.gridLayout_4.addWidget(self.fileButton, 0, 1, 1, 1)
self.defaultOutputFolderButton = QPushButton(self.buttonWidget)
self.defaultOutputFolderButton.setObjectName(u"defaultOutputFolderButton")
self.defaultOutputFolderButton.setMinimumSize(QSize(0, 30))
icon7 = QIcon() icon7 = QIcon()
icon7.addFile(u":/Other/icons/clear.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon7.addFile(u":/Other/icons/folder_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.clearButton.setIcon(icon7) self.defaultOutputFolderButton.setIcon(icon7)
self.gridLayout_4.addWidget(self.clearButton, 0, 2, 1, 1) self.gridLayout_4.addWidget(self.defaultOutputFolderButton, 0, 5, 1, 1)
self.defaultOutputFolderBox = QCheckBox(self.buttonWidget)
self.defaultOutputFolderBox.setObjectName(u"defaultOutputFolderBox")
sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
sizePolicy1.setHorizontalStretch(0)
sizePolicy1.setVerticalStretch(0)
sizePolicy1.setHeightForWidth(self.defaultOutputFolderBox.sizePolicy().hasHeightForWidth())
self.defaultOutputFolderBox.setSizePolicy(sizePolicy1)
self.defaultOutputFolderBox.setTristate(True)
self.gridLayout_4.addWidget(self.defaultOutputFolderBox, 0, 4, 1, 1)
self.formatBox = QComboBox(self.buttonWidget)
self.formatBox.setObjectName(u"formatBox")
self.formatBox.setMinimumSize(QSize(0, 28))
self.gridLayout_4.addWidget(self.formatBox, 1, 4, 1, 2)
self.directoryButton.raise_()
self.clearButton.raise_() self.clearButton.raise_()
self.fileButton.raise_()
self.deviceBox.raise_() self.deviceBox.raise_()
self.convertButton.raise_() self.convertButton.raise_()
self.formatBox.raise_() self.formatBox.raise_()
self.defaultOutputFolderButton.raise_()
self.fileButton.raise_()
self.defaultOutputFolderBox.raise_()
self.gridLayout.addWidget(self.buttonWidget, 3, 0, 1, 2) self.gridLayout.addWidget(self.buttonWidget, 3, 0, 1, 2)
@@ -167,11 +179,11 @@ class Ui_mainWindow(object):
self.gridLayout_3.setContentsMargins(0, 0, 0, 0) self.gridLayout_3.setContentsMargins(0, 0, 0, 0)
self.hLabel = QLabel(self.customWidget) self.hLabel = QLabel(self.customWidget)
self.hLabel.setObjectName(u"hLabel") self.hLabel.setObjectName(u"hLabel")
sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred) sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
sizePolicy1.setHorizontalStretch(0) sizePolicy2.setHorizontalStretch(0)
sizePolicy1.setVerticalStretch(0) sizePolicy2.setVerticalStretch(0)
sizePolicy1.setHeightForWidth(self.hLabel.sizePolicy().hasHeightForWidth()) sizePolicy2.setHeightForWidth(self.hLabel.sizePolicy().hasHeightForWidth())
self.hLabel.setSizePolicy(sizePolicy1) self.hLabel.setSizePolicy(sizePolicy2)
self.gridLayout_3.addWidget(self.hLabel, 0, 2, 1, 1) self.gridLayout_3.addWidget(self.hLabel, 0, 2, 1, 1)
@@ -183,8 +195,8 @@ class Ui_mainWindow(object):
self.wLabel = QLabel(self.customWidget) self.wLabel = QLabel(self.customWidget)
self.wLabel.setObjectName(u"wLabel") self.wLabel.setObjectName(u"wLabel")
sizePolicy1.setHeightForWidth(self.wLabel.sizePolicy().hasHeightForWidth()) sizePolicy2.setHeightForWidth(self.wLabel.sizePolicy().hasHeightForWidth())
self.wLabel.setSizePolicy(sizePolicy1) self.wLabel.setSizePolicy(sizePolicy2)
self.gridLayout_3.addWidget(self.wLabel, 0, 0, 1, 1) self.gridLayout_3.addWidget(self.wLabel, 0, 0, 1, 1)
@@ -223,11 +235,8 @@ class Ui_mainWindow(object):
self.preserveMarginBox = QSpinBox(self.croppingWidget) self.preserveMarginBox = QSpinBox(self.croppingWidget)
self.preserveMarginBox.setObjectName(u"preserveMarginBox") self.preserveMarginBox.setObjectName(u"preserveMarginBox")
sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) sizePolicy1.setHeightForWidth(self.preserveMarginBox.sizePolicy().hasHeightForWidth())
sizePolicy2.setHorizontalStretch(0) self.preserveMarginBox.setSizePolicy(sizePolicy1)
sizePolicy2.setVerticalStretch(0)
sizePolicy2.setHeightForWidth(self.preserveMarginBox.sizePolicy().hasHeightForWidth())
self.preserveMarginBox.setSizePolicy(sizePolicy2)
self.preserveMarginBox.setMaximum(99) self.preserveMarginBox.setMaximum(99)
self.preserveMarginBox.setSingleStep(5) self.preserveMarginBox.setSingleStep(5)
self.preserveMarginBox.setValue(0) self.preserveMarginBox.setValue(0)
@@ -434,9 +443,7 @@ class Ui_mainWindow(object):
self.statusBar.setSizeGripEnabled(False) self.statusBar.setSizeGripEnabled(False)
mainWindow.setStatusBar(self.statusBar) mainWindow.setStatusBar(self.statusBar)
QWidget.setTabOrder(self.convertButton, self.clearButton) QWidget.setTabOrder(self.convertButton, self.clearButton)
QWidget.setTabOrder(self.clearButton, self.directoryButton) QWidget.setTabOrder(self.clearButton, self.deviceBox)
QWidget.setTabOrder(self.directoryButton, self.fileButton)
QWidget.setTabOrder(self.fileButton, self.deviceBox)
QWidget.setTabOrder(self.deviceBox, self.formatBox) QWidget.setTabOrder(self.deviceBox, self.formatBox)
QWidget.setTabOrder(self.formatBox, self.mangaBox) QWidget.setTabOrder(self.formatBox, self.mangaBox)
QWidget.setTabOrder(self.mangaBox, self.rotateBox) QWidget.setTabOrder(self.mangaBox, self.rotateBox)
@@ -479,24 +486,28 @@ class Ui_mainWindow(object):
self.kofiButton.setText(QCoreApplication.translate("mainWindow", u"Support me on Ko-fi", None)) self.kofiButton.setText(QCoreApplication.translate("mainWindow", u"Support me on Ko-fi", None))
self.wikiButton.setText(QCoreApplication.translate("mainWindow", u"Wiki", None)) self.wikiButton.setText(QCoreApplication.translate("mainWindow", u"Wiki", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.directoryButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Add directory containing JPG, PNG or GIF files to queue.<br/><span style=\" font-weight:600;\">CBR, CBZ and CB7 files inside will not be processed!</span></p></body></html>", None)) self.convertButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Shift+Click to select the output directory for this list.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.convertButton.setText(QCoreApplication.translate("mainWindow", u"Convert", None))
self.clearButton.setText(QCoreApplication.translate("mainWindow", u"Clear list", None))
#if QT_CONFIG(tooltip)
self.deviceBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Target device.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.directoryButton.setText(QCoreApplication.translate("mainWindow", u"Add image folder", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.fileButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Add CBR, CBZ, CB7 or PDF file to queue.</p></body></html>", None)) self.fileButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Add CBR, CBZ, CB7 or PDF file to queue.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.fileButton.setText(QCoreApplication.translate("mainWindow", u"Add file(s)", None)) self.fileButton.setText(QCoreApplication.translate("mainWindow", u"Add file(s)", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.deviceBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Target device.</p></body></html>", None)) self.defaultOutputFolderButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Use this to select the default output directory.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.defaultOutputFolderButton.setText("")
#if QT_CONFIG(tooltip)
self.defaultOutputFolderBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - next to source<br/></span>Place output files next to source files</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - folder next to source<br/></span>Place output files in a folder next to source files</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Custom<br/></span>Place output files in custom directory specified by right button</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.defaultOutputFolderBox.setText(QCoreApplication.translate("mainWindow", u"Output Folder", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.formatBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Output format.</p></body></html>", None)) self.formatBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Output format.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
#if QT_CONFIG(tooltip)
self.convertButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Shift+Click to select the output directory.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.convertButton.setText(QCoreApplication.translate("mainWindow", u"Convert", None))
self.clearButton.setText(QCoreApplication.translate("mainWindow", u"Clear list", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.hLabel.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>", None)) self.hLabel.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)

View File

@@ -1,4 +1,4 @@
__version__ = '7.5.1' __version__ = '8.0.1'
__license__ = 'ISC' __license__ = 'ISC'
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi' __copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'

View File

@@ -34,7 +34,7 @@ from tempfile import mkdtemp, gettempdir, TemporaryFile
from shutil import move, copytree, rmtree, copyfile from shutil import move, copytree, rmtree, copyfile
from multiprocessing import Pool from multiprocessing import Pool
from uuid import uuid4 from uuid import uuid4
from natsort import os_sort_keygen from natsort import os_sort_keygen, os_sorted
from slugify import slugify as slugify_ext from slugify import slugify as slugify_ext
from PIL import Image, ImageFile from PIL import Image, ImageFile
from pathlib import Path from pathlib import Path
@@ -42,7 +42,8 @@ from subprocess import STDOUT, PIPE, CalledProcessError
from psutil import virtual_memory, disk_usage from psutil import virtual_memory, disk_usage
from html import escape as hescape from html import escape as hescape
from .shared import available_archive_tools, getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run from .shared import getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run
from .comicarchive import SEVENZIP, available_archive_tools
from . import comic2panel from . import comic2panel
from . import image from . import image
from . import comicarchive from . import comicarchive
@@ -336,6 +337,8 @@ def buildOPF(dstdir, title, filelist, cover=None):
".xhtml\" media-type=\"application/xhtml+xml\"/>\n") ".xhtml\" media-type=\"application/xhtml+xml\"/>\n")
if '.png' == filename[1]: if '.png' == filename[1]:
mt = 'image/png' mt = 'image/png'
elif '.gif' == filename[1]:
mt = 'image/gif'
else: else:
mt = 'image/jpeg' mt = 'image/jpeg'
f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\"" + f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\"" +
@@ -448,10 +451,13 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, len
"margin: 0;\n", "margin: 0;\n",
"padding: 0;\n", "padding: 0;\n",
"}\n", "}\n",
"img {\n",
"display: block;\n",
"}\n",
]) ])
if options.kindle_scribe_azw3:
f.writelines([
"img {\n",
"display: block;\n",
"}\n",
])
if options.iskindle and options.panelview: if options.iskindle and options.panelview:
f.writelines(["#PV {\n", f.writelines(["#PV {\n",
"position: absolute;\n", "position: absolute;\n",
@@ -642,8 +648,12 @@ def imgFileProcessing(work):
img.autocontrastImage() img.autocontrastImage()
img.resizeImage() img.resizeImage()
img.optimizeForDisplay(opt.reducerainbow) img.optimizeForDisplay(opt.reducerainbow)
if opt.forcepng and not opt.forcecolor: if opt.forcecolor and img.color:
pass
elif opt.forcepng:
img.quantizeImage() img.quantizeImage()
else:
img.convertToGrayscale()
output.append(img.saveToDir()) output.append(img.saveToDir())
return output return output
except Exception: except Exception:
@@ -681,16 +691,6 @@ def getWorkFolder(afile):
cbx = comicarchive.ComicArchive(afile) cbx = comicarchive.ComicArchive(afile)
path = cbx.extract(workdir) path = cbx.extract(workdir)
sanitizePermissions(path) sanitizePermissions(path)
tdir = os.listdir(workdir)
if len(tdir) == 2 and 'ComicInfo.xml' in tdir:
tdir.remove('ComicInfo.xml')
if os.path.isdir(os.path.join(workdir, tdir[0])):
os.replace(
os.path.join(workdir, 'ComicInfo.xml'),
os.path.join(workdir, tdir[0], 'ComicInfo.xml')
)
if len(tdir) == 1 and os.path.isdir(os.path.join(workdir, tdir[0])):
path = os.path.join(workdir, tdir[0])
except OSError as e: except OSError as e:
rmtree(workdir, True) rmtree(workdir, True)
raise UserWarning(e) raise UserWarning(e)
@@ -813,29 +813,48 @@ def getPanelViewSize(deviceres, size):
return str(int(x)), str(int(y)) return str(int(x)), str(int(y))
def removeNonImages(filetree):
# clean dot from original file
dot_clean(filetree)
for root, dirs, files in os.walk(filetree):
for name in files:
_, ext = getImageFileName(name)
if ext not in ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2'):
if os.path.exists(os.path.join(root, name)):
os.remove(os.path.join(root, name))
# remove empty nested folders
for root, dirs, files in os.walk(filetree, False):
if not files and not dirs:
os.rmdir(root)
def sanitizeTree(filetree): def sanitizeTree(filetree):
chapterNames = {} chapterNames = {}
page = 1 page = 1
cover_path = None cover_path = None
for root, dirs, files in os.walk(filetree): for root, dirs, files in os.walk(filetree):
dirs.sort(key=OS_SORT_KEY)
files.sort(key=OS_SORT_KEY) files.sort(key=OS_SORT_KEY)
for name in files: for name in files:
splitname = os.path.splitext(name) _, ext = getImageFileName(name)
# 9999 page limit # 9999 page limit
slugified = f'kcc-{page:04}' unique_name = f'kcc-{page:04}'
page += 1 page += 1
newKey = os.path.join(root, slugified + splitname[1]) newKey = os.path.join(root, unique_name + ext)
key = os.path.join(root, name) key = os.path.join(root, name)
if key != newKey: if key != newKey:
os.replace(key, newKey) os.replace(key, newKey)
if not cover_path: if not cover_path:
cover_path = newKey cover_path = newKey
is_natural_sorted = False
if os_sorted(dirs) == sorted(dirs):
is_natural_sorted = True
dirs.sort(key=OS_SORT_KEY)
for i, name in enumerate(dirs): for i, name in enumerate(dirs):
tmpName = name tmpName = name
slugified = slugify(name) slugified = slugify(name, is_natural_sorted)
while os.path.exists(os.path.join(root, slugified)) and name.upper() != slugified.upper(): while os.path.exists(os.path.join(root, slugified)) and name.upper() != slugified.upper():
slugified += "A" slugified += "A"
chapterNames[slugified] = tmpName chapterNames[slugified] = tmpName
@@ -861,8 +880,7 @@ def sanitizePermissions(filetree):
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD) os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD)
for name in dirs: for name in dirs:
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD | S_IEXEC) os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD | S_IEXEC)
# clean dot from original file
dot_clean(filetree)
def dot_clean(filetree): def dot_clean(filetree):
for root, _, files in os.walk(filetree, topdown=False): for root, _, files in os.walk(filetree, topdown=False):
@@ -981,10 +999,6 @@ def detectSuboptimalProcessing(tmppath, orgpath):
os.remove(os.path.join(root, name)) os.remove(os.path.join(root, name))
except OSError as e: except OSError as e:
raise RuntimeError(f"{name}: {e}") raise RuntimeError(f"{name}: {e}")
# remove empty nested folders
for root, dirs, files in os.walk(tmppath, False):
if not files and not dirs:
os.rmdir(root)
if alreadyProcessed: if alreadyProcessed:
print("WARNING: Source files are probably created by KCC. The second conversion will decrease quality.") print("WARNING: Source files are probably created by KCC. The second conversion will decrease quality.")
if GUI: if GUI:
@@ -1007,23 +1021,27 @@ def createNewTome(parent):
return tomePath, tomePathRoot return tomePath, tomePathRoot
def slugify(value): def slugify(value, is_natural_sorted):
if options.format == 'CBZ': if options.format == 'CBZ' and is_natural_sorted:
return value return value
value = slugify_ext(value, regex_pattern=r'[^-a-z0-9_\.]+').strip('.') if options.format != 'CBZ':
value = sub(r'0*([0-9]{4,})', r'\1', sub(r'([0-9]+)', r'0000\1', value, count=2)) # convert all unicode to ascii via slugify
value = slugify_ext(value, regex_pattern=r'[^-a-z0-9_\.]+').strip('.')
if not is_natural_sorted:
# pad zeros to numbers
value = sub(r'0*([0-9]{4,})', r'\1', sub(r'([0-9]+)', r'0000\1', value, count=2))
return value return value
def makeZIP(zipfilename, basedir, isepub=False): def makeZIP(zipfilename, basedir, isepub=False):
start = perf_counter() start = perf_counter()
zipfilename = os.path.abspath(zipfilename) + '.zip' zipfilename = os.path.abspath(zipfilename) + '.zip'
if '7z' in available_archive_tools(): if SEVENZIP in available_archive_tools():
if isepub: if isepub:
mimetypeFile = open(os.path.join(basedir, 'mimetype'), 'w') mimetypeFile = open(os.path.join(basedir, 'mimetype'), 'w')
mimetypeFile.write('application/epub+zip') mimetypeFile.write('application/epub+zip')
mimetypeFile.close() mimetypeFile.close()
subprocess_run(['7z', 'a', '-tzip', zipfilename, os.path.join(basedir, "*")], capture_output=True, check=True) subprocess_run([SEVENZIP, 'a', '-tzip', zipfilename, os.path.join(basedir, "*")], capture_output=True, check=True)
else: else:
zipOutput = ZipFile(zipfilename, 'w', ZIP_DEFLATED) zipOutput = ZipFile(zipfilename, 'w', ZIP_DEFLATED)
if isepub: if isepub:
@@ -1223,13 +1241,13 @@ def checkTools(source):
source = source.upper() source = source.upper()
if source.endswith('.CB7') or source.endswith('.7Z') or source.endswith('.RAR') or source.endswith('.CBR') or \ if source.endswith('.CB7') or source.endswith('.7Z') or source.endswith('.RAR') or source.endswith('.CBR') or \
source.endswith('.ZIP') or source.endswith('.CBZ'): source.endswith('.ZIP') or source.endswith('.CBZ'):
if '7z' not in available_archive_tools(): if SEVENZIP not in available_archive_tools():
print('ERROR: 7z is missing!') print('ERROR: 7z is missing!')
sys.exit(1) sys.exit(1)
if options.format == 'MOBI': if options.format == 'MOBI':
try: try:
subprocess_run(['kindlegen', '-locale', 'en'], stdout=PIPE, stderr=STDOUT, check=True) subprocess_run(['kindlegen', '-locale', 'en'], stdout=PIPE, stderr=STDOUT, check=True)
except FileNotFoundError: except (FileNotFoundError, CalledProcessError):
print('ERROR: KindleGen is missing!') print('ERROR: KindleGen is missing!')
sys.exit(1) sys.exit(1)
@@ -1300,6 +1318,7 @@ def makeBook(source, qtgui=None):
path = getWorkFolder(source) path = getWorkFolder(source)
print("Checking images...") print("Checking images...")
getComicInfo(os.path.join(path, "OEBPS", "Images"), source) getComicInfo(os.path.join(path, "OEBPS", "Images"), source)
removeNonImages(os.path.join(path, "OEBPS", "Images"))
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source) detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images')) chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
cover = image.Cover(cover_path, options) cover = image.Cover(cover_path, options)

View File

@@ -18,7 +18,7 @@
# PERFORMANCE OF THIS SOFTWARE. # PERFORMANCE OF THIS SOFTWARE.
# #
from functools import cached_property from functools import cached_property, lru_cache
import os import os
import platform import platform
import distro import distro
@@ -28,6 +28,7 @@ from xml.parsers.expat import ExpatError
from .shared import subprocess_run from .shared import subprocess_run
EXTRACTION_ERROR = 'Failed to extract archive. Try extracting file outside of KCC.' EXTRACTION_ERROR = 'Failed to extract archive. Try extracting file outside of KCC.'
SEVENZIP = '7zz' if platform.system() == 'Darwin' else '7z'
class ComicArchive: class ComicArchive:
@@ -39,7 +40,7 @@ class ComicArchive:
@cached_property @cached_property
def type(self): def type(self):
extraction_commands = [ extraction_commands = [
['7z', 'l', '-y', '-p1', self.filepath], [SEVENZIP, 'l', '-y', '-p1', self.filepath],
] ]
if distro.id() == 'fedora' or distro.like() == 'fedora': if distro.id() == 'fedora' or distro.like() == 'fedora':
@@ -68,7 +69,7 @@ class ComicArchive:
extraction_commands = [ extraction_commands = [
['tar', '--exclude', '__MACOSX', '--exclude', '.DS_Store', '--exclude', 'thumbs.db', '--exclude', 'Thumbs.db', '-xf', self.filepath, '-C', targetdir], ['tar', '--exclude', '__MACOSX', '--exclude', '.DS_Store', '--exclude', 'thumbs.db', '--exclude', 'Thumbs.db', '-xf', self.filepath, '-C', targetdir],
['7z', 'x', '-y', '-xr!__MACOSX', '-xr!.DS_Store', '-xr!thumbs.db', '-xr!Thumbs.db', '-o' + targetdir, self.filepath], [SEVENZIP, 'x', '-y', '-xr!__MACOSX', '-xr!.DS_Store', '-xr!thumbs.db', '-xr!Thumbs.db', '-o' + targetdir, self.filepath],
] ]
if platform.system() == 'Darwin': if platform.system() == 'Darwin':
@@ -100,13 +101,13 @@ class ComicArchive:
def addFile(self, sourcefile): def addFile(self, sourcefile):
if self.type in ['RAR', 'RAR5']: if self.type in ['RAR', 'RAR5']:
raise NotImplementedError raise NotImplementedError
process = subprocess_run(['7z', 'a', '-y', self.filepath, sourcefile], process = subprocess_run([SEVENZIP, 'a', '-y', self.filepath, sourcefile],
stdout=PIPE, stderr=STDOUT) stdout=PIPE, stderr=STDOUT)
if process.returncode != 0: if process.returncode != 0:
raise OSError('Failed to add the file.') raise OSError('Failed to add the file.')
def extractMetadata(self): def extractMetadata(self):
process = subprocess_run(['7z', 'x', '-y', '-so', self.filepath, 'ComicInfo.xml'], process = subprocess_run([SEVENZIP, 'x', '-y', '-so', self.filepath, 'ComicInfo.xml'],
stdout=PIPE, stderr=STDOUT) stdout=PIPE, stderr=STDOUT)
if process.returncode != 0: if process.returncode != 0:
raise OSError(EXTRACTION_ERROR) raise OSError(EXTRACTION_ERROR)
@@ -114,3 +115,16 @@ class ComicArchive:
return parseString(process.stdout) return parseString(process.stdout)
except ExpatError: except ExpatError:
return None return None
@lru_cache
def available_archive_tools():
available = []
for tool in ['tar', SEVENZIP, 'unar', 'unrar']:
try:
subprocess_run([tool], stdout=PIPE, stderr=STDOUT)
available.append(tool)
except (FileNotFoundError, CalledProcessError):
pass
return available

View File

@@ -20,7 +20,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import io import io
import os import os
import numpy as np
from pathlib import Path from pathlib import Path
from functools import cached_property
import mozjpeg_lossless_optimization import mozjpeg_lossless_optimization
from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter, ImageDraw from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter, ImageDraw
from .page_number_crop_alg import get_bbox_crop_margin_page_number, get_bbox_crop_margin from .page_number_crop_alg import get_bbox_crop_margin_page_number, get_bbox_crop_margin
@@ -146,11 +148,9 @@ class ComicPageParser:
# Detect corruption in source image, let caller catch any exceptions triggered. # Detect corruption in source image, let caller catch any exceptions triggered.
srcImgPath = os.path.join(source[0], source[1]) srcImgPath = os.path.join(source[0], source[1])
Image.open(srcImgPath).verify()
self.image = Image.open(srcImgPath) self.image = Image.open(srcImgPath)
self.image.verify()
self.image = Image.open(srcImgPath).convert('RGB')
self.color = self.colorCheck()
self.fill = self.fillCheck() self.fill = self.fillCheck()
# backwards compatibility for Pillow >9.1.0 # backwards compatibility for Pillow >9.1.0
if not hasattr(Image, 'Resampling'): if not hasattr(Image, 'Resampling'):
@@ -181,13 +181,13 @@ class ComicPageParser:
new_image = Image.new("RGB", (int(width / 2), int(height*2))) new_image = Image.new("RGB", (int(width / 2), int(height*2)))
new_image.paste(pageone, (0, 0)) new_image.paste(pageone, (0, 0))
new_image.paste(pagetwo, (0, height)) new_image.paste(pagetwo, (0, height))
self.payload.append(['N', self.source, new_image, self.color, self.fill]) self.payload.append(['N', self.source, new_image, self.fill])
elif (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth \ elif (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth \
and not self.opt.webtoon and self.opt.splitter == 1: and not self.opt.webtoon and self.opt.splitter == 1:
spread = self.image spread = self.image
if not self.opt.norotate: if not self.opt.norotate:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True) spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
self.payload.append(['R', self.source, spread, self.color, self.fill]) self.payload.append(['R', self.source, spread, self.fill])
elif (width > height) != (dstwidth > dstheight) and not self.opt.webtoon: elif (width > height) != (dstwidth > dstheight) and not self.opt.webtoon:
if self.opt.splitter != 1: if self.opt.splitter != 1:
if width > height: if width > height:
@@ -202,38 +202,15 @@ class ComicPageParser:
else: else:
pageone = self.image.crop(leftbox) pageone = self.image.crop(leftbox)
pagetwo = self.image.crop(rightbox) pagetwo = self.image.crop(rightbox)
self.payload.append(['S1', self.source, pageone, self.color, self.fill]) self.payload.append(['S1', self.source, pageone, self.fill])
self.payload.append(['S2', self.source, pagetwo, self.color, self.fill]) self.payload.append(['S2', self.source, pagetwo, self.fill])
if self.opt.splitter > 0: if self.opt.splitter > 0:
spread = self.image spread = self.image
if not self.opt.norotate: if not self.opt.norotate:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True) spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
self.payload.append(['R', self.source, spread, self.payload.append(['R', self.source, spread, self.fill])
self.color, self.fill])
else: else:
self.payload.append(['N', self.source, self.image, self.color, self.fill]) self.payload.append(['N', self.source, self.image, self.fill])
def colorCheck(self):
if self.opt.webtoon:
return True
else:
img = self.image.copy()
bands = img.getbands()
if bands == ('R', 'G', 'B') or bands == ('R', 'G', 'B', 'A'):
thumb = img.resize((40, 40))
SSE, bias = 0, [0, 0, 0]
bias = ImageStat.Stat(thumb).mean[:3]
bias = [b - sum(bias) / 3 for b in bias]
for pixel in thumb.getdata():
mu = sum(pixel) / 3
SSE += sum((pixel[i] - mu - bias[i]) * (pixel[i] - mu - bias[i]) for i in [0, 1, 2])
MSE = float(SSE) / (40 * 40)
if MSE > 22:
return True
else:
return False
else:
return False
def fillCheck(self): def fillCheck(self):
if self.opt.bordersColor: if self.opt.bordersColor:
@@ -275,14 +252,14 @@ class ComicPageParser:
class ComicPage: class ComicPage:
def __init__(self, options, mode, path, image, color, fill): def __init__(self, options, mode, path, image, fill):
self.opt = options self.opt = options
_, self.size, self.palette, self.gamma = self.opt.profileData _, self.size, self.palette, self.gamma = self.opt.profileData
if self.opt.hq: if self.opt.hq:
self.size = (int(self.size[0] * 1.5), int(self.size[1] * 1.5)) self.size = (int(self.size[0] * 1.5), int(self.size[1] * 1.5))
self.kindle_scribe_azw3 = (options.profile == 'KS') and (options.format in ('MOBI', 'EPUB')) self.kindle_scribe_azw3 = (options.profile == 'KS') and (options.format in ('MOBI', 'EPUB'))
self.image = image self.original_color_mode = image.mode
self.color = color self.image = image.convert("RGB")
self.fill = fill self.fill = fill
self.rotated = False self.rotated = False
self.orgPath = os.path.join(path[0], path[1]) self.orgPath = os.path.join(path[0], path[1])
@@ -301,11 +278,29 @@ class ComicPage:
if not hasattr(Image, 'Resampling'): if not hasattr(Image, 'Resampling'):
Image.Resampling = Image Image.Resampling = Image
@cached_property
def color(self):
if self.original_color_mode in ("L", "1"):
return False
img = self.image.convert("YCbCr")
_, cb, cr = img.split()
cb_hist = cb.histogram()
cr_hist = cr.histogram()
cb_nonzero = [i for i, e in enumerate(cb_hist) if e]
cr_nonzero = [i for i, e in enumerate(cr_hist) if e]
cb_spread = cb_nonzero[-1] - cb_nonzero[0] if len(cb_nonzero) else 0
cr_spread = cr_nonzero[-1] - cr_nonzero[0] if len(cr_nonzero) else 0
SPREAD_THRESHOLD=20
if cb_spread < SPREAD_THRESHOLD and cr_spread < SPREAD_THRESHOLD:
return False
else:
return True
def saveToDir(self): def saveToDir(self):
try: try:
flags = [] flags = []
if not self.opt.forcecolor and not self.opt.forcepng:
self.image = self.image.convert('L')
if self.rotated: if self.rotated:
flags.append('Rotated') flags.append('Rotated')
if self.fill != 'white': if self.fill != 'white':
@@ -327,8 +322,12 @@ class ComicPage:
def save_with_codec(self, image, targetPath): def save_with_codec(self, image, targetPath):
if self.opt.forcepng: if self.opt.forcepng:
image.info["transparency"] = None image.info["transparency"] = None
targetPath += '.png' if self.opt.iskindle and ('MOBI' in self.opt.format or 'EPUB' in self.opt.format):
image.save(targetPath, 'PNG', optimize=1) targetPath += '.gif'
image.save(targetPath, 'GIF', optimize=1, interlace=False)
else:
targetPath += '.png'
image.save(targetPath, 'PNG', optimize=1)
else: else:
targetPath += '.jpg' targetPath += '.jpg'
if self.opt.mozjpeg: if self.opt.mozjpeg:
@@ -353,15 +352,16 @@ class ComicPage:
else: else:
self.image = ImageOps.autocontrast(Image.eval(self.image, lambda a: int(255 * (a / 255.) ** gamma))) self.image = ImageOps.autocontrast(Image.eval(self.image, lambda a: int(255 * (a / 255.) ** gamma)))
def convertToGrayscale(self):
self.image = self.image.convert('L')
def quantizeImage(self): def quantizeImage(self):
colors = len(self.palette) // 3 # remove all color pixels from image, since colorCheck() has some tolerance
if colors < 256: # quantize with a small number of color pixels in a mostly b/w image can have unexpected results
self.palette += self.palette[:3] * (256 - colors) self.image = self.image.convert("L").convert("RGB")
palImg = Image.new('P', (1, 1)) palImg = Image.new('P', (1, 1))
palImg.putpalette(self.palette) palImg.putpalette(self.palette)
self.image = self.image.convert('L')
self.image = self.image.convert('RGB')
# Quantize is deprecated but new function call it internally anyway...
self.image = self.image.quantize(palette=palImg) self.image = self.image.quantize(palette=palImg)
def optimizeForDisplay(self, reducerainbow): def optimizeForDisplay(self, reducerainbow):

View File

@@ -18,9 +18,7 @@
# PERFORMANCE OF THIS SOFTWARE. # PERFORMANCE OF THIS SOFTWARE.
# #
from functools import lru_cache
import os import os
from hashlib import md5
from html.parser import HTMLParser from html.parser import HTMLParser
import subprocess import subprocess
from packaging.version import Version from packaging.version import Version
@@ -50,12 +48,6 @@ class HTMLStripper(HTMLParser):
def getImageFileName(imgfile): def getImageFileName(imgfile):
name, ext = os.path.splitext(imgfile) name, ext = os.path.splitext(imgfile)
ext = ext.lower() ext = ext.lower()
if (name.startswith('.') and len(name) == 1):
return None
if name.startswith('._'):
return None
if ext not in ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2', '.j2k', '.jpx']:
return None
return [name, ext] return [name, ext]
@@ -132,19 +124,6 @@ def dependencyCheck(level):
print('ERROR: ' + ', '.join(missing) + ' is not installed!') print('ERROR: ' + ', '.join(missing) + ' is not installed!')
sys.exit(1) sys.exit(1)
@lru_cache
def available_archive_tools():
available = []
for tool in ['tar', '7z', 'unar', 'unrar']:
try:
subprocess_run([tool], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
available.append(tool)
except FileNotFoundError:
pass
return available
def subprocess_run(command, **kwargs): def subprocess_run(command, **kwargs):
if (os.name == 'nt'): if (os.name == 'nt'):
kwargs.setdefault('creationflags', subprocess.CREATE_NO_WINDOW) kwargs.setdefault('creationflags', subprocess.CREATE_NO_WINDOW)

View File

@@ -5,7 +5,7 @@ requests>=2.31.0
python-slugify>=1.2.1 python-slugify>=1.2.1
raven>=6.0.0 raven>=6.0.0
packaging>=23.2 packaging>=23.2
mozjpeg-lossless-optimization==1.2.0 mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0 natsort>=8.4.0
distro>=1.8.0 distro>=1.8.0
numpy>=1.22.4 numpy>=1.22.4