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

Compare commits

...

22 Commits

Author SHA1 Message Date
Alex Xu
90c9ba7539 bump 8.0.4 2025-07-10 09:19:57 -07:00
Alex Xu
84da718167 more extraction fixes (#1023) 2025-07-10 09:14:06 -07:00
Alex Xu
fe7559e6a9 Add note about RTL CBZ output 2025-07-10 06:58:03 -07:00
Alex Xu
a79c740387 don't autocontrast color content (#1021) 2025-07-09 13:59:54 -07:00
Alex Xu
bc98eecae9 bump 8.0.3 2025-07-09 11:28:25 -07:00
Alex Xu
e7a07377ef add avif input support (#1019)
* add avif input support

* add avif
2025-07-09 11:27:58 -07:00
Alex Xu
07ef11013a fix cbz metadata (#1018) 2025-07-09 11:14:25 -07:00
Alex Xu
551fe6edbf bump 8.0.2 2025-07-05 07:41:08 -07:00
Alex Xu
dbf8a3ddbd fix ._cover.jpg on macos 15 (#1016) 2025-07-05 07:40:09 -07:00
Alex Xu
8f9e230b62 rotate first (#1015) 2025-07-04 19:52:34 -07:00
Alex Xu
36d9a4151e Update README.md 2025-07-04 14:23:28 -07:00
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
14 changed files with 460 additions and 406 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
- Right to left mode not working?
- RTL mode only affects splitting order for CBZ output. Your cbz reader itself sets the page turn direction.
- Colors inverted? - Colors inverted?
- Disable Kindle dark mode - Disable Kindle dark mode
- Cannot connect Kindle Scribe or 2024+ Kindle to macOS - Cannot connect Kindle Scribe or 2024+ Kindle to macOS
@@ -254,6 +256,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.
--rotatefirst Put rotated spread first in spread splitter option.
--reducerainbow Reduce rainbow effect on color eink by slightly blurring images --reducerainbow Reduce rainbow effect on color eink by slightly blurring images
CUSTOM PROFILE: CUSTOM PROFILE:
@@ -301,6 +304,8 @@ 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 An example PR adding a new checkbox is here: https://github.com/ciromattia/kcc/pull/785
video of adding a new checkbox: https://youtu.be/g3I8DU74C7g
Do not use `git merge` to merge master from upstream, Do not use `git merge` to merge master from upstream,
use the "Sync fork" button on your fork on GitHub in your branch use the "Sync fork" button on your fork on GitHub in your branch
to avoid weird looking merges in pull requests. to avoid weird looking merges in pull requests.

View File

@@ -4,7 +4,7 @@ channels:
- defaults - defaults
dependencies: dependencies:
- python=3.11 - python=3.11
- Pillow>=5.2.0 - Pillow>=11.3.0
- psutil>=5.9.5 - psutil>=5.9.5
- python-slugify>=1.2.1 - python-slugify>=1.2.1
- raven>=6.0.0 - raven>=6.0.0

View File

@@ -453,13 +453,13 @@
<property name="bottomMargin"> <property name="bottomMargin">
<number>0</number> <number>0</number>
</property> </property>
<item row="4" column="2"> <item row="6" column="2">
<widget class="QCheckBox" name="croppingBox"> <widget class="QCheckBox" name="interPanelCropBox">
<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 - Disabled&lt;/span&gt;&lt;/p&gt;&lt;p&gt;Disabled&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - Margins&lt;br/&gt;&lt;/span&gt;Margins&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Margins + page numbers&lt;br/&gt;&lt;/span&gt;Margins +page numbers&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <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>
<property name="text"> <property name="text">
<string>Cropping mode</string> <string>Inter-panel crop</string>
</property> </property>
<property name="tristate"> <property name="tristate">
<bool>true</bool> <bool>true</bool>
@@ -476,6 +476,41 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="0">
<widget class="QLineEdit" name="authorEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="focusPolicy">
<enum>Qt::FocusPolicy::ClickFocus</enum>
</property>
<property name="toolTip">
<string>Default Author is KCC</string>
</property>
<property name="placeholderText">
<string>Default Author</string>
</property>
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QCheckBox" name="croppingBox">
<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;/span&gt;&lt;/p&gt;&lt;p&gt;Disabled&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - Margins&lt;br/&gt;&lt;/span&gt;Margins&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Margins + page numbers&lt;br/&gt;&lt;/span&gt;Margins +page numbers&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Cropping mode</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0"> <item row="2" column="0">
<widget class="QCheckBox" name="webtoonBox"> <widget class="QCheckBox" name="webtoonBox">
<property name="toolTip"> <property name="toolTip">
@@ -486,6 +521,155 @@
</property> </property>
</widget> </widget>
</item> </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>
</widget>
</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="7" column="0">
<widget class="QCheckBox" name="comicinfoTitleBox">
<property name="toolTip">
<string>Write Title from ComicInfo.xml</string>
</property>
<property name="text">
<string>ComicInfo Title</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QCheckBox" name="qualityBox">
<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 - 4 panels&lt;br/&gt;&lt;/span&gt;Zoom each corner separately.&lt;/p&gt;&lt;p style='white-space:pre'&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - 2 panels&lt;br/&gt;&lt;/span&gt;Zoom only the top and bottom of the page.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - 4 high-quality panels&lt;br/&gt;&lt;/span&gt;Zoom each corner separately. Try to increase the quality of magnification. Check wiki for more details.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Panel View 4/2/HQ</string>
</property>
<property name="tristate">
<bool>true</bool>
</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>Rainbow blur</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="6" column="0">
<widget class="QCheckBox" name="fileFusionBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Combines all selected files into a single file. (Helpful for combining chapters into volumes.)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>File Fusion</string>
</property>
</widget>
</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="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="2">
<widget class="QCheckBox" name="disableProcessingBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Do not process any image, ignore profile and processing options.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Disable processing</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QCheckBox" name="chunkSizeCheckBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700; text-decoration: underline;&quot;&gt;Unchecked&lt;br/&gt;&lt;/span&gt;Maximal output file size is 100 MB for Webtoon, 400 MB for others before split occurs.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700; text-decoration: underline;&quot;&gt;Checked&lt;/span&gt;&lt;br/&gt;Output file size specified in &amp;quot;Chunk size MB&amp;quot; before split occurs.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Chunk size</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="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="1" column="1"> <item row="1" column="1">
<widget class="QCheckBox" name="rotateBox"> <widget class="QCheckBox" name="rotateBox">
<property name="toolTip"> <property name="toolTip">
@@ -512,62 +696,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="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>
</widget>
</item>
<item row="1" column="2">
<widget class="QCheckBox" name="qualityBox">
<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 - 4 panels&lt;br/&gt;&lt;/span&gt;Zoom each corner separately.&lt;/p&gt;&lt;p style='white-space:pre'&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - 2 panels&lt;br/&gt;&lt;/span&gt;Zoom only the top and bottom of the page.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - 4 high-quality panels&lt;br/&gt;&lt;/span&gt;Zoom each corner separately. Try to increase the quality of magnification. Check wiki for more details.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Panel View 4/2/HQ</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="2">
<widget class="QCheckBox" name="disableProcessingBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Do not process any image, ignore profile and processing options.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Disable processing</string>
</property>
</widget>
</item>
<item row="4" column="1"> <item row="4" column="1">
<widget class="QCheckBox" name="maximizeStrips"> <widget class="QCheckBox" name="maximizeStrips">
<property name="toolTip"> <property name="toolTip">
@@ -578,131 +706,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="0"> <item row="8" column="1">
<widget class="QLineEdit" name="authorEdit"> <widget class="QCheckBox" name="rotateFirstBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="focusPolicy">
<enum>Qt::FocusPolicy::ClickFocus</enum>
</property>
<property name="toolTip"> <property name="toolTip">
<string>Default Author is KCC</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;When the spread splitter option is partially checked,&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Rotate Last&lt;br/&gt;&lt;/span&gt;Put the rotated 2 page spread after the split spreads.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Rotate First&lt;br/&gt;&lt;/span&gt;Put the rotated 2 page spread before the split spreads.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="placeholderText">
<string>Default Author</string>
</property>
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
</widget>
</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>
<property name="text"> <property name="text">
<string>Delete input</string> <string>Rotate First</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="6" column="0">
<widget class="QCheckBox" name="fileFusionBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Combines all selected files into a single file. (Helpful for combining chapters into volumes.)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>File Fusion</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>Rainbow blur</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QCheckBox" name="chunkSizeCheckBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700; text-decoration: underline;&quot;&gt;Unchecked&lt;br/&gt;&lt;/span&gt;Maximal output file size is 100 MB for Webtoon, 400 MB for others before split occurs.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700; text-decoration: underline;&quot;&gt;Checked&lt;/span&gt;&lt;br/&gt;Output file size specified in &amp;quot;Chunk size MB&amp;quot; before split occurs.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Chunk size</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QCheckBox" name="comicinfoTitleBox">
<property name="toolTip">
<string>Write Title from ComicInfo.xml</string>
</property>
<property name="text">
<string>ComicInfo Title</string>
</property> </property>
</widget> </widget>
</item> </item>

View File

@@ -40,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
@@ -278,6 +278,8 @@ class WorkerThread(QThread):
options.filefusion = False options.filefusion = False
if GUI.noRotateBox.isChecked(): if GUI.noRotateBox.isChecked():
options.norotate = True options.norotate = True
if GUI.rotateFirstBox.isChecked():
options.rotatefirst = True
if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked: if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked:
options.forcepng = True options.forcepng = True
elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked: elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked:
@@ -881,6 +883,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'fileFusionBox': GUI.fileFusionBox.checkState().value, 'fileFusionBox': GUI.fileFusionBox.checkState().value,
'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState().value, 'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState().value,
'noRotateBox': GUI.noRotateBox.checkState().value, 'noRotateBox': GUI.noRotateBox.checkState().value,
'rotateFirstBox': GUI.rotateFirstBox.checkState().value,
'maximizeStrips': GUI.maximizeStrips.checkState().value, 'maximizeStrips': GUI.maximizeStrips.checkState().value,
'gammaSlider': float(self.gammaValue) * 100, 'gammaSlider': float(self.gammaValue) * 100,
'chunkSizeCheckBox': GUI.chunkSizeCheckBox.checkState().value, 'chunkSizeCheckBox': GUI.chunkSizeCheckBox.checkState().value,
@@ -1166,7 +1169,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'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')

View File

@@ -12228,7 +12228,7 @@ qt_resource_struct = b"\
\x00\x00\x01\x1c\x00\x00\x00\x00\x00\x01\x00\x01P\xb1\ \x00\x00\x01\x1c\x00\x00\x00\x00\x00\x01\x00\x01P\xb1\
\x00\x00\x01\x88;p\xbcJ\ \x00\x00\x01\x88;p\xbcJ\
\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\x97~\xfd]]\ \x00\x00\x01\x97\xc9|\x88\xde\
\x00\x00\x01V\x00\x00\x00\x00\x00\x01\x00\x01\x9d\x9a\ \x00\x00\x01V\x00\x00\x00\x00\x00\x01\x00\x01\x9d\x9a\
\x00\x00\x01\x88;p\xbcI\ \x00\x00\x01\x88;p\xbcI\
\x00\x00\x01\xb8\x00\x00\x00\x00\x00\x01\x00\x01\xf6n\ \x00\x00\x01\xb8\x00\x00\x00\x00\x00\x01\x00\x01\xf6n\

View File

@@ -251,65 +251,16 @@ 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.croppingBox = QCheckBox(self.optionWidget)
self.croppingBox.setObjectName(u"croppingBox")
self.croppingBox.setTristate(True)
self.gridLayout_2.addWidget(self.croppingBox, 4, 2, 1, 1)
self.mangaBox = QCheckBox(self.optionWidget)
self.mangaBox.setObjectName(u"mangaBox")
self.gridLayout_2.addWidget(self.mangaBox, 1, 0, 1, 1)
self.webtoonBox = QCheckBox(self.optionWidget)
self.webtoonBox.setObjectName(u"webtoonBox")
self.gridLayout_2.addWidget(self.webtoonBox, 2, 0, 1, 1)
self.rotateBox = QCheckBox(self.optionWidget)
self.rotateBox.setObjectName(u"rotateBox")
self.rotateBox.setTristate(True)
self.gridLayout_2.addWidget(self.rotateBox, 1, 1, 1, 1)
self.borderBox = QCheckBox(self.optionWidget)
self.borderBox.setObjectName(u"borderBox")
self.borderBox.setTristate(True)
self.gridLayout_2.addWidget(self.borderBox, 3, 0, 1, 1)
self.gammaBox = QCheckBox(self.optionWidget)
self.gammaBox.setObjectName(u"gammaBox")
self.gridLayout_2.addWidget(self.gammaBox, 2, 2, 1, 1)
self.interPanelCropBox = QCheckBox(self.optionWidget) self.interPanelCropBox = QCheckBox(self.optionWidget)
self.interPanelCropBox.setObjectName(u"interPanelCropBox") self.interPanelCropBox.setObjectName(u"interPanelCropBox")
self.interPanelCropBox.setTristate(True) self.interPanelCropBox.setTristate(True)
self.gridLayout_2.addWidget(self.interPanelCropBox, 6, 2, 1, 1) self.gridLayout_2.addWidget(self.interPanelCropBox, 6, 2, 1, 1)
self.colorBox = QCheckBox(self.optionWidget) self.mangaBox = QCheckBox(self.optionWidget)
self.colorBox.setObjectName(u"colorBox") self.mangaBox.setObjectName(u"mangaBox")
self.gridLayout_2.addWidget(self.colorBox, 3, 2, 1, 1) self.gridLayout_2.addWidget(self.mangaBox, 1, 0, 1, 1)
self.qualityBox = QCheckBox(self.optionWidget)
self.qualityBox.setObjectName(u"qualityBox")
self.qualityBox.setTristate(True)
self.gridLayout_2.addWidget(self.qualityBox, 1, 2, 1, 1)
self.disableProcessingBox = QCheckBox(self.optionWidget)
self.disableProcessingBox.setObjectName(u"disableProcessingBox")
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")
@@ -323,27 +274,79 @@ class Ui_mainWindow(object):
self.gridLayout_2.addWidget(self.authorEdit, 0, 0, 1, 1) self.gridLayout_2.addWidget(self.authorEdit, 0, 0, 1, 1)
self.croppingBox = QCheckBox(self.optionWidget)
self.croppingBox.setObjectName(u"croppingBox")
self.croppingBox.setTristate(True)
self.gridLayout_2.addWidget(self.croppingBox, 4, 2, 1, 1)
self.webtoonBox = QCheckBox(self.optionWidget)
self.webtoonBox.setObjectName(u"webtoonBox")
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.deleteBox = QCheckBox(self.optionWidget) self.deleteBox = QCheckBox(self.optionWidget)
self.deleteBox.setObjectName(u"deleteBox") self.deleteBox.setObjectName(u"deleteBox")
self.gridLayout_2.addWidget(self.deleteBox, 5, 1, 1, 1) self.gridLayout_2.addWidget(self.deleteBox, 5, 1, 1, 1)
self.comicinfoTitleBox = QCheckBox(self.optionWidget)
self.comicinfoTitleBox.setObjectName(u"comicinfoTitleBox")
self.gridLayout_2.addWidget(self.comicinfoTitleBox, 7, 0, 1, 1)
self.qualityBox = QCheckBox(self.optionWidget)
self.qualityBox.setObjectName(u"qualityBox")
self.qualityBox.setTristate(True)
self.gridLayout_2.addWidget(self.qualityBox, 1, 2, 1, 1)
self.reduceRainbowBox = QCheckBox(self.optionWidget)
self.reduceRainbowBox.setObjectName(u"reduceRainbowBox")
self.gridLayout_2.addWidget(self.reduceRainbowBox, 7, 2, 1, 1)
self.noRotateBox = QCheckBox(self.optionWidget)
self.noRotateBox.setObjectName(u"noRotateBox")
self.gridLayout_2.addWidget(self.noRotateBox, 6, 1, 1, 1)
self.fileFusionBox = QCheckBox(self.optionWidget)
self.fileFusionBox.setObjectName(u"fileFusionBox")
self.gridLayout_2.addWidget(self.fileFusionBox, 6, 0, 1, 1)
self.gammaBox = QCheckBox(self.optionWidget)
self.gammaBox.setObjectName(u"gammaBox")
self.gridLayout_2.addWidget(self.gammaBox, 2, 2, 1, 1)
self.mozJpegBox = QCheckBox(self.optionWidget) self.mozJpegBox = QCheckBox(self.optionWidget)
self.mozJpegBox.setObjectName(u"mozJpegBox") self.mozJpegBox.setObjectName(u"mozJpegBox")
self.mozJpegBox.setTristate(True) self.mozJpegBox.setTristate(True)
self.gridLayout_2.addWidget(self.mozJpegBox, 4, 0, 1, 1) self.gridLayout_2.addWidget(self.mozJpegBox, 4, 0, 1, 1)
self.disableProcessingBox = QCheckBox(self.optionWidget)
self.disableProcessingBox.setObjectName(u"disableProcessingBox")
self.gridLayout_2.addWidget(self.disableProcessingBox, 5, 2, 1, 1)
self.chunkSizeCheckBox = QCheckBox(self.optionWidget)
self.chunkSizeCheckBox.setObjectName(u"chunkSizeCheckBox")
self.gridLayout_2.addWidget(self.chunkSizeCheckBox, 7, 1, 1, 1)
self.spreadShiftBox = QCheckBox(self.optionWidget) self.spreadShiftBox = QCheckBox(self.optionWidget)
self.spreadShiftBox.setObjectName(u"spreadShiftBox") self.spreadShiftBox.setObjectName(u"spreadShiftBox")
self.gridLayout_2.addWidget(self.spreadShiftBox, 5, 0, 1, 1) self.gridLayout_2.addWidget(self.spreadShiftBox, 5, 0, 1, 1)
self.fileFusionBox = QCheckBox(self.optionWidget)
self.fileFusionBox.setObjectName(u"fileFusionBox")
self.gridLayout_2.addWidget(self.fileFusionBox, 6, 0, 1, 1)
self.upscaleBox = QCheckBox(self.optionWidget) self.upscaleBox = QCheckBox(self.optionWidget)
self.upscaleBox.setObjectName(u"upscaleBox") self.upscaleBox.setObjectName(u"upscaleBox")
self.upscaleBox.setTristate(True) self.upscaleBox.setTristate(True)
@@ -355,25 +358,27 @@ class Ui_mainWindow(object):
self.gridLayout_2.addWidget(self.outputSplit, 3, 1, 1, 1) self.gridLayout_2.addWidget(self.outputSplit, 3, 1, 1, 1)
self.noRotateBox = QCheckBox(self.optionWidget) self.rotateBox = QCheckBox(self.optionWidget)
self.noRotateBox.setObjectName(u"noRotateBox") self.rotateBox.setObjectName(u"rotateBox")
self.rotateBox.setTristate(True)
self.gridLayout_2.addWidget(self.noRotateBox, 6, 1, 1, 1) self.gridLayout_2.addWidget(self.rotateBox, 1, 1, 1, 1)
self.reduceRainbowBox = QCheckBox(self.optionWidget) self.borderBox = QCheckBox(self.optionWidget)
self.reduceRainbowBox.setObjectName(u"reduceRainbowBox") self.borderBox.setObjectName(u"borderBox")
self.borderBox.setTristate(True)
self.gridLayout_2.addWidget(self.reduceRainbowBox, 7, 2, 1, 1) self.gridLayout_2.addWidget(self.borderBox, 3, 0, 1, 1)
self.chunkSizeCheckBox = QCheckBox(self.optionWidget) self.maximizeStrips = QCheckBox(self.optionWidget)
self.chunkSizeCheckBox.setObjectName(u"chunkSizeCheckBox") self.maximizeStrips.setObjectName(u"maximizeStrips")
self.gridLayout_2.addWidget(self.chunkSizeCheckBox, 7, 1, 1, 1) self.gridLayout_2.addWidget(self.maximizeStrips, 4, 1, 1, 1)
self.comicinfoTitleBox = QCheckBox(self.optionWidget) self.rotateFirstBox = QCheckBox(self.optionWidget)
self.comicinfoTitleBox.setObjectName(u"comicinfoTitleBox") self.rotateFirstBox.setObjectName(u"rotateFirstBox")
self.gridLayout_2.addWidget(self.comicinfoTitleBox, 7, 0, 1, 1) self.gridLayout_2.addWidget(self.rotateFirstBox, 8, 1, 1, 1)
self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2) self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
@@ -528,69 +533,73 @@ class Ui_mainWindow(object):
self.preserveMarginLabel.setText(QCoreApplication.translate("mainWindow", u"Preserve Margin %", None)) self.preserveMarginLabel.setText(QCoreApplication.translate("mainWindow", u"Preserve Margin %", 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)
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)) 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) #endif // QT_CONFIG(tooltip)
self.croppingBox.setText(QCoreApplication.translate("mainWindow", u"Cropping mode", None)) self.interPanelCropBox.setText(QCoreApplication.translate("mainWindow", u"Inter-panel crop", 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"Right-to-left mode", None)) self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Right-to-left mode", None))
#if QT_CONFIG(tooltip)
self.authorEdit.setToolTip(QCoreApplication.translate("mainWindow", u"Default Author is KCC", None))
#endif // QT_CONFIG(tooltip)
self.authorEdit.setPlaceholderText(QCoreApplication.translate("mainWindow", u"Default Author", 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) #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)) 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.webtoonBox.setText(QCoreApplication.translate("mainWindow", u"Webtoon mode", None)) self.webtoonBox.setText(QCoreApplication.translate("mainWindow", u"Webtoon mode", None))
#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 - Split and rotate<br/></span>Double page spreads will be displayed twice. First split and then rotated. </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)
self.rotateBox.setText(QCoreApplication.translate("mainWindow", u"Spread splitter", None))
#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))
#endif // QT_CONFIG(tooltip)
self.borderBox.setText(QCoreApplication.translate("mainWindow", u"W/B margins", 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.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) #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)) 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) #endif // QT_CONFIG(tooltip)
self.colorBox.setText(QCoreApplication.translate("mainWindow", u"Color mode", None)) self.colorBox.setText(QCoreApplication.translate("mainWindow", u"Color mode", 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.comicinfoTitleBox.setToolTip(QCoreApplication.translate("mainWindow", u"Write Title from ComicInfo.xml", None))
#endif // QT_CONFIG(tooltip)
self.comicinfoTitleBox.setText(QCoreApplication.translate("mainWindow", u"ComicInfo Title", 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)
self.qualityBox.setText(QCoreApplication.translate("mainWindow", u"Panel View 4/2/HQ", None)) self.qualityBox.setText(QCoreApplication.translate("mainWindow", u"Panel View 4/2/HQ", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.disableProcessingBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Do not process any image, ignore profile and processing options.</p></body></html>", None)) self.reduceRainbowBox.setToolTip(QCoreApplication.translate("mainWindow", u"Reduce rainbow effect on color eink by slightly blurring images", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.disableProcessingBox.setText(QCoreApplication.translate("mainWindow", u"Disable processing", None)) self.reduceRainbowBox.setText(QCoreApplication.translate("mainWindow", u"Rainbow blur", None))
#if QT_CONFIG(tooltip) #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)) self.noRotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"Do not rotate double page spreads in spread splitter option.", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.maximizeStrips.setText(QCoreApplication.translate("mainWindow", u"1x4 to 2x2 strips", None)) self.noRotateBox.setText(QCoreApplication.translate("mainWindow", u"No rotate", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.authorEdit.setToolTip(QCoreApplication.translate("mainWindow", u"Default Author is KCC", None)) self.fileFusionBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Combines all selected files into a single file. (Helpful for combining chapters into volumes.)</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.authorEdit.setPlaceholderText(QCoreApplication.translate("mainWindow", u"Default Author", None)) self.fileFusionBox.setText(QCoreApplication.translate("mainWindow", u"File Fusion", 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.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.deleteBox.setText(QCoreApplication.translate("mainWindow", u"Delete input", None)) self.gammaBox.setText(QCoreApplication.translate("mainWindow", u"Custom gamma", None))
#if QT_CONFIG(tooltip) #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)) 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) #endif // QT_CONFIG(tooltip)
self.mozJpegBox.setText(QCoreApplication.translate("mainWindow", u"JPEG/PNG/mozJpeg", None)) self.mozJpegBox.setText(QCoreApplication.translate("mainWindow", u"JPEG/PNG/mozJpeg", None))
#if QT_CONFIG(tooltip)
self.disableProcessingBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Do not process any image, ignore profile and processing options.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.disableProcessingBox.setText(QCoreApplication.translate("mainWindow", u"Disable processing", None))
#if QT_CONFIG(tooltip)
self.chunkSizeCheckBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:700; text-decoration: underline;\">Unchecked<br/></span>Maximal output file size is 100 MB for Webtoon, 400 MB for others before split occurs.</p><p><span style=\" font-weight:700; text-decoration: underline;\">Checked</span><br/>Output file size specified in &quot;Chunk size MB&quot; before split occurs.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.chunkSizeCheckBox.setText(QCoreApplication.translate("mainWindow", u"Chunk size", None))
#if QT_CONFIG(tooltip) #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)) 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) #endif // QT_CONFIG(tooltip)
self.spreadShiftBox.setText(QCoreApplication.translate("mainWindow", u"Spread shift", None)) self.spreadShiftBox.setText(QCoreApplication.translate("mainWindow", u"Spread shift", None))
#if QT_CONFIG(tooltip)
self.fileFusionBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Combines all selected files into a single file. (Helpful for combining chapters into volumes.)</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.fileFusionBox.setText(QCoreApplication.translate("mainWindow", u"File Fusion", None))
#if QT_CONFIG(tooltip) #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)) 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) #endif // QT_CONFIG(tooltip)
@@ -600,21 +609,21 @@ class Ui_mainWindow(object):
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.outputSplit.setText(QCoreApplication.translate("mainWindow", u"Output split", None)) self.outputSplit.setText(QCoreApplication.translate("mainWindow", u"Output split", 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.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 - Split and rotate<br/></span>Double page spreads will be displayed twice. First split and then rotated. </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.noRotateBox.setText(QCoreApplication.translate("mainWindow", u"No rotate", None)) self.rotateBox.setText(QCoreApplication.translate("mainWindow", u"Spread splitter", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.reduceRainbowBox.setToolTip(QCoreApplication.translate("mainWindow", u"Reduce rainbow effect on color eink by slightly blurring images", 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.reduceRainbowBox.setText(QCoreApplication.translate("mainWindow", u"Rainbow blur", None)) self.borderBox.setText(QCoreApplication.translate("mainWindow", u"W/B margins", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.chunkSizeCheckBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:700; text-decoration: underline;\">Unchecked<br/></span>Maximal output file size is 100 MB for Webtoon, 400 MB for others before split occurs.</p><p><span style=\" font-weight:700; text-decoration: underline;\">Checked</span><br/>Output file size specified in &quot;Chunk size MB&quot; before split occurs.</p></body></html>", None)) 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) #endif // QT_CONFIG(tooltip)
self.chunkSizeCheckBox.setText(QCoreApplication.translate("mainWindow", u"Chunk size", None)) self.maximizeStrips.setText(QCoreApplication.translate("mainWindow", u"1x4 to 2x2 strips", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.comicinfoTitleBox.setToolTip(QCoreApplication.translate("mainWindow", u"Write Title from ComicInfo.xml", None)) self.rotateFirstBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>When the spread splitter option is partially checked,</p><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Rotate Last<br/></span>Put the rotated 2 page spread after the split spreads.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Rotate First<br/></span>Put the rotated 2 page spread before the split spreads.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.comicinfoTitleBox.setText(QCoreApplication.translate("mainWindow", u"ComicInfo Title", None)) self.rotateFirstBox.setText(QCoreApplication.translate("mainWindow", u"Rotate First", None))
self.gammaLabel.setText(QCoreApplication.translate("mainWindow", u"Gamma: Auto", None)) self.gammaLabel.setText(QCoreApplication.translate("mainWindow", u"Gamma: Auto", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.chunkSizeWidget.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Warning: chunk size greater than default may cause<br/>performance/battery issues, especially on older devices.</p></body></html>", None)) self.chunkSizeWidget.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Warning: chunk size greater than default may cause<br/>performance/battery issues, especially on older devices.</p></body></html>", None))

View File

@@ -1,4 +1,4 @@
__version__ = '7.6.0' __version__ = '8.0.4'
__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
@@ -638,21 +639,22 @@ def imgFileProcessing(work):
workImg = image.ComicPageParser((dirpath, afile), opt) workImg = image.ComicPageParser((dirpath, afile), opt)
for i in workImg.payload: for i in workImg.payload:
img = image.ComicPage(opt, *i) img = image.ComicPage(opt, *i)
is_not_color = not opt.forcecolor or not img.color
if is_not_color:
img.convertToGrayscale()
if opt.cropping == 2 and not opt.webtoon: if opt.cropping == 2 and not opt.webtoon:
img.cropPageNumber(opt.croppingp, opt.croppingm) img.cropPageNumber(opt.croppingp, opt.croppingm)
if opt.cropping == 1 and not opt.webtoon: if opt.cropping == 1 and not opt.webtoon:
img.cropMargin(opt.croppingp, opt.croppingm) img.cropMargin(opt.croppingp, opt.croppingm)
if opt.interpanelcrop > 0: if opt.interpanelcrop > 0:
img.cropInterPanelEmptySections("horizontal" if opt.interpanelcrop == 1 else "both") img.cropInterPanelEmptySections("horizontal" if opt.interpanelcrop == 1 else "both")
img.autocontrastImage() img.gammaCorrectImage()
if is_not_color:
img.autocontrastImage()
img.resizeImage() img.resizeImage()
img.optimizeForDisplay(opt.reducerainbow) img.optimizeForDisplay(opt.reducerainbow)
if opt.forcecolor and workImg.color: if is_not_color and opt.forcepng:
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:
@@ -690,16 +692,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)
@@ -822,29 +814,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', '.avif'):
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
@@ -870,8 +881,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):
@@ -990,10 +1000,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:
@@ -1016,23 +1022,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:
@@ -1097,6 +1107,8 @@ def makeParser():
help="Shift first page to opposite side in landscape for spread alignment") help="Shift first page to opposite side in landscape for spread alignment")
output_options.add_argument("--norotate", action="store_true", dest="norotate", default=False, output_options.add_argument("--norotate", action="store_true", dest="norotate", default=False,
help="Do not rotate double page spreads in spread splitter option.") help="Do not rotate double page spreads in spread splitter option.")
output_options.add_argument("--rotatefirst", action="store_true", dest="rotatefirst", default=False,
help="Put rotated 2 page spread first in spread splitter option.")
processing_options.add_argument("-n", "--noprocessing", action="store_true", dest="noprocessing", default=False, processing_options.add_argument("-n", "--noprocessing", action="store_true", dest="noprocessing", default=False,
help="Do not modify image and ignore any profil or processing option") help="Do not modify image and ignore any profil or processing option")
@@ -1232,7 +1244,7 @@ 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':
@@ -1309,6 +1321,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,12 +69,12 @@ 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':
extraction_commands.append( extraction_commands.append(
['unar', self.filepath, '-f', '-o', targetdir] ['unar', self.filepath, '-D', '-f', '-o', targetdir]
) )
extraction_commands.reverse() extraction_commands.reverse()
@@ -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])
@@ -290,7 +267,7 @@ class ComicPage:
if 'N' in mode: if 'N' in mode:
self.targetPathOrder = '-kcc-x' self.targetPathOrder = '-kcc-x'
elif 'R' in mode: elif 'R' in mode:
self.targetPathOrder = '-kcc-d' self.targetPathOrder = '-kcc-a' if options.rotatefirst else '-kcc-d'
if not options.norotate: if not options.norotate:
self.rotated = True self.rotated = True
elif 'S1' in mode: elif 'S1' in mode:
@@ -301,6 +278,26 @@ 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 = []
@@ -344,16 +341,21 @@ class ComicPage:
image.save(targetPath, 'JPEG', optimize=1, quality=85) image.save(targetPath, 'JPEG', optimize=1, quality=85)
return targetPath return targetPath
def autocontrastImage(self): def gammaCorrectImage(self):
gamma = self.opt.gamma gamma = self.opt.gamma
if gamma < 0.1: if gamma < 0.1:
gamma = self.gamma gamma = self.gamma
if self.gamma != 1.0 and self.color: if self.gamma != 1.0 and self.color:
gamma = 1.0 gamma = 1.0
if gamma == 1.0: if gamma == 1.0:
self.image = ImageOps.autocontrast(self.image) pass
else: else:
self.image = ImageOps.autocontrast(Image.eval(self.image, lambda a: int(255 * (a / 255.) ** gamma))) self.image = Image.eval(self.image, lambda a: int(255 * (a / 255.) ** gamma))
def autocontrastImage(self):
# autocontrast on non grayscale images has unexpected results
# since it autocontrasts each color channel separately
self.image = ImageOps.autocontrast(self.image)
def convertToGrayscale(self): def convertToGrayscale(self):
self.image = self.image.convert('L') self.image = self.image.convert('L')
@@ -361,7 +363,7 @@ class ComicPage:
def quantizeImage(self): def quantizeImage(self):
# remove all color pixels from image, since colorCheck() has some tolerance # remove all color pixels from image, since colorCheck() has some tolerance
# quantize with a small number of color pixels in a mostly b/w image can have unexpected results # quantize with a small number of color pixels in a mostly b/w image can have unexpected results
self.image = self.image.convert("L").convert("RGB") self.image = self.image.convert("RGB")
palImg = Image.new('P', (1, 1)) palImg = Image.new('P', (1, 1))
palImg.putpalette(self.palette) palImg.putpalette(self.palette)
@@ -476,6 +478,9 @@ class Cover:
stroke_width=25 stroke_width=25
) )
copy.save(target, "JPEG", optimize=1, quality=85) copy.save(target, "JPEG", optimize=1, quality=85)
dot_cover = Path(target).with_stem('._' + Path(target).stem)
if os.path.exists(dot_cover):
os.remove(dot_cover)
except IOError: except IOError:
raise RuntimeError('Failed to save cover.') raise RuntimeError('Failed to save cover.')

View File

@@ -18,7 +18,6 @@
# PERFORMANCE OF THIS SOFTWARE. # PERFORMANCE OF THIS SOFTWARE.
# #
from functools import lru_cache
import os import os
from html.parser import HTMLParser from html.parser import HTMLParser
import subprocess import subprocess
@@ -49,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]
@@ -123,27 +116,14 @@ def dependencyCheck(level):
missing.append('python-slugify 1.2.1+') missing.append('python-slugify 1.2.1+')
try: try:
from PIL import __version__ as pillowVersion from PIL import __version__ as pillowVersion
if Version('5.2.0') > Version(pillowVersion): if Version('11.3.0') > Version(pillowVersion):
missing.append('Pillow 5.2.0+') missing.append('Pillow 11.3.0+')
except ImportError: except ImportError:
missing.append('Pillow 5.2.0+') missing.append('Pillow 11.3.0+')
if len(missing) > 0: if len(missing) > 0:
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, subprocess.CalledProcessError):
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

@@ -1,11 +1,11 @@
PySide6>=6.5.1 PySide6>=6.5.1
Pillow>=5.2.0 Pillow>=11.3.0
psutil>=5.9.5 psutil>=5.9.5
requests>=2.31.0 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

View File

@@ -75,7 +75,7 @@ setuptools.setup(
packages=['kindlecomicconverter'], packages=['kindlecomicconverter'],
install_requires=[ install_requires=[
'pyside6>=6.5.1', 'pyside6>=6.5.1',
'Pillow>=5.2.0', 'Pillow>=11.3.0',
'psutil>=5.9.5', 'psutil>=5.9.5',
'python-slugify>=1.2.1,<9.0.0', 'python-slugify>=1.2.1,<9.0.0',
'raven>=6.0.0', 'raven>=6.0.0',
@@ -83,7 +83,7 @@ setuptools.setup(
'mozjpeg-lossless-optimization>=1.1.2', 'mozjpeg-lossless-optimization>=1.1.2',
'natsort>=8.4.0', 'natsort>=8.4.0',
'distro', 'distro',
'numpy>=1.22.4,<2.0.0' 'numpy>=1.22.4'
], ],
classifiers=[], classifiers=[],
zip_safe=False, zip_safe=False,