1
0
mirror of https://github.com/ciromattia/kcc synced 2026-04-18 15:08:48 +00:00

Compare commits

...

14 Commits

Author SHA1 Message Date
Alex Xu
b35a2baf05 bump 7.2.1 2025-02-20 18:24:53 -08:00
Alex Xu
11a395e983 fix pdf workdir (#830) 2025-02-20 18:24:06 -08:00
Alex Xu
2e39a8c227 add jpeg2000 (#828) 2025-02-19 16:29:26 -08:00
Alex Xu
02535421a0 rename batch to chunk (#826) 2025-02-12 15:31:40 -08:00
Alex Xu
3d4fae62d8 reduce rainbow checkbox (#824) 2025-02-09 19:10:56 -08:00
Alex Xu
2b550b8b98 bump to 7.2.0 2025-02-06 14:08:11 -08:00
Alex Xu
ecee7cf6f5 don't sanitize twice 2025-02-06 14:07:09 -08:00
Alex Xu
b0a5558da1 write temp files next to source instead of on main ssd (#820)
* extract to same folder

* rename split to batch

* write temp files to source
2025-01-30 12:42:02 -08:00
Alex Xu
1b487c18d6 with certain file structures: fix large file chunking, ComicInfo.xml, permissions (#819)
* fix nested folder extraction

* add comicinfo.xml handling

* sanitize

* add error handling

* space
2025-01-30 11:31:33 -08:00
Alex Xu
a3546d19c3 add build_binary commands to readme (#818) 2025-01-29 19:14:08 -08:00
X5Games / Neyney10
2f703ef92c Inter-panel cropping method. (#810)
* Inter-panel cropping method.

* 1. Save interpanelcrop option.
2. Update readme with the the new interpanelcrop argument.
3. Add a tooltip to the inter-panel crop box.
2025-01-27 13:44:23 -08:00
Alex Xu
4fb993b38b add example new checkbox PR (#815) 2025-01-27 13:42:06 -08:00
Alex Xu
1401f94c1f reorganize imports to match autogenerated files (#813)
* reorganize imports of QtGui and more

* remove unused imports
2025-01-23 09:36:03 -08:00
X5Games / Neyney10
70d10204ee Update UI to match Qt Creator 15.0. (#809) 2025-01-16 13:33:54 -08:00
13 changed files with 514 additions and 343 deletions

View File

@@ -163,6 +163,8 @@ PROCESSING:
Set cropping power [Default=1.0] Set cropping power [Default=1.0]
--cm CROPPINGM, --croppingminimum CROPPINGM --cm CROPPINGM, --croppingminimum CROPPINGM
Set cropping minimum area ratio [Default=0.0] Set cropping minimum area ratio [Default=0.0]
--ipc INTERPANELCROP, --interpanelcrop INTERPANELCROP
Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0]
--blackborders Disable autodetection and force black borders --blackborders Disable autodetection and force black borders
--whiteborders Disable autodetection and force white borders --whiteborders Disable autodetection and force white borders
--forcecolor Don't convert images to grayscale --forcecolor Don't convert images to grayscale
@@ -185,6 +187,7 @@ OUTPUT SETTINGS:
Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0] Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0]
--spreadshift Shift first page to opposite side in landscape for two page spread alignment --spreadshift Shift first page to opposite side in landscape for two page spread alignment
--norotate Do not rotate double page spreads in spread splitter option. --norotate Do not rotate double page spreads in spread splitter option.
--reducerainbow Reduce rainbow effect on color eink by slightly blurring images
CUSTOM PROFILE: CUSTOM PROFILE:
--customwidth CUSTOMWIDTH --customwidth CUSTOMWIDTH
@@ -229,6 +232,8 @@ If you want to edit the code, a good code editor is [VS Code](https://code.visua
If you want to edit the `.ui` files, use [Qt Creator](https://www.qt.io/download-qt-installer-oss), included in **Qt for desktop development**. If you want to edit the `.ui` files, use [Qt Creator](https://www.qt.io/download-qt-installer-oss), included in **Qt for desktop development**.
Then use the `gen_ui_files` scripts to autogenerate the python UI. Then use the `gen_ui_files` scripts to autogenerate the python UI.
An example PR adding a new checkbox is here: https://github.com/ciromattia/kcc/pull/785
### Windows install from source ### Windows install from source
@@ -247,6 +252,12 @@ venv\Scripts\activate.bat
python kcc.py python kcc.py
``` ```
You can build a `.exe` of KCC like the downloads we offer with
```
python setup.py build_binary
```
### macOS install from source ### macOS install from source
One time setup and running for the first time: One time setup and running for the first time:
@@ -264,6 +275,12 @@ source venv/bin/activate
python kcc.py python kcc.py
``` ```
You can build a `.app` of KCC like the downloads we offer with
```
python setup.py build_binary
```
## CREDITS ## CREDITS
**KCC** is made by **KCC** is made by

View File

@@ -1,3 +1,3 @@
pyside6-uic gui/KCC.ui > kindlecomicconverter/KCC_ui.py pyside6-uic gui/KCC.ui --from-imports > kindlecomicconverter/KCC_ui.py
pyside6-uic gui/MetaEditor.ui > kindlecomicconverter/KCC_ui_editor.py pyside6-uic gui/MetaEditor.ui --from-imports > kindlecomicconverter/KCC_ui_editor.py
pyside6-rcc gui/KCC.qrc > kindlecomicconverter/KCC_rc.py pyside6-rcc gui/KCC.qrc > kindlecomicconverter/KCC_rc.py

View File

@@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>450</width> <width>482</width>
<height>400</height> <height>448</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@@ -37,36 +37,6 @@
<property name="bottomMargin"> <property name="bottomMargin">
<number>0</number> <number>0</number>
</property> </property>
<item row="5" column="1">
<widget class="QCheckBox" name="deleteBox">
<property name="toolTip">
<string>Delete input file(s) or directory. It's not recoverable!</string>
</property>
<property name="text">
<string>Delete input</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="mangaBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Enable right-to-left reading.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Manga mode</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="outputSplit">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Automatic mode&lt;br/&gt;&lt;/span&gt;The output will be split automatically.&lt;/p&gt;&lt;p style='white-space:pre'&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Volume mode&lt;br/&gt;&lt;/span&gt;Every subdirectory will be considered as a separate volume.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Output split</string>
</property>
</widget>
</item>
<item row="4" column="2"> <item row="4" column="2">
<widget class="QCheckBox" name="croppingBox"> <widget class="QCheckBox" name="croppingBox">
<property name="toolTip"> <property name="toolTip">
@@ -80,39 +50,23 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="0"> <item row="1" column="0">
<widget class="QCheckBox" name="mozJpegBox"> <widget class="QCheckBox" name="mangaBox">
<property name="toolTip"> <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 - JPEG&lt;br/&gt;&lt;/span&gt;Use JPEG files&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - force PNG&lt;br/&gt;&lt;/span&gt;Create PNG files instead JPEG&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - mozJpeg&lt;br/&gt;&lt;/span&gt;10-20% smaller JPEG file, with the same image quality, but processing time multiplied by 2&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;Enable right-to-left reading.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
<property name="text"> <property name="text">
<string>JPEG/PNG/mozJpeg</string> <string>Manga mode</string>
</property>
<property name="tristate">
<bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="2" column="0">
<widget class="QCheckBox" name="upscaleBox"> <widget class="QCheckBox" name="webtoonBox">
<property name="toolTip"> <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 - Nothing&lt;br/&gt;&lt;/span&gt;Images smaller than device resolution will not be resized.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - Stretching&lt;br/&gt;&lt;/span&gt;Images smaller than device resolution will be resized. Aspect ratio will be not preserved.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Upscaling&lt;br/&gt;&lt;/span&gt;Images smaller than device resolution will be resized. Aspect ratio will be preserved.&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;Enable special parsing mode for Korean Webtoons.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
<property name="text"> <property name="text">
<string>Stretch/Upscale</string> <string>Webtoon mode</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QCheckBox" name="colorBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Disable conversion to grayscale.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Color mode</string>
</property> </property>
</widget> </widget>
</item> </item>
@@ -129,46 +83,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="2">
<widget class="QCheckBox" name="gammaBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Disable automatic gamma correction.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Custom gamma</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="spreadShiftBox">
<property name="toolTip">
<string>Shift first page to opposite side in landscape for two page spread alignment</string>
</property>
<property name="text">
<string>Spread shift</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="webtoonBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Enable special parsing mode for Korean Webtoons.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Webtoon mode</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QCheckBox" name="maximizeStrips">
<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 - 1x4&lt;br/&gt;&lt;/span&gt;Keep format 1x4 panels strips.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - 2x2&lt;br/&gt;&lt;/span&gt;Turn 1x4 strips to 2x2 to maximize screen usage.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>1x4 to 2x2 strips</string>
</property>
</widget>
</item>
<item row="3" column="0"> <item row="3" column="0">
<widget class="QCheckBox" name="borderBox"> <widget class="QCheckBox" name="borderBox">
<property name="toolTip"> <property name="toolTip">
@@ -182,13 +96,36 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="1"> <item row="2" column="2">
<widget class="QCheckBox" name="noRotateBox"> <widget class="QCheckBox" name="gammaBox">
<property name="toolTip"> <property name="toolTip">
<string>Do not rotate double page spreads in spread splitter option.</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Disable automatic gamma correction.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
<property name="text"> <property name="text">
<string>No rotate</string> <string>Custom gamma</string>
</property>
</widget>
</item>
<item row="6" column="2">
<widget class="QCheckBox" name="interPanelCropBox">
<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 - Disabled&lt;br/&gt;&lt;/span&gt;Disabled&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - Horizontal&lt;br/&gt;&lt;/span&gt;Crop empty horizontal lines.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Both&lt;br/&gt;&lt;/span&gt;Crop empty horizontal and vertical lines.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Inter-panel crop</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QCheckBox" name="colorBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Disable conversion to grayscale.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Color mode</string>
</property> </property>
</widget> </widget>
</item> </item>
@@ -215,6 +152,16 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1">
<widget class="QCheckBox" name="maximizeStrips">
<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 - 1x4&lt;br/&gt;&lt;/span&gt;Keep format 1x4 panels strips.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - 2x2&lt;br/&gt;&lt;/span&gt;Turn 1x4 strips to 2x2 to maximize screen usage.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>1x4 to 2x2 strips</string>
</property>
</widget>
</item>
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLineEdit" name="authorEdit"> <widget class="QLineEdit" name="authorEdit">
<property name="sizePolicy"> <property name="sizePolicy">
@@ -224,7 +171,7 @@
</sizepolicy> </sizepolicy>
</property> </property>
<property name="focusPolicy"> <property name="focusPolicy">
<enum>Qt::ClickFocus</enum> <enum>Qt::FocusPolicy::ClickFocus</enum>
</property> </property>
<property name="toolTip"> <property name="toolTip">
<string>Default Author is KCC</string> <string>Default Author is KCC</string>
@@ -237,6 +184,82 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="1">
<widget class="QCheckBox" name="deleteBox">
<property name="toolTip">
<string>Delete input file(s) or directory. It's not recoverable!</string>
</property>
<property name="text">
<string>Delete input</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="mozJpegBox">
<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 - JPEG&lt;br/&gt;&lt;/span&gt;Use JPEG files&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - force PNG&lt;br/&gt;&lt;/span&gt;Create PNG files instead JPEG&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - mozJpeg&lt;br/&gt;&lt;/span&gt;10-20% smaller JPEG file, with the same image quality, but processing time multiplied by 2&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>JPEG/PNG/mozJpeg</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="spreadShiftBox">
<property name="toolTip">
<string>Shift first page to opposite side in landscape for two page spread alignment</string>
</property>
<property name="text">
<string>Spread shift</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="upscaleBox">
<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 - Nothing&lt;br/&gt;&lt;/span&gt;Images smaller than device resolution will not be resized.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - Stretching&lt;br/&gt;&lt;/span&gt;Images smaller than device resolution will be resized. Aspect ratio will be not preserved.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Upscaling&lt;br/&gt;&lt;/span&gt;Images smaller than device resolution will be resized. Aspect ratio will be preserved.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Stretch/Upscale</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="outputSplit">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Automatic mode&lt;br/&gt;&lt;/span&gt;The output will be split automatically.&lt;/p&gt;&lt;p style='white-space:pre'&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Volume mode&lt;br/&gt;&lt;/span&gt;Every subdirectory will be considered as a separate volume.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Output split</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QCheckBox" name="noRotateBox">
<property name="toolTip">
<string>Do not rotate double page spreads in spread splitter option.</string>
</property>
<property name="text">
<string>No rotate</string>
</property>
</widget>
</item>
<item row="7" column="2">
<widget class="QCheckBox" name="reduceRainbowBox">
<property name="toolTip">
<string>Reduce rainbow effect on color eink by slightly blurring images</string>
</property>
<property name="text">
<string>Reduce Rainbow</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@@ -274,7 +297,7 @@
<number>5</number> <number>5</number>
</property> </property>
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
</widget> </widget>
</item> </item>
@@ -315,7 +338,7 @@
<number>1</number> <number>1</number>
</property> </property>
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
</widget> </widget>
</item> </item>
@@ -521,13 +544,13 @@
<string notr="true"/> <string notr="true"/>
</property> </property>
<property name="selectionMode"> <property name="selectionMode">
<enum>QAbstractItemView::NoSelection</enum> <enum>QAbstractItemView::SelectionMode::NoSelection</enum>
</property> </property>
<property name="verticalScrollMode"> <property name="verticalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum> <enum>QAbstractItemView::ScrollMode::ScrollPerPixel</enum>
</property> </property>
<property name="horizontalScrollMode"> <property name="horizontalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum> <enum>QAbstractItemView::ScrollMode::ScrollPerPixel</enum>
</property> </property>
</widget> </widget>
</item> </item>
@@ -548,7 +571,7 @@
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="alignment"> <property name="alignment">
<set>Qt::AlignJustify|Qt::AlignVCenter</set> <set>Qt::AlignmentFlag::AlignJustify|Qt::AlignmentFlag::AlignVCenter</set>
</property> </property>
</widget> </widget>
</item> </item>

View File

@@ -16,6 +16,11 @@
# 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.
from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings)
from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices)
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QApplication, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog)
from PySide6.QtNetwork import (QLocalSocket, QLocalServer)
import os import os
import re import re
import sys import sys
@@ -25,9 +30,6 @@ from shutil import move, rmtree
from subprocess import STDOUT, PIPE from subprocess import STDOUT, PIPE
import requests import requests
# noinspection PyUnresolvedReferences
from PySide6 import QtGui, QtCore, QtWidgets, QtNetwork
from PySide6.QtCore import Qt
from xml.sax.saxutils import escape from xml.sax.saxutils import escape
from psutil import Process from psutil import Process
from copy import copy from copy import copy
@@ -44,18 +46,18 @@ from . import KCC_ui
from . import KCC_ui_editor from . import KCC_ui_editor
class QApplicationMessaging(QtWidgets.QApplication): class QApplicationMessaging(QApplication):
messageFromOtherInstance = QtCore.Signal(bytes) messageFromOtherInstance = Signal(bytes)
def __init__(self, argv): def __init__(self, argv):
QtWidgets.QApplication.__init__(self, argv) QApplication.__init__(self, argv)
self._key = 'KCC' self._key = 'KCC'
self._timeout = 1000 self._timeout = 1000
self._locked = False self._locked = False
socket = QtNetwork.QLocalSocket(self) socket = QLocalSocket(self)
socket.connectToServer(self._key, QtCore.QIODeviceBase.OpenModeFlag.WriteOnly) socket.connectToServer(self._key, QIODeviceBase.OpenModeFlag.WriteOnly)
if not socket.waitForConnected(self._timeout): if not socket.waitForConnected(self._timeout):
self._server = QtNetwork.QLocalServer(self) self._server = QLocalServer(self)
self._server.newConnection.connect(self.handleMessage) self._server.newConnection.connect(self.handleMessage)
self._server.listen(self._key) self._server.listen(self._key)
else: else:
@@ -67,11 +69,11 @@ class QApplicationMessaging(QtWidgets.QApplication):
self._server.close() self._server.close()
def event(self, e): def event(self, e):
if e.type() == QtCore.QEvent.Type.FileOpen: if e.type() == QEvent.Type.FileOpen:
self.messageFromOtherInstance.emit(bytes(e.file(), 'UTF-8')) self.messageFromOtherInstance.emit(bytes(e.file(), 'UTF-8'))
return True return True
else: else:
return QtWidgets.QApplication.event(self, e) return QApplication.event(self, e)
def isRunning(self): def isRunning(self):
return self._locked return self._locked
@@ -82,56 +84,56 @@ class QApplicationMessaging(QtWidgets.QApplication):
self.messageFromOtherInstance.emit(socket.readAll().data()) self.messageFromOtherInstance.emit(socket.readAll().data())
def sendMessage(self, message): def sendMessage(self, message):
socket = QtNetwork.QLocalSocket(self) socket = QLocalSocket(self)
socket.connectToServer(self._key, QtCore.QIODeviceBase.OpenModeFlag.WriteOnly) socket.connectToServer(self._key, QIODeviceBase.OpenModeFlag.WriteOnly)
socket.waitForConnected(self._timeout) socket.waitForConnected(self._timeout)
socket.write(bytes(message, 'UTF-8')) socket.write(bytes(message, 'UTF-8'))
socket.waitForBytesWritten(self._timeout) socket.waitForBytesWritten(self._timeout)
socket.disconnectFromServer() socket.disconnectFromServer()
class QMainWindowKCC(QtWidgets.QMainWindow): class QMainWindowKCC(QMainWindow):
progressBarTick = QtCore.Signal(str) progressBarTick = Signal(str)
modeConvert = QtCore.Signal(int) modeConvert = Signal(int)
addMessage = QtCore.Signal(str, str, bool) addMessage = Signal(str, str, bool)
addTrayMessage = QtCore.Signal(str, str) addTrayMessage = Signal(str, str)
showDialog = QtCore.Signal(str, str) showDialog = Signal(str, str)
hideProgressBar = QtCore.Signal() hideProgressBar = Signal()
forceShutdown = QtCore.Signal() forceShutdown = Signal()
class Icons: class Icons:
def __init__(self): def __init__(self):
self.deviceKindle = QtGui.QIcon() self.deviceKindle = QIcon()
self.deviceKindle.addPixmap(QtGui.QPixmap(":/Devices/icons/Kindle.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.deviceKindle.addPixmap(QPixmap(":/Devices/icons/Kindle.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.deviceKobo = QtGui.QIcon() self.deviceKobo = QIcon()
self.deviceKobo.addPixmap(QtGui.QPixmap(":/Devices/icons/Kobo.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.deviceKobo.addPixmap(QPixmap(":/Devices/icons/Kobo.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.deviceRmk = QtGui.QIcon() self.deviceRmk = QIcon()
self.deviceRmk.addPixmap(QtGui.QPixmap(":/Devices/icons/Rmk.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.deviceRmk.addPixmap(QPixmap(":/Devices/icons/Rmk.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.deviceOther = QtGui.QIcon() self.deviceOther = QIcon()
self.deviceOther.addPixmap(QtGui.QPixmap(":/Devices/icons/Other.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.deviceOther.addPixmap(QPixmap(":/Devices/icons/Other.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.MOBIFormat = QtGui.QIcon() self.MOBIFormat = QIcon()
self.MOBIFormat.addPixmap(QtGui.QPixmap(":/Formats/icons/MOBI.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.MOBIFormat.addPixmap(QPixmap(":/Formats/icons/MOBI.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.CBZFormat = QtGui.QIcon() self.CBZFormat = QIcon()
self.CBZFormat.addPixmap(QtGui.QPixmap(":/Formats/icons/CBZ.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.CBZFormat.addPixmap(QPixmap(":/Formats/icons/CBZ.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.EPUBFormat = QtGui.QIcon() self.EPUBFormat = QIcon()
self.EPUBFormat.addPixmap(QtGui.QPixmap(":/Formats/icons/EPUB.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.EPUBFormat.addPixmap(QPixmap(":/Formats/icons/EPUB.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.info = QtGui.QIcon() self.info = QIcon()
self.info.addPixmap(QtGui.QPixmap(":/Status/icons/info.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.info.addPixmap(QPixmap(":/Status/icons/info.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.warning = QtGui.QIcon() self.warning = QIcon()
self.warning.addPixmap(QtGui.QPixmap(":/Status/icons/warning.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.warning.addPixmap(QPixmap(":/Status/icons/warning.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.error = QtGui.QIcon() self.error = QIcon()
self.error.addPixmap(QtGui.QPixmap(":/Status/icons/error.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.error.addPixmap(QPixmap(":/Status/icons/error.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.programIcon = QtGui.QIcon() self.programIcon = QIcon()
self.programIcon.addPixmap(QtGui.QPixmap(":/Icon/icons/comic2ebook.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) self.programIcon.addPixmap(QPixmap(":/Icon/icons/comic2ebook.png"), QIcon.Mode.Normal, QIcon.State.Off)
class VersionThread(QtCore.QThread): class VersionThread(QThread):
def __init__(self): def __init__(self):
QtCore.QThread.__init__(self) QThread.__init__(self)
self.newVersion = '' self.newVersion = ''
self.md5 = '' self.md5 = ''
self.barProgress = 0 self.barProgress = 0
@@ -160,9 +162,9 @@ class VersionThread(QtCore.QThread):
self.answer = dialoganswer self.answer = dialoganswer
class ProgressThread(QtCore.QThread): class ProgressThread(QThread):
def __init__(self): def __init__(self):
QtCore.QThread.__init__(self) QThread.__init__(self)
self.running = False self.running = False
self.content = None self.content = None
self.progress = 0 self.progress = 0
@@ -184,9 +186,9 @@ class ProgressThread(QtCore.QThread):
self.running = False self.running = False
class WorkerThread(QtCore.QThread): class WorkerThread(QThread):
def __init__(self): def __init__(self):
QtCore.QThread.__init__(self) QThread.__init__(self)
self.conversionAlive = False self.conversionAlive = False
self.errors = False self.errors = False
self.kindlegenErrorCode = [0] self.kindlegenErrorCode = [0]
@@ -242,6 +244,7 @@ class WorkerThread(QtCore.QThread):
options.cropping = GUI.croppingBox.checkState().value options.cropping = GUI.croppingBox.checkState().value
if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked: if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked:
options.croppingp = float(GUI.croppingPowerValue) options.croppingp = float(GUI.croppingPowerValue)
options.interpanelcrop = GUI.interPanelCropBox.checkState().value
if GUI.borderBox.checkState() == Qt.CheckState.PartiallyChecked: if GUI.borderBox.checkState() == Qt.CheckState.PartiallyChecked:
options.white_borders = True options.white_borders = True
elif GUI.borderBox.checkState() == Qt.CheckState.Checked: elif GUI.borderBox.checkState() == Qt.CheckState.Checked:
@@ -250,6 +253,8 @@ class WorkerThread(QtCore.QThread):
options.batchsplit = 2 options.batchsplit = 2
if GUI.colorBox.isChecked(): if GUI.colorBox.isChecked():
options.forcecolor = True options.forcecolor = True
if GUI.reduceRainbowBox.isChecked():
options.reducerainbow = True
if GUI.maximizeStrips.isChecked(): if GUI.maximizeStrips.isChecked():
options.maximizestrips = True options.maximizestrips = True
if GUI.disableProcessingBox.isChecked(): if GUI.disableProcessingBox.isChecked():
@@ -435,7 +440,7 @@ class WorkerThread(QtCore.QThread):
MW.modeConvert.emit(1) MW.modeConvert.emit(1)
class SystemTrayIcon(QtWidgets.QSystemTrayIcon): class SystemTrayIcon(QSystemTrayIcon):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
if self.isSystemTrayAvailable(): if self.isSystemTrayAvailable():
@@ -448,7 +453,7 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
MW.activateWindow() MW.activateWindow()
def addTrayMessage(self, message, icon): def addTrayMessage(self, message, icon):
icon = getattr(QtWidgets.QSystemTrayIcon.MessageIcon, icon) icon = getattr(QSystemTrayIcon.MessageIcon, icon)
if self.supportsMessages() and not MW.isActiveWindow(): if self.supportsMessages() and not MW.isActiveWindow():
self.showMessage('Kindle Comic Converter', message, icon) self.showMessage('Kindle Comic Converter', message, icon)
@@ -458,7 +463,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if self.needClean: if self.needClean:
self.needClean = False self.needClean = False
GUI.jobList.clear() GUI.jobList.clear()
dname = QtWidgets.QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath) 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('/', '\\')
@@ -471,10 +476,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.needClean = False self.needClean = False
GUI.jobList.clear() GUI.jobList.clear()
if self.tar or self.sevenzip: if self.tar or self.sevenzip:
fnames = QtWidgets.QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath, fnames = QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
'Comic (*.cbz *.cbr *.cb7 *.zip *.rar *.7z *.pdf);;All (*.*)') 'Comic (*.cbz *.cbr *.cb7 *.zip *.rar *.7z *.pdf);;All (*.*)')
else: else:
fnames = QtWidgets.QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath, fnames = QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
'Comic (*.pdf);;All (*.*)') 'Comic (*.pdf);;All (*.*)')
for fname in fnames[0]: for fname in fnames[0]:
if fname != '': if fname != '':
@@ -486,8 +491,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
def selectFileMetaEditor(self): def selectFileMetaEditor(self):
sname = '' sname = ''
if QtWidgets.QApplication.keyboardModifiers() == QtCore.Qt.ShiftModifier: if QApplication.keyboardModifiers() == Qt.ShiftModifier:
dname = QtWidgets.QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath) dname = QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath)
if dname != '': if dname != '':
sname = os.path.join(dname, 'ComicInfo.xml') sname = os.path.join(dname, 'ComicInfo.xml')
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
@@ -495,7 +500,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.lastPath = os.path.abspath(sname) self.lastPath = os.path.abspath(sname)
else: else:
if self.sevenzip: if self.sevenzip:
fname = QtWidgets.QFileDialog.getOpenFileName(MW, 'Select file', self.lastPath, fname = QFileDialog.getOpenFileName(MW, 'Select file', self.lastPath,
'Comic (*.cbz *.cbr *.cb7)') 'Comic (*.cbz *.cbr *.cb7)')
else: else:
fname = [''] fname = ['']
@@ -524,7 +529,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
def openWiki(self): def openWiki(self):
# noinspection PyCallByClass # noinspection PyCallByClass
QtGui.QDesktopServices.openUrl(QtCore.QUrl('https://github.com/ciromattia/kcc/wiki')) QDesktopServices.openUrl(QUrl('https://github.com/ciromattia/kcc/wiki'))
def modeChange(self, mode): def modeChange(self, mode):
if mode == 1: if mode == 1:
@@ -559,16 +564,16 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if enable == 1: if enable == 1:
self.conversionAlive = False self.conversionAlive = False
self.worker.sync() self.worker.sync()
icon = QtGui.QIcon() icon = QIcon()
icon.addPixmap(QtGui.QPixmap(":/Other/icons/convert.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) icon.addPixmap(QPixmap(":/Other/icons/convert.png"), QIcon.Mode.Normal, QIcon.State.Off)
GUI.convertButton.setIcon(icon) GUI.convertButton.setIcon(icon)
GUI.convertButton.setText('Convert') GUI.convertButton.setText('Convert')
GUI.centralWidget.setAcceptDrops(True) GUI.centralWidget.setAcceptDrops(True)
elif enable == 0: elif enable == 0:
self.conversionAlive = True self.conversionAlive = True
self.worker.sync() self.worker.sync()
icon = QtGui.QIcon() icon = QIcon()
icon.addPixmap(QtGui.QPixmap(":/Other/icons/clear.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) icon.addPixmap(QPixmap(":/Other/icons/clear.png"), QIcon.Mode.Normal, QIcon.State.Off)
GUI.convertButton.setIcon(icon) GUI.convertButton.setIcon(icon)
GUI.convertButton.setText('Abort') GUI.convertButton.setText('Abort')
GUI.centralWidget.setAcceptDrops(False) GUI.centralWidget.setAcceptDrops(False)
@@ -684,15 +689,15 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
def addMessage(self, message, icon, replace=False): def addMessage(self, message, icon, replace=False):
if icon != '': if icon != '':
icon = getattr(self.icons, icon) icon = getattr(self.icons, icon)
item = QtWidgets.QListWidgetItem(icon, ' ' + self.stripTags(message)) item = QListWidgetItem(icon, ' ' + self.stripTags(message))
else: else:
item = QtWidgets.QListWidgetItem(' ' + self.stripTags(message)) item = QListWidgetItem(' ' + self.stripTags(message))
if replace: if replace:
GUI.jobList.takeItem(GUI.jobList.count() - 1) GUI.jobList.takeItem(GUI.jobList.count() - 1)
# Due to lack of HTML support in QListWidgetItem we overlay text field with QLabel # Due to lack of HTML support in QListWidgetItem we overlay text field with QLabel
# We still fill original text field with transparent content to trigger creation of horizontal scrollbar # We still fill original text field with transparent content to trigger creation of horizontal scrollbar
item.setForeground(QtGui.QColor('transparent')) item.setForeground(QColor('transparent'))
label = QtWidgets.QLabel(message) label = QLabel(message)
label.setOpenExternalLinks(True) label.setOpenExternalLinks(True)
GUI.jobList.addItem(item) GUI.jobList.addItem(item)
GUI.jobList.setItemWidget(item, label) GUI.jobList.setItemWidget(item, label)
@@ -700,11 +705,11 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
def showDialog(self, message, kind): def showDialog(self, message, kind):
if kind == 'error': if kind == 'error':
QtWidgets.QMessageBox.critical(MW, 'KCC - Error', message, QtWidgets.QMessageBox.StandardButton.Ok) QMessageBox.critical(MW, 'KCC - Error', message, QMessageBox.StandardButton.Ok)
elif kind == 'question': elif kind == 'question':
GUI.versionCheck.setAnswer(QtWidgets.QMessageBox.question(MW, 'KCC - Question', message, GUI.versionCheck.setAnswer(QMessageBox.question(MW, 'KCC - Question', message,
QtWidgets.QMessageBox.Yes, QMessageBox.Yes,
QtWidgets.QMessageBox.No)) QMessageBox.No))
def updateProgressbar(self, command): def updateProgressbar(self, command):
if command == 'tick': if command == 'tick':
@@ -728,8 +733,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.conversionAlive = False self.conversionAlive = False
self.worker.sync() self.worker.sync()
else: else:
if QtWidgets.QApplication.keyboardModifiers() == QtCore.Qt.KeyboardModifier.ShiftModifier: if QApplication.keyboardModifiers() == Qt.KeyboardModifier.ShiftModifier:
dname = QtWidgets.QFileDialog.getExistingDirectory(MW, 'Select output directory', self.lastPath) dname = QFileDialog.getExistingDirectory(MW, 'Select output directory', self.lastPath)
if dname != '': if dname != '':
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
dname = dname.replace('/', '\\') dname = dname.replace('/', '\\')
@@ -787,11 +792,13 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'gammaBox': GUI.gammaBox.checkState().value, 'gammaBox': GUI.gammaBox.checkState().value,
'croppingBox': GUI.croppingBox.checkState().value, 'croppingBox': GUI.croppingBox.checkState().value,
'croppingPowerSlider': float(self.croppingPowerValue) * 100, 'croppingPowerSlider': float(self.croppingPowerValue) * 100,
'interPanelCropBox': GUI.interPanelCropBox.checkState().value,
'upscaleBox': GUI.upscaleBox.checkState().value, 'upscaleBox': GUI.upscaleBox.checkState().value,
'borderBox': GUI.borderBox.checkState().value, 'borderBox': GUI.borderBox.checkState().value,
'webtoonBox': GUI.webtoonBox.checkState().value, 'webtoonBox': GUI.webtoonBox.checkState().value,
'outputSplit': GUI.outputSplit.checkState().value, 'outputSplit': GUI.outputSplit.checkState().value,
'colorBox': GUI.colorBox.checkState().value, 'colorBox': GUI.colorBox.checkState().value,
'reduceRainbowBox': GUI.reduceRainbowBox.checkState().value,
'disableProcessingBox': GUI.disableProcessingBox.checkState().value, 'disableProcessingBox': GUI.disableProcessingBox.checkState().value,
'mozJpegBox': GUI.mozJpegBox.checkState().value, 'mozJpegBox': GUI.mozJpegBox.checkState().value,
'widthBox': GUI.widthBox.value(), 'widthBox': GUI.widthBox.value(),
@@ -874,7 +881,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.setupUi(MW) self.setupUi(MW)
self.editor = KCCGUI_MetaEditor() self.editor = KCCGUI_MetaEditor()
self.icons = Icons() self.icons = Icons()
self.settings = QtCore.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.lastDevice = self.settings.value('lastDevice', 0, type=int) self.lastDevice = self.settings.value('lastDevice', 0, type=int)
@@ -907,7 +914,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
elif sys.platform.startswith('darwin'): elif sys.platform.startswith('darwin'):
for element in ['editorButton', 'wikiButton', 'directoryButton', 'clearButton', 'fileButton', 'deviceBox', for element in ['editorButton', 'wikiButton', 'directoryButton', 'clearButton', 'fileButton', 'deviceBox',
'convertButton', 'formatBox']: 'convertButton', 'formatBox']:
getattr(GUI, element).setMinimumSize(QtCore.QSize(0, 0)) getattr(GUI, element).setMinimumSize(QSize(0, 0))
GUI.gridLayout.setContentsMargins(-1, -1, -1, -1) GUI.gridLayout.setContentsMargins(-1, -1, -1, -1)
for element in ['gridLayout_2', 'gridLayout_3', 'gridLayout_4', 'horizontalLayout', 'horizontalLayout_2']: for element in ['gridLayout_2', 'gridLayout_3', 'gridLayout_4', 'horizontalLayout', 'horizontalLayout_2']:
getattr(GUI, element).setContentsMargins(-1, 0, -1, 0) getattr(GUI, element).setContentsMargins(-1, 0, -1, 0)
@@ -1050,11 +1057,11 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"Kobo Mini/Touch", "Kobo Mini/Touch",
] ]
statusBarLabel = QtWidgets.QLabel('<b><a href="https://kcc.iosphe.re/">HOMEPAGE</a> - <a href="https://github.' statusBarLabel = QLabel('<b><a href="https://kcc.iosphe.re/">HOMEPAGE</a> - <a href="https://github.'
'com/ciromattia/kcc/blob/master/README.md#issues--new-features--donations">DO' 'com/ciromattia/kcc/blob/master/README.md#issues--new-features--donations">DO'
'NATE</a> - <a href="http://www.mobileread.com/forums/showthread.php?t=207461' 'NATE</a> - <a href="http://www.mobileread.com/forums/showthread.php?t=207461'
'">FORUM</a></b>') '">FORUM</a></b>')
statusBarLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) statusBarLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
statusBarLabel.setOpenExternalLinks(True) statusBarLabel.setOpenExternalLinks(True)
GUI.statusBar.addPermanentWidget(statusBarLabel, 1) GUI.statusBar.addPermanentWidget(statusBarLabel, 1)
@@ -1218,15 +1225,15 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog):
return escape(s.strip()) return escape(s.strip())
def __init__(self): def __init__(self):
self.ui = QtWidgets.QDialog() self.ui = QDialog()
self.parser = None self.parser = None
self.setupUi(self.ui) self.setupUi(self.ui)
self.ui.setWindowFlags(self.ui.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint) self.ui.setWindowFlags(self.ui.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
self.okButton.clicked.connect(self.saveData) self.okButton.clicked.connect(self.saveData)
self.cancelButton.clicked.connect(self.ui.close) self.cancelButton.clicked.connect(self.ui.close)
if sys.platform.startswith('linux'): if sys.platform.startswith('linux'):
self.ui.resize(450, 260) self.ui.resize(450, 260)
self.ui.setMinimumSize(QtCore.QSize(450, 260)) self.ui.setMinimumSize(QSize(450, 260))
elif sys.platform.startswith('darwin'): elif sys.platform.startswith('darwin'):
self.ui.resize(450, 310) self.ui.resize(450, 310)
self.ui.setMinimumSize(QtCore.QSize(450, 310)) self.ui.setMinimumSize(QSize(450, 310))

View File

@@ -11644,7 +11644,7 @@ qt_resource_struct = b"\
\x00\x00\x012\x00\x00\x00\x00\x00\x01\x00\x01yY\ \x00\x00\x012\x00\x00\x00\x00\x00\x01\x00\x01yY\
\x00\x00\x01\x88;p\xbcI\ \x00\x00\x01\x88;p\xbcI\
\x00\x00\x01\x94\x00\x00\x00\x00\x00\x01\x00\x01\xd2-\ \x00\x00\x01\x94\x00\x00\x00\x00\x00\x01\x00\x01\xd2-\
\x00\x00\x01\x94\x1a\xbb\xe8\xa7\ \x00\x00\x01\x94\xb4\xd4\xf0a\
\x00\x00\x01z\x00\x00\x00\x00\x00\x01\x00\x01\x8c\xe6\ \x00\x00\x01z\x00\x00\x00\x00\x00\x01\x00\x01\x8c\xe6\
\x00\x00\x01\x88;p\xbcH\ \x00\x00\x01\x88;p\xbcH\
\x00\x00\x01\x04\x00\x00\x00\x00\x00\x01\x00\x01LR\ \x00\x00\x01\x04\x00\x00\x00\x00\x00\x01\x00\x01LR\

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(450, 400) mainWindow.resize(482, 448)
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)
@@ -40,43 +40,21 @@ class Ui_mainWindow(object):
self.gridLayout_2 = QGridLayout(self.optionWidget) self.gridLayout_2 = QGridLayout(self.optionWidget)
self.gridLayout_2.setObjectName(u"gridLayout_2") self.gridLayout_2.setObjectName(u"gridLayout_2")
self.gridLayout_2.setContentsMargins(0, 0, 0, 0) self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
self.deleteBox = QCheckBox(self.optionWidget)
self.deleteBox.setObjectName(u"deleteBox")
self.gridLayout_2.addWidget(self.deleteBox, 5, 1, 1, 1)
self.mangaBox = QCheckBox(self.optionWidget)
self.mangaBox.setObjectName(u"mangaBox")
self.gridLayout_2.addWidget(self.mangaBox, 1, 0, 1, 1)
self.outputSplit = QCheckBox(self.optionWidget)
self.outputSplit.setObjectName(u"outputSplit")
self.gridLayout_2.addWidget(self.outputSplit, 3, 1, 1, 1)
self.croppingBox = QCheckBox(self.optionWidget) self.croppingBox = QCheckBox(self.optionWidget)
self.croppingBox.setObjectName(u"croppingBox") self.croppingBox.setObjectName(u"croppingBox")
self.croppingBox.setTristate(True) self.croppingBox.setTristate(True)
self.gridLayout_2.addWidget(self.croppingBox, 4, 2, 1, 1) self.gridLayout_2.addWidget(self.croppingBox, 4, 2, 1, 1)
self.mozJpegBox = QCheckBox(self.optionWidget) self.mangaBox = QCheckBox(self.optionWidget)
self.mozJpegBox.setObjectName(u"mozJpegBox") self.mangaBox.setObjectName(u"mangaBox")
self.mozJpegBox.setTristate(True)
self.gridLayout_2.addWidget(self.mozJpegBox, 4, 0, 1, 1) self.gridLayout_2.addWidget(self.mangaBox, 1, 0, 1, 1)
self.upscaleBox = QCheckBox(self.optionWidget) self.webtoonBox = QCheckBox(self.optionWidget)
self.upscaleBox.setObjectName(u"upscaleBox") self.webtoonBox.setObjectName(u"webtoonBox")
self.upscaleBox.setTristate(True)
self.gridLayout_2.addWidget(self.upscaleBox, 2, 1, 1, 1) self.gridLayout_2.addWidget(self.webtoonBox, 2, 0, 1, 1)
self.colorBox = QCheckBox(self.optionWidget)
self.colorBox.setObjectName(u"colorBox")
self.gridLayout_2.addWidget(self.colorBox, 3, 2, 1, 1)
self.rotateBox = QCheckBox(self.optionWidget) self.rotateBox = QCheckBox(self.optionWidget)
self.rotateBox.setObjectName(u"rotateBox") self.rotateBox.setObjectName(u"rotateBox")
@@ -84,36 +62,27 @@ class Ui_mainWindow(object):
self.gridLayout_2.addWidget(self.rotateBox, 1, 1, 1, 1) self.gridLayout_2.addWidget(self.rotateBox, 1, 1, 1, 1)
self.gammaBox = QCheckBox(self.optionWidget)
self.gammaBox.setObjectName(u"gammaBox")
self.gridLayout_2.addWidget(self.gammaBox, 2, 2, 1, 1)
self.spreadShiftBox = QCheckBox(self.optionWidget)
self.spreadShiftBox.setObjectName(u"spreadShiftBox")
self.gridLayout_2.addWidget(self.spreadShiftBox, 5, 0, 1, 1)
self.webtoonBox = QCheckBox(self.optionWidget)
self.webtoonBox.setObjectName(u"webtoonBox")
self.gridLayout_2.addWidget(self.webtoonBox, 2, 0, 1, 1)
self.maximizeStrips = QCheckBox(self.optionWidget)
self.maximizeStrips.setObjectName(u"maximizeStrips")
self.gridLayout_2.addWidget(self.maximizeStrips, 4, 1, 1, 1)
self.borderBox = QCheckBox(self.optionWidget) self.borderBox = QCheckBox(self.optionWidget)
self.borderBox.setObjectName(u"borderBox") self.borderBox.setObjectName(u"borderBox")
self.borderBox.setTristate(True) self.borderBox.setTristate(True)
self.gridLayout_2.addWidget(self.borderBox, 3, 0, 1, 1) self.gridLayout_2.addWidget(self.borderBox, 3, 0, 1, 1)
self.noRotateBox = QCheckBox(self.optionWidget) self.gammaBox = QCheckBox(self.optionWidget)
self.noRotateBox.setObjectName(u"noRotateBox") self.gammaBox.setObjectName(u"gammaBox")
self.gridLayout_2.addWidget(self.noRotateBox, 6, 1, 1, 1) self.gridLayout_2.addWidget(self.gammaBox, 2, 2, 1, 1)
self.interPanelCropBox = QCheckBox(self.optionWidget)
self.interPanelCropBox.setObjectName(u"interPanelCropBox")
self.interPanelCropBox.setTristate(True)
self.gridLayout_2.addWidget(self.interPanelCropBox, 6, 2, 1, 1)
self.colorBox = QCheckBox(self.optionWidget)
self.colorBox.setObjectName(u"colorBox")
self.gridLayout_2.addWidget(self.colorBox, 3, 2, 1, 1)
self.qualityBox = QCheckBox(self.optionWidget) self.qualityBox = QCheckBox(self.optionWidget)
self.qualityBox.setObjectName(u"qualityBox") self.qualityBox.setObjectName(u"qualityBox")
@@ -126,6 +95,11 @@ class Ui_mainWindow(object):
self.gridLayout_2.addWidget(self.disableProcessingBox, 5, 2, 1, 1) self.gridLayout_2.addWidget(self.disableProcessingBox, 5, 2, 1, 1)
self.maximizeStrips = QCheckBox(self.optionWidget)
self.maximizeStrips.setObjectName(u"maximizeStrips")
self.gridLayout_2.addWidget(self.maximizeStrips, 4, 1, 1, 1)
self.authorEdit = QLineEdit(self.optionWidget) self.authorEdit = QLineEdit(self.optionWidget)
self.authorEdit.setObjectName(u"authorEdit") self.authorEdit.setObjectName(u"authorEdit")
sizePolicy = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed) sizePolicy = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
@@ -133,11 +107,48 @@ class Ui_mainWindow(object):
sizePolicy.setVerticalStretch(0) sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.authorEdit.sizePolicy().hasHeightForWidth()) sizePolicy.setHeightForWidth(self.authorEdit.sizePolicy().hasHeightForWidth())
self.authorEdit.setSizePolicy(sizePolicy) self.authorEdit.setSizePolicy(sizePolicy)
self.authorEdit.setFocusPolicy(Qt.ClickFocus) self.authorEdit.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
self.authorEdit.setClearButtonEnabled(False) self.authorEdit.setClearButtonEnabled(False)
self.gridLayout_2.addWidget(self.authorEdit, 0, 0, 1, 1) self.gridLayout_2.addWidget(self.authorEdit, 0, 0, 1, 1)
self.deleteBox = QCheckBox(self.optionWidget)
self.deleteBox.setObjectName(u"deleteBox")
self.gridLayout_2.addWidget(self.deleteBox, 5, 1, 1, 1)
self.mozJpegBox = QCheckBox(self.optionWidget)
self.mozJpegBox.setObjectName(u"mozJpegBox")
self.mozJpegBox.setTristate(True)
self.gridLayout_2.addWidget(self.mozJpegBox, 4, 0, 1, 1)
self.spreadShiftBox = QCheckBox(self.optionWidget)
self.spreadShiftBox.setObjectName(u"spreadShiftBox")
self.gridLayout_2.addWidget(self.spreadShiftBox, 5, 0, 1, 1)
self.upscaleBox = QCheckBox(self.optionWidget)
self.upscaleBox.setObjectName(u"upscaleBox")
self.upscaleBox.setTristate(True)
self.gridLayout_2.addWidget(self.upscaleBox, 2, 1, 1, 1)
self.outputSplit = QCheckBox(self.optionWidget)
self.outputSplit.setObjectName(u"outputSplit")
self.gridLayout_2.addWidget(self.outputSplit, 3, 1, 1, 1)
self.noRotateBox = QCheckBox(self.optionWidget)
self.noRotateBox.setObjectName(u"noRotateBox")
self.gridLayout_2.addWidget(self.noRotateBox, 6, 1, 1, 1)
self.reduceRainbowBox = QCheckBox(self.optionWidget)
self.reduceRainbowBox.setObjectName(u"reduceRainbowBox")
self.gridLayout_2.addWidget(self.reduceRainbowBox, 7, 2, 1, 1)
self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2) self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
@@ -156,7 +167,7 @@ class Ui_mainWindow(object):
self.gammaSlider.setObjectName(u"gammaSlider") self.gammaSlider.setObjectName(u"gammaSlider")
self.gammaSlider.setMaximum(250) self.gammaSlider.setMaximum(250)
self.gammaSlider.setSingleStep(5) self.gammaSlider.setSingleStep(5)
self.gammaSlider.setOrientation(Qt.Horizontal) self.gammaSlider.setOrientation(Qt.Orientation.Horizontal)
self.horizontalLayout_2.addWidget(self.gammaSlider) self.horizontalLayout_2.addWidget(self.gammaSlider)
@@ -178,7 +189,7 @@ class Ui_mainWindow(object):
self.croppingPowerSlider.setObjectName(u"croppingPowerSlider") self.croppingPowerSlider.setObjectName(u"croppingPowerSlider")
self.croppingPowerSlider.setMaximum(300) self.croppingPowerSlider.setMaximum(300)
self.croppingPowerSlider.setSingleStep(1) self.croppingPowerSlider.setSingleStep(1)
self.croppingPowerSlider.setOrientation(Qt.Horizontal) self.croppingPowerSlider.setOrientation(Qt.Orientation.Horizontal)
self.horizontalLayout_3.addWidget(self.croppingPowerSlider) self.horizontalLayout_3.addWidget(self.croppingPowerSlider)
@@ -284,9 +295,9 @@ class Ui_mainWindow(object):
self.jobList = QListWidget(self.centralWidget) self.jobList = QListWidget(self.centralWidget)
self.jobList.setObjectName(u"jobList") self.jobList.setObjectName(u"jobList")
self.jobList.setStyleSheet(u"") self.jobList.setStyleSheet(u"")
self.jobList.setSelectionMode(QAbstractItemView.NoSelection) self.jobList.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.jobList.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) self.jobList.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
self.jobList.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) self.jobList.setHorizontalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
self.gridLayout.addWidget(self.jobList, 2, 0, 1, 2) self.gridLayout.addWidget(self.jobList, 2, 0, 1, 2)
@@ -295,7 +306,7 @@ class Ui_mainWindow(object):
self.progressBar.setMinimumSize(QSize(0, 30)) self.progressBar.setMinimumSize(QSize(0, 30))
self.progressBar.setFont(font) self.progressBar.setFont(font)
self.progressBar.setVisible(False) self.progressBar.setVisible(False)
self.progressBar.setAlignment(Qt.AlignJustify|Qt.AlignVCenter) self.progressBar.setAlignment(Qt.AlignmentFlag.AlignJustify|Qt.AlignmentFlag.AlignVCenter)
self.gridLayout.addWidget(self.progressBar, 1, 0, 1, 2) self.gridLayout.addWidget(self.progressBar, 1, 0, 1, 2)
@@ -377,61 +388,37 @@ class Ui_mainWindow(object):
def retranslateUi(self, mainWindow): def retranslateUi(self, mainWindow):
mainWindow.setWindowTitle(QCoreApplication.translate("mainWindow", u"Kindle Comic Converter", None)) mainWindow.setWindowTitle(QCoreApplication.translate("mainWindow", u"Kindle Comic Converter", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.deleteBox.setToolTip(QCoreApplication.translate("mainWindow", u"Delete input file(s) or directory. It's not recoverable!", None)) self.croppingBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Disabled</span></p><p>Disabled</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Margins<br/></span>Margins</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Margins + page numbers<br/></span>Margins +page numbers</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.deleteBox.setText(QCoreApplication.translate("mainWindow", u"Delete input", None)) self.croppingBox.setText(QCoreApplication.translate("mainWindow", u"Cropping mode", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.mangaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable right-to-left reading.</p></body></html>", None)) self.mangaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable right-to-left reading.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Manga mode", None)) self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Manga mode", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.outputSplit.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Automatic mode<br/></span>The output will be split automatically.</p><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Checked - Volume mode<br/></span>Every subdirectory will be considered as a separate volume.</p></body></html>", None)) self.webtoonBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable special parsing mode for Korean Webtoons.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.outputSplit.setText(QCoreApplication.translate("mainWindow", u"Output split", None)) self.webtoonBox.setText(QCoreApplication.translate("mainWindow", u"Webtoon mode", None))
#if QT_CONFIG(tooltip)
self.croppingBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Disabled</span></p><p>Disabled</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Margins<br/></span>Margins</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Margins + page numbers<br/></span>Margins +page numbers</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.croppingBox.setText(QCoreApplication.translate("mainWindow", u"Cropping mode", None))
#if QT_CONFIG(tooltip)
self.mozJpegBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - JPEG<br/></span>Use JPEG files</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - force PNG<br/></span>Create PNG files instead JPEG</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - mozJpeg<br/></span>10-20% smaller JPEG file, with the same image quality, but processing time multiplied by 2</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.mozJpegBox.setText(QCoreApplication.translate("mainWindow", u"JPEG/PNG/mozJpeg", None))
#if QT_CONFIG(tooltip)
self.upscaleBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Nothing<br/></span>Images smaller than device resolution will not be resized.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Stretching<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be not preserved.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Upscaling<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be preserved.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.upscaleBox.setText(QCoreApplication.translate("mainWindow", u"Stretch/Upscale", None))
#if QT_CONFIG(tooltip)
self.colorBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Disable conversion to grayscale.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.colorBox.setText(QCoreApplication.translate("mainWindow", u"Color mode", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.rotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Split<br/></span>Double page spreads will be cut into two separate pages.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Rotate and split<br/></span>Double page spreads will be displayed twice. First rotated and then split. </p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Rotate<br/></span>Double page spreads will be rotated.</p></body></html>", None)) self.rotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Split<br/></span>Double page spreads will be cut into two separate pages.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Rotate and split<br/></span>Double page spreads will be displayed twice. First rotated and then split. </p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Rotate<br/></span>Double page spreads will be rotated.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.rotateBox.setText(QCoreApplication.translate("mainWindow", u"Spread splitter", None)) self.rotateBox.setText(QCoreApplication.translate("mainWindow", u"Spread splitter", None))
#if QT_CONFIG(tooltip)
self.gammaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Disable automatic gamma correction.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.gammaBox.setText(QCoreApplication.translate("mainWindow", u"Custom gamma", None))
#if QT_CONFIG(tooltip)
self.spreadShiftBox.setToolTip(QCoreApplication.translate("mainWindow", u"Shift first page to opposite side in landscape for two page spread alignment", None))
#endif // QT_CONFIG(tooltip)
self.spreadShiftBox.setText(QCoreApplication.translate("mainWindow", u"Spread shift", None))
#if QT_CONFIG(tooltip)
self.webtoonBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable special parsing mode for Korean Webtoons.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.webtoonBox.setText(QCoreApplication.translate("mainWindow", u"Webtoon mode", None))
#if QT_CONFIG(tooltip)
self.maximizeStrips.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 1x4<br/></span>Keep format 1x4 panels strips.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 2x2<br/></span>Turn 1x4 strips to 2x2 to maximize screen usage.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.maximizeStrips.setText(QCoreApplication.translate("mainWindow", u"1x4 to 2x2 strips", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.borderBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Autodetection<br/></span>The color of margins fill will be detected automatically.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - White<br/></span>Margins will be filled with white color.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Black<br/></span>Margins will be filled with black color.</p></body></html>", None)) self.borderBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Autodetection<br/></span>The color of margins fill will be detected automatically.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - White<br/></span>Margins will be filled with white color.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Black<br/></span>Margins will be filled with black color.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.borderBox.setText(QCoreApplication.translate("mainWindow", u"W/B margins", None)) self.borderBox.setText(QCoreApplication.translate("mainWindow", u"W/B margins", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.noRotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"Do not rotate double page spreads in spread splitter option.", None)) self.gammaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Disable automatic gamma correction.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.noRotateBox.setText(QCoreApplication.translate("mainWindow", u"No rotate", None)) self.gammaBox.setText(QCoreApplication.translate("mainWindow", u"Custom gamma", None))
#if QT_CONFIG(tooltip)
self.interPanelCropBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Disabled<br/></span>Disabled</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Horizontal<br/></span>Crop empty horizontal lines.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Both<br/></span>Crop empty horizontal and vertical lines.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.interPanelCropBox.setText(QCoreApplication.translate("mainWindow", u"Inter-panel crop", None))
#if QT_CONFIG(tooltip)
self.colorBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Disable conversion to grayscale.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.colorBox.setText(QCoreApplication.translate("mainWindow", u"Color mode", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.qualityBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 4 panels<br/></span>Zoom each corner separately.</p><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - 2 panels<br/></span>Zoom only the top and bottom of the page.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 4 high-quality panels<br/></span>Zoom each corner separately. Try to increase the quality of magnification. Check wiki for more details.</p></body></html>", None)) self.qualityBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 4 panels<br/></span>Zoom each corner separately.</p><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - 2 panels<br/></span>Zoom only the top and bottom of the page.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 4 high-quality panels<br/></span>Zoom each corner separately. Try to increase the quality of magnification. Check wiki for more details.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
@@ -440,10 +427,42 @@ class Ui_mainWindow(object):
self.disableProcessingBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><pre style=\" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">Do not process any image, ignore profile and processing options</pre></body></html>", None)) self.disableProcessingBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><pre style=\" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">Do not process any image, ignore profile and processing options</pre></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.disableProcessingBox.setText(QCoreApplication.translate("mainWindow", u"Disable processing", None)) self.disableProcessingBox.setText(QCoreApplication.translate("mainWindow", u"Disable processing", None))
#if QT_CONFIG(tooltip)
self.maximizeStrips.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 1x4<br/></span>Keep format 1x4 panels strips.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 2x2<br/></span>Turn 1x4 strips to 2x2 to maximize screen usage.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.maximizeStrips.setText(QCoreApplication.translate("mainWindow", u"1x4 to 2x2 strips", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.authorEdit.setToolTip(QCoreApplication.translate("mainWindow", u"Default Author is KCC", None)) self.authorEdit.setToolTip(QCoreApplication.translate("mainWindow", u"Default Author is KCC", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.authorEdit.setPlaceholderText(QCoreApplication.translate("mainWindow", u"Default Author", None)) self.authorEdit.setPlaceholderText(QCoreApplication.translate("mainWindow", u"Default Author", None))
#if QT_CONFIG(tooltip)
self.deleteBox.setToolTip(QCoreApplication.translate("mainWindow", u"Delete input file(s) or directory. It's not recoverable!", None))
#endif // QT_CONFIG(tooltip)
self.deleteBox.setText(QCoreApplication.translate("mainWindow", u"Delete input", None))
#if QT_CONFIG(tooltip)
self.mozJpegBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - JPEG<br/></span>Use JPEG files</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - force PNG<br/></span>Create PNG files instead JPEG</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - mozJpeg<br/></span>10-20% smaller JPEG file, with the same image quality, but processing time multiplied by 2</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.mozJpegBox.setText(QCoreApplication.translate("mainWindow", u"JPEG/PNG/mozJpeg", None))
#if QT_CONFIG(tooltip)
self.spreadShiftBox.setToolTip(QCoreApplication.translate("mainWindow", u"Shift first page to opposite side in landscape for two page spread alignment", None))
#endif // QT_CONFIG(tooltip)
self.spreadShiftBox.setText(QCoreApplication.translate("mainWindow", u"Spread shift", None))
#if QT_CONFIG(tooltip)
self.upscaleBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Nothing<br/></span>Images smaller than device resolution will not be resized.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Stretching<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be not preserved.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Upscaling<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be preserved.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.upscaleBox.setText(QCoreApplication.translate("mainWindow", u"Stretch/Upscale", None))
#if QT_CONFIG(tooltip)
self.outputSplit.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Automatic mode<br/></span>The output will be split automatically.</p><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Checked - Volume mode<br/></span>Every subdirectory will be considered as a separate volume.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.outputSplit.setText(QCoreApplication.translate("mainWindow", u"Output split", None))
#if QT_CONFIG(tooltip)
self.noRotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"Do not rotate double page spreads in spread splitter option.", None))
#endif // QT_CONFIG(tooltip)
self.noRotateBox.setText(QCoreApplication.translate("mainWindow", u"No rotate", None))
#if QT_CONFIG(tooltip)
self.reduceRainbowBox.setToolTip(QCoreApplication.translate("mainWindow", u"Reduce rainbow effect on color eink by slightly blurring images", None))
#endif // QT_CONFIG(tooltip)
self.reduceRainbowBox.setText(QCoreApplication.translate("mainWindow", u"Reduce Rainbow", None))
self.gammaLabel.setText(QCoreApplication.translate("mainWindow", u"Gamma: Auto", None)) self.gammaLabel.setText(QCoreApplication.translate("mainWindow", u"Gamma: Auto", None))
self.croppingPowerLabel.setText(QCoreApplication.translate("mainWindow", u"Cropping power:", None)) self.croppingPowerLabel.setText(QCoreApplication.translate("mainWindow", u"Cropping power:", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)

View File

@@ -1,4 +1,4 @@
__version__ = '7.1.2' __version__ = '7.2.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

@@ -612,8 +612,11 @@ def imgFileProcessing(work):
img.cropPageNumber(opt.croppingp, opt.croppingm) img.cropPageNumber(opt.croppingp, opt.croppingm)
if opt.cropping > 0 and not opt.webtoon: if opt.cropping > 0 and not opt.webtoon:
img.cropMargin(opt.croppingp, opt.croppingm) img.cropMargin(opt.croppingp, opt.croppingm)
if opt.interpanelcrop > 0:
img.cropInterPanelEmptySections("horizontal" if opt.interpanelcrop == 1 else "both")
img.autocontrastImage() img.autocontrastImage()
img.resizeImage() img.resizeImage()
img.optimizeForDisplay(opt.reducerainbow)
if opt.forcepng and not opt.forcecolor: if opt.forcepng and not opt.forcecolor:
img.quantizeImage() img.quantizeImage()
output.append(img.saveToDir()) output.append(img.saveToDir())
@@ -626,7 +629,7 @@ def getWorkFolder(afile):
if os.path.isdir(afile): if os.path.isdir(afile):
if disk_usage(gettempdir())[2] < getDirectorySize(afile) * 2.5: if disk_usage(gettempdir())[2] < getDirectorySize(afile) * 2.5:
raise UserWarning("Not enough disk space to perform conversion.") raise UserWarning("Not enough disk space to perform conversion.")
workdir = mkdtemp('', 'KCC-') workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
try: try:
os.rmdir(workdir) os.rmdir(workdir)
fullPath = os.path.join(workdir, 'OEBPS', 'Images') fullPath = os.path.join(workdir, 'OEBPS', 'Images')
@@ -642,26 +645,37 @@ def getWorkFolder(afile):
if afile.lower().endswith('.pdf'): if afile.lower().endswith('.pdf'):
pdf = pdfjpgextract.PdfJpgExtract(afile) pdf = pdfjpgextract.PdfJpgExtract(afile)
path, njpg = pdf.extract() path, njpg = pdf.extract()
workdir = path
sanitizePermissions(path)
if njpg == 0: if njpg == 0:
rmtree(path, True) rmtree(path, True)
raise UserWarning("Failed to extract images from PDF file.") raise UserWarning("Failed to extract images from PDF file.")
else: else:
workdir = mkdtemp('', 'KCC-') workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
try: try:
cbx = comicarchive.ComicArchive(afile) cbx = comicarchive.ComicArchive(afile)
path = cbx.extract(workdir) path = cbx.extract(workdir)
sanitizePermissions(path)
tdir = os.listdir(workdir) tdir = os.listdir(workdir)
if 'ComicInfo.xml' in tdir: is_nested_single_dir = False
tdir.remove('ComicInfo.xml') if len(tdir) == 2 and 'ComicInfo.xml' in tdir:
tdir.remove('ComicInfo.xml')
is_nested_single_dir = os.path.isdir(os.path.join(workdir, tdir[0]))
if is_nested_single_dir:
os.replace(
os.path.join(workdir, 'ComicInfo.xml'),
os.path.join(workdir, tdir[0], 'ComicInfo.xml')
)
if len(tdir) == 1 and is_nested_single_dir:
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)
else: else:
raise UserWarning("Failed to open source file/directory.") raise UserWarning("Failed to open source file/directory.")
sanitizePermissions(path) newpath = mkdtemp('', 'KCC-', os.path.dirname(afile))
newpath = mkdtemp('', 'KCC-')
copytree(path, os.path.join(newpath, 'OEBPS', 'Images')) copytree(path, os.path.join(newpath, 'OEBPS', 'Images'))
rmtree(path, True) rmtree(workdir, True)
return newpath return newpath
@@ -815,12 +829,11 @@ def sanitizePermissions(filetree):
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)
def splitDirectory(path): def chunk_directory(path):
level = -1 level = -1
for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')): for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
for f in files: for f in files:
if f.endswith('.jpg') or f.endswith('.jpeg') or f.endswith('.png') or f.endswith('.gif') or \ if getImageFileName(f):
f.endswith('.webp'):
newLevel = os.path.join(root, f).replace(os.path.join(path, 'OEBPS', 'Images'), '').count(os.sep) newLevel = os.path.join(root, f).replace(os.path.join(path, 'OEBPS', 'Images'), '').count(os.sep)
if level != -1 and level != newLevel: if level != -1 and level != newLevel:
level = 0 level = 0
@@ -828,16 +841,17 @@ def splitDirectory(path):
else: else:
level = newLevel level = newLevel
if level > 0: if level > 0:
splitter = splitProcess(os.path.join(path, 'OEBPS', 'Images'), level) parent = pathlib.Path(path).parent
chunker = chunk_process(os.path.join(path, 'OEBPS', 'Images'), level, parent)
path = [path] path = [path]
for tome in splitter: for tome in chunker:
path.append(tome) path.append(tome)
return path return path
else: else:
raise UserWarning('Unsupported directory structure.') raise UserWarning('Unsupported directory structure.')
def splitProcess(path, mode): def chunk_process(path, mode, parent):
output = [] output = []
currentSize = 0 currentSize = 0
currentTarget = path currentTarget = path
@@ -857,7 +871,7 @@ def splitProcess(path, mode):
else: else:
size = getDirectorySize(os.path.join(root, name)) size = getDirectorySize(os.path.join(root, name))
if currentSize + size > targetSize: if currentSize + size > targetSize:
currentTarget, pathRoot = createNewTome() currentTarget, pathRoot = createNewTome(parent)
output.append(pathRoot) output.append(pathRoot)
currentSize = size currentSize = size
else: else:
@@ -869,7 +883,7 @@ def splitProcess(path, mode):
for root, dirs, _ in walkLevel(path, 0): for root, dirs, _ in walkLevel(path, 0):
for name in dirs: for name in dirs:
if not firstTome: if not firstTome:
currentTarget, pathRoot = createNewTome() currentTarget, pathRoot = createNewTome(parent)
output.append(pathRoot) output.append(pathRoot)
move(os.path.join(root, name), os.path.join(currentTarget, name)) move(os.path.join(root, name), os.path.join(currentTarget, name))
else: else:
@@ -906,7 +920,10 @@ def detectCorruption(tmppath, orgpath):
else: else:
raise RuntimeError('Image file %s is corrupted. Error: %s' % (pathOrg, str(err))) raise RuntimeError('Image file %s is corrupted. Error: %s' % (pathOrg, str(err)))
else: else:
os.remove(os.path.join(root, name)) try:
os.remove(os.path.join(root, name))
except OSError as e:
raise RuntimeError(f"{name}: {e}")
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:
@@ -922,8 +939,8 @@ def detectCorruption(tmppath, orgpath):
GUI.addMessage.emit('', '', False) GUI.addMessage.emit('', '', False)
def createNewTome(): def createNewTome(parent):
tomePathRoot = mkdtemp('', 'KCC-') tomePathRoot = mkdtemp('', 'KCC-', parent)
tomePath = os.path.join(tomePathRoot, 'OEBPS', 'Images') tomePath = os.path.join(tomePathRoot, 'OEBPS', 'Images')
os.makedirs(tomePath) os.makedirs(tomePath)
return tomePath, tomePathRoot return tomePath, tomePathRoot
@@ -1013,12 +1030,16 @@ def makeParser():
help="Set cropping power [Default=1.0]") help="Set cropping power [Default=1.0]")
processing_options.add_argument("--cm", "--croppingminimum", type=float, dest="croppingm", default="0.0", processing_options.add_argument("--cm", "--croppingminimum", type=float, dest="croppingm", default="0.0",
help="Set cropping minimum area ratio [Default=0.0]") help="Set cropping minimum area ratio [Default=0.0]")
processing_options.add_argument("--ipc", "--interpanelcrop", type=int, dest="interpanelcrop", default="0",
help="Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0]")
processing_options.add_argument("--blackborders", action="store_true", dest="black_borders", default=False, processing_options.add_argument("--blackborders", action="store_true", dest="black_borders", default=False,
help="Disable autodetection and force black borders") help="Disable autodetection and force black borders")
processing_options.add_argument("--whiteborders", action="store_true", dest="white_borders", default=False, processing_options.add_argument("--whiteborders", action="store_true", dest="white_borders", default=False,
help="Disable autodetection and force white borders") help="Disable autodetection and force white borders")
processing_options.add_argument("--forcecolor", action="store_true", dest="forcecolor", default=False, processing_options.add_argument("--forcecolor", action="store_true", dest="forcecolor", default=False,
help="Don't convert images to grayscale") help="Don't convert images to grayscale")
output_options.add_argument("--reducerainbow", action="store_true", dest="reducerainbow", default=False,
help="Reduce rainbow effect on color eink by slightly blurring images.")
processing_options.add_argument("--forcepng", action="store_true", dest="forcepng", default=False, processing_options.add_argument("--forcepng", action="store_true", dest="forcepng", default=False,
help="Create PNG files instead JPEG") help="Create PNG files instead JPEG")
processing_options.add_argument("--mozjpeg", action="store_true", dest="mozjpeg", default=False, processing_options.add_argument("--mozjpeg", action="store_true", dest="mozjpeg", default=False,
@@ -1175,7 +1196,7 @@ def makeBook(source, qtgui=None):
GUI.progressBarTick.emit('1') GUI.progressBarTick.emit('1')
chapterNames = sanitizeTree(os.path.join(path, 'OEBPS', 'Images')) chapterNames = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
if options.batchsplit > 0: if options.batchsplit > 0:
tomes = splitDirectory(path) tomes = chunk_directory(path)
else: else:
tomes = [path] tomes = [path]
filepath = [] filepath = []

View File

@@ -0,0 +1,28 @@
def threshold_from_power(power):
return 240-(power*64)
'''
Groups close values together
'''
def group_close_values(vals, max_dist_tolerated):
groups = []
group_start = -1
group_end = 0
for i in range(len(vals)):
dist = vals[i] - group_end
if group_start == -1:
group_start = vals[i]
group_end = vals[i]
elif dist <= max_dist_tolerated:
group_end = vals[i]
else:
groups.append((group_start, group_end))
group_start = -1
group_end = -1
if group_start != -1:
groups.append((group_start, group_end))
return groups

View File

@@ -24,6 +24,7 @@ import mozjpeg_lossless_optimization
from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter
from .shared import md5Checksum from .shared import md5Checksum
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
from .inter_panel_crop_alg import crop_empty_inter_panel
AUTO_CROP_THRESHOLD = 0.015 AUTO_CROP_THRESHOLD = 0.015
@@ -340,6 +341,14 @@ class ComicPage:
# Quantize is deprecated but new function call it internally anyway... # 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):
# Reduce rainbow artifacts for grayscale images by breaking up dither patterns that cause Moire interference with color filter array
if reducerainbow and not self.color:
unsharpFilter = ImageFilter.UnsharpMask(radius=1, percent=100)
self.image = self.image.filter(unsharpFilter)
self.image = self.image.filter(ImageFilter.BoxBlur(1.0))
self.image = self.image.filter(unsharpFilter)
def resizeImage(self): def resizeImage(self):
# kindle scribe conversion to mobi is limited in resolution by kindlegen, same with send to kindle and epub # kindle scribe conversion to mobi is limited in resolution by kindlegen, same with send to kindle and epub
if self.kindle_scribe_azw3: if self.kindle_scribe_azw3:
@@ -390,6 +399,8 @@ class ComicPage:
if bbox: if bbox:
self.maybeCrop(bbox, minimum) self.maybeCrop(bbox, minimum)
def cropInterPanelEmptySections(self, direction):
self.image = crop_empty_inter_panel(self.image, direction, background_color=self.fill)
class Cover: class Cover:
def __init__(self, source, target, opt, tomeid): def __init__(self, source, target, opt, tomeid):

View File

@@ -0,0 +1,76 @@
from PIL import Image, ImageFilter, ImageOps
import numpy as np
from typing import Literal
from .common_crop import threshold_from_power, group_close_values
'''
Crops inter-panel empty spaces (ignores empty spaces near borders - for that use crop margins).
Parameters:
img (PIL image): A PIL image.
direction (horizontal or vertical or both): To crop rows (horizontal), cols (vertical) or both.
keep (float): Distance to keep between panels after cropping (in percentage relative to the original distance).
background_color (string): 'white' for white background, anything else for black.
Returns:
img (PIL image): A PIL image after cropping empty sections.
'''
def crop_empty_inter_panel(img, direction: Literal["horizontal", "vertical", "both"], keep=0.04, background_color='white'):
img_temp = img
if img.mode != 'L':
img_temp = ImageOps.grayscale(img)
if background_color != 'white':
img_temp = ImageOps.invert(img)
img_mat = np.array(img)
power = 1
img_temp = ImageOps.autocontrast(img_temp, 1).filter(ImageFilter.BoxBlur(1))
img_temp = img_temp.point(lambda p: 255 if p <= threshold_from_power(power) else 0)
if direction in ["horizontal", "both"]:
rows_idx_to_remove = empty_sections(img_temp, keep, horizontal=True)
img_mat = np.delete(img_mat, rows_idx_to_remove, 0)
if direction in ["vertical", "both"]:
cols_idx_to_remove = empty_sections(img_temp, keep, horizontal=False)
img_mat = np.delete(img_mat, cols_idx_to_remove, 1)
return Image.fromarray(img_mat)
'''
Finds empty sections (excluding near borders).
Parameters:
img (PIL image): A PIL image.
keep (float): Distance to keep between panels after cropping (in percentage relative to the original distance).
horizontal (boolean): True to find empty rows, False to find empty columns.
Returns:
Itertable (list or NumPy array): indices of rows or columns to remove.
'''
def empty_sections(img, keep, horizontal=True):
axis = 1 if horizontal else 0
img_mat = np.array(img)
img_mat_max = np.max(img_mat, axis=axis)
img_mat_empty_idx = np.where(img_mat_max == 0)[0]
empty_sections = group_close_values(img_mat_empty_idx, 1)
sections_to_remove = []
for section in empty_sections:
if section[1] < img.size[1] * 0.99 and section[0] > img.size[1] * 0.01: # if not near borders
sections_to_remove.append(section)
if len(sections_to_remove) != 0:
sections_to_remove_after_keep = [(int(x1+(keep/2)*(x2-x1)), int(x2-(keep/2)*(x2-x1))) for x1,x2 in sections_to_remove]
idx_to_remove = np.concatenate([np.arange(x1, x2) for x1,x2 in sections_to_remove_after_keep])
return idx_to_remove
return []

View File

@@ -1,5 +1,7 @@
from PIL import ImageOps, ImageFilter from PIL import ImageOps, ImageFilter
import numpy as np import numpy as np
from .common_crop import threshold_from_power, group_close_values
''' '''
Some assupmptions on the page number sizes Some assupmptions on the page number sizes
@@ -51,12 +53,11 @@ def get_bbox_crop_margin_page_number(img, power=1, background_color='white'):
threshold = threshold_from_power(power) threshold = threshold_from_power(power)
bw_img = img.point(lambda p: 255 if p <= threshold else 0) bw_img = img.point(lambda p: 255 if p <= threshold else 0)
bw_bbox = bw_img.getbbox() bw_bbox = bw_img.getbbox()
if not bw_bbox: # bbox cannot be found in case that the entire resulted image is black. if not bw_bbox: # bbox cannot be found in case that the entire resulted image is black.
return None return None
left, top_y_pos, right, bot_y_pos = bw_bbox left, top_y_pos, right, bot_y_pos = bw_bbox
''' '''
We inspect the lower bottom part of the image where we suspect might be a page number. We inspect the lower bottom part of the image where we suspect might be a page number.
We assume that page number consist of 1 to 3 digits and the total min and max size of the number We assume that page number consist of 1 to 3 digits and the total min and max size of the number
@@ -73,7 +74,7 @@ def get_bbox_crop_margin_page_number(img, power=1, background_color='white'):
img_part_mat = np.array(img_part) img_part_mat = np.array(img_part)
window_groups = [] window_groups = []
for i in range(img_part.size[1]): for i in range(img_part.size[1]):
row_groups = [(g[0], g[1], i, i) for g in group_pixels(img_part_mat[i], img.size[0]*max_dist_size[0], threshold)] row_groups = [(g[0], g[1], i, i) for g in group_close_values(np.where(img_part_mat[i] <= threshold)[0], img.size[0]*max_dist_size[0])]
window_groups.extend(row_groups) window_groups.extend(row_groups)
window_groups = np.array(window_groups) window_groups = np.array(window_groups)
@@ -109,7 +110,6 @@ def get_bbox_crop_margin_page_number(img, power=1, background_color='white'):
cropped_bbox = (0, 0, img.size[0], bot_y_pos-(window_h-boxes_in_same_y_range[0][2]+1)) cropped_bbox = (0, 0, img.size[0], bot_y_pos-(window_h-boxes_in_same_y_range[0][2]+1))
cropped_bbox = bw_img.crop(cropped_bbox).getbbox() cropped_bbox = bw_img.crop(cropped_bbox).getbbox()
return cropped_bbox return cropped_bbox
@@ -145,33 +145,6 @@ def get_bbox_crop_margin(img, power=1, background_color='white'):
return bw_img.getbbox() return bw_img.getbbox()
'''
Groups close pixels together (x axis)
'''
def group_pixels(row, max_dist_tolerated, threshold):
groups = []
idx = np.where(row <= threshold)[0]
group_start = -1
group_end = 0
for i in range(len(idx)):
dist = idx[i] - group_end
if group_start == -1:
group_start = idx[i]
group_end = idx[i]
elif dist <= max_dist_tolerated:
group_end = idx[i]
else:
groups.append((group_start, group_end))
group_start = -1
group_end = -1
if group_start != -1:
groups.append((group_start, group_end))
return groups
def box_intersect(box1, box2, max_dist): def box_intersect(box1, box2, max_dist):
return not (box2[0]-max_dist[0] > box1[1] return not (box2[0]-max_dist[0] > box1[1]
or box2[1]+max_dist[0] < box1[0] or box2[1]+max_dist[0] < box1[0]
@@ -209,7 +182,3 @@ def merge_boxes(boxes, max_dist_tolerated):
else: else:
j += 1 j += 1
return boxes return boxes
def threshold_from_power(power):
return 240-(power*64)

View File

@@ -49,7 +49,7 @@ 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) or ext not in ['.png', '.jpg', '.jpeg', '.gif', '.webp']: if (name.startswith('.') and len(name) == 1) or ext not in ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2', '.j2k', '.jpx']:
return None return None
return [name, ext] return [name, ext]