1
0
mirror of https://github.com/ciromattia/kcc synced 2026-04-16 22:18:51 +00:00

Compare commits

...

35 Commits

Author SHA1 Message Date
Alex Xu
e3a3e9b3c2 bump to 9.0.0 2025-07-20 21:59:24 -07:00
Alex Xu
380dc5c42c add pymupdf requirement 2025-07-20 21:57:08 -07:00
Alex Xu
8ab7520754 nested archives are not supported 2025-07-20 13:49:18 -07:00
Alex Xu
3cd6e09bcb extract_image instead of Pixmap if possible 20x faster 2025-07-20 12:20:13 -07:00
Alex Xu
cb5f4db5c4 don't try catch so many layers of pdf 2025-07-20 12:04:25 -07:00
Its-my-right
f1ffb2c4e8 Update rainbow_artifacts_eraser.py
Enable odd width for BW image when using Rainbow Eraser by passing original image dimensions to irfft2
2025-07-20 11:16:18 -07:00
Its-my-right
26327728d0 Update rainbow_artifacts_eraser.py
add param s=luminance.shape to irfft2 to avoid dimensions error on luminance
2025-07-20 10:55:09 -07:00
Alex Xu
61bfb0a51f fix pdf resolution 2025-07-20 09:19:40 -07:00
Alex Xu
2f03119926 add mupdf perf_counters 2025-07-20 09:02:19 -07:00
Alex Xu
8f08c63f4e pdf input uses all cpu cores 2025-07-20 09:02:19 -07:00
Adrian
eb24a400b4 Improve pdf support by using mupdf (#983)
* Improve pdf support with mupdf

* parallel page ranges not pages

* fix black blank

* remove full=True

* add TODO

* fix doc close

---------

Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
2025-07-18 17:45:20 -07:00
Its-my-right
cc2eb9dcf3 Feature/rainbow eraser for color images (#1034)
* Add rainbow_artifacts_eraser helper

This helper file contains the methods necessary to perform a fourier transform on the picture, to remove frequencies responsible for rainbow artifacts on Kaleido screens, and performe the reverse fourier transform

* Replace blurring method with frequency removal method to erase rainbow effect on Kaleido 3 screens

* High performance improvements by using rfft2 instead of fft2

* Fine-tuned the settings and added the perpendicular direction for a better final rendering

The finer settings allow for more information to be retained in the final image, while still effectively removing the rainbow effect.

Adding the perpendicular direction results in a better rendering of the final image (avoiding visual artifacts related to suppression at the main angle).

* Revert the addition of perpendicular angles and lower attenuation_factor

It was a mistake to add the perpendicular angles in the previous commit: I had the rainbow effect removal process called 2 times when I did this, for testing purposes (One before downscale and one after downscale).

The proper way to call the process is only after the downscale. And in this case it is not necessary to remove frequencies along the perpendicular angles.

In the mean time, attenuation_factor=0.15 has proven to work well along a collection of testing images.

It should be my latest commit for this feature

* Also attenuate high frequencies at 45°

CFA is sometimes orientated at 135°, sometimes at 45° so until we find if there is a law depending on the screen size, e-reader model or something, the best we can do is attenuate high frequencies on those two directions

* fix imports

* Update comic2ebook.py

Calculate is_color with (opt.forcecolor and img.color)

pass is_color to img.optimizeForDisplay

* Update image.py

Remove color check condition, because now we process colored images too.

Pass is_color to erase_rainbow_artifacts

* Update rainbow_artifacts_eraser.py

Add support for colored images: Convert to YUV, extract luminance channel, do FFT -> Filter -> IFFT on luminance channel, insert back to YUV, convert back to RGB

To maximize compatibility until we know for sure the orientation of CFA for each device, filtering is now done on 135° + 45° axis

After more testing, attenuation_factor is decreased to 0.10

* Update comic2ebook.py

Rename rainbow eraser param

* Update image.py

rename rainbow eraser param

* Update KCC.ui

Rename rainbow eraser checkbox and tooltip

* Update KCC_ui.py

Rename erase rainbow checkbox and tooltip

* Update KCC_gui.py

Rename erase rainbow checkbox and option

* Update README.md

rename erase rainbow param

* Update KCC_gui.py

correct param name for eraserainbow

---------

Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
2025-07-18 10:48:56 -07:00
Alex Xu
d1a443b3d8 Update README.md 2025-07-18 10:23:36 -07:00
Alex Xu
f8c35ce634 Update README.md 2025-07-18 10:23:01 -07:00
Alex Xu
580a800d25 Update README.md 2025-07-18 10:18:01 -07:00
Alex Xu
023797f012 fix imports 2025-07-18 07:38:50 -07:00
Its-my-right
63a18627d3 Also attenuate high frequencies at 45°
CFA is sometimes orientated at 135°, sometimes at 45° so until we find if there is a law depending on the screen size, e-reader model or something, the best we can do is attenuate high frequencies on those two directions
2025-07-18 07:38:50 -07:00
Its-my-right
0d967084a0 Revert the addition of perpendicular angles and lower attenuation_factor
It was a mistake to add the perpendicular angles in the previous commit: I had the rainbow effect removal process called 2 times when I did this, for testing purposes (One before downscale and one after downscale).

The proper way to call the process is only after the downscale. And in this case it is not necessary to remove frequencies along the perpendicular angles.

In the mean time, attenuation_factor=0.15 has proven to work well along a collection of testing images.

It should be my latest commit for this feature
2025-07-18 07:38:50 -07:00
Its-my-right
2da5b11858 Fine-tuned the settings and added the perpendicular direction for a better final rendering
The finer settings allow for more information to be retained in the final image, while still effectively removing the rainbow effect.

Adding the perpendicular direction results in a better rendering of the final image (avoiding visual artifacts related to suppression at the main angle).
2025-07-18 07:38:50 -07:00
Its-my-right
f6d10337d8 High performance improvements by using rfft2 instead of fft2 2025-07-18 07:38:50 -07:00
Its-my-right
cf047ecf6f Replace blurring method with frequency removal method to erase rainbow effect on Kaleido 3 screens 2025-07-18 07:38:50 -07:00
Its-my-right
e7ee8bed9d Add rainbow_artifacts_eraser helper
This helper file contains the methods necessary to perform a fourier transform on the picture, to remove frequencies responsible for rainbow artifacts on Kaleido screens, and performe the reverse fourier transform
2025-07-18 07:38:50 -07:00
Alex Xu
9c8a1759cf update level comment 2025-07-13 22:29:11 -07:00
Alex Xu
6299754964 draft: add black point level (#1028)
* initial black point

* convert to L

* add GUI
2025-07-13 21:52:17 -07:00
Alex Xu
a3db86a29b convert to grayscale as last step 2025-07-13 15:23:54 -07:00
Alex Xu
67714a9b06 fix color autocontrast (#1026) 2025-07-11 16:28:19 -07:00
Alex Xu
95f9a3cda9 put dot_clean in sanitize 2025-07-10 23:07:15 -07:00
Alex Xu
0e12fc30c6 restore lenient ComicInfo.xml handling (#1024) 2025-07-10 13:57:29 -07:00
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
14 changed files with 747 additions and 367 deletions

View File

@@ -99,8 +99,16 @@ On Mac, right click open to get past the security warning.
For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation
## FAQ
- Should I use Calibre?
- No. Calibre doesn't properly support fixed layout EPUB/MOBI, so modifying KCC output in Calibre will break the formatting.
Viewing KCC output in Calibre will also not work properly.
On 7th gen and later Kindles running firmware 5.16.3+, you can get cover thumbnails simply by USB dropping into documents folder.
On 6th gen and older, you can get cover thumbnails by keeping Kindle plugged in during conversion.
If you are careful to not modify the file however, you can still use Calibre, but direct USB dropping is reccomended.
- 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
- 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?
- Disable Kindle dark mode
- Cannot connect Kindle Scribe or 2024+ Kindle to macOS
@@ -222,6 +230,7 @@ PROCESSING:
Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]
-g GAMMA, --gamma GAMMA
Apply gamma correction to linearize the image [Default=Auto]
--autolevel Set most common dark pixel value to be black point for leveling.
-c CROPPING, --cropping CROPPING
Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]
--cp CROPPINGP, --croppingpower CROPPINGP
@@ -255,7 +264,7 @@ OUTPUT SETTINGS:
--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.
--rotatefirst Put rotated spread first in spread splitter option.
--reducerainbow Reduce rainbow effect on color eink by slightly blurring images
--eraserainbow Erase rainbow effect on color eink screen by attenuating interfering frequencies
CUSTOM PROFILE:
--customwidth CUSTOMWIDTH

View File

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

View File

@@ -453,6 +453,39 @@
<property name="bottomMargin">
<number>0</number>
</property>
<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="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>Right-to-left mode</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="borderBox">
<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 - Autodetection&lt;br/&gt;&lt;/span&gt;The color of margins fill will be detected automatically.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - White&lt;br/&gt;&lt;/span&gt;Margins will be filled with white color.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Black&lt;br/&gt;&lt;/span&gt;Margins will be filled with black color.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>W/B margins</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="6" column="2">
<widget class="QCheckBox" name="interPanelCropBox">
<property name="toolTip">
@@ -466,13 +499,13 @@
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="mangaBox">
<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 style='white-space:pre'&gt;Enable right-to-left reading.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<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>Right-to-left mode</string>
<string>File Fusion</string>
</property>
</widget>
</item>
@@ -498,46 +531,56 @@
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QCheckBox" name="croppingBox">
<item row="8" column="1">
<widget class="QCheckBox" name="rotateFirstBox">
<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;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="text">
<string>Cropping mode</string>
<string>Rotate First</string>
</property>
</widget>
</item>
<item row="7" column="2">
<widget class="QCheckBox" name="eraseRainbowBox">
<property name="toolTip">
<string>Erase rainbow effect on color eink screen by attenuating interfering frequencies</string>
</property>
<property name="text">
<string>Rainbow eraser</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="1" column="1">
<widget class="QCheckBox" name="rotateBox">
<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 - Split&lt;br/&gt;&lt;/span&gt;Double page spreads will be cut into two separate pages.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - Split and rotate&lt;br/&gt;&lt;/span&gt;Double page spreads will be displayed twice. First split and then rotated. &lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Rotate&lt;br/&gt;&lt;/span&gt;Double page spreads will be rotated.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Spread splitter</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="webtoonBox">
<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;Enable special parsing mode for Korean Webtoons.&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;&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>Webtoon mode</string>
</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="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>
<string>Output split</string>
</property>
</widget>
</item>
@@ -564,56 +607,13 @@
</property>
</widget>
</item>
<item row="7" column="2">
<widget class="QCheckBox" name="reduceRainbowBox">
<item row="5" column="0">
<widget class="QCheckBox" name="spreadShiftBox">
<property name="toolTip">
<string>Reduce rainbow effect on color eink by slightly blurring images</string>
<string>Shift first page to opposite side in landscape for two page spread alignment</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>
<string>Spread shift</string>
</property>
</widget>
</item>
@@ -627,69 +627,33 @@
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QCheckBox" name="chunkSizeCheckBox">
<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&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>
<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>Chunk size</string>
<string>Webtoon mode</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="spreadShiftBox">
<item row="3" column="2">
<widget class="QCheckBox" name="colorBox">
<property name="toolTip">
<string>Shift first page to opposite side in landscape for two page spread alignment</string>
<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>Spread shift</string>
<string>Color mode</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="upscaleBox">
<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 - 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&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>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">
<widget class="QCheckBox" name="rotateBox">
<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 - Split&lt;br/&gt;&lt;/span&gt;Double page spreads will be cut into two separate pages.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - Split and rotate&lt;br/&gt;&lt;/span&gt;Double page spreads will be displayed twice. First split and then rotated. &lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Rotate&lt;br/&gt;&lt;/span&gt;Double page spreads will be rotated.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Spread splitter</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="borderBox">
<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 - Autodetection&lt;br/&gt;&lt;/span&gt;The color of margins fill will be detected automatically.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - White&lt;br/&gt;&lt;/span&gt;Margins will be filled with white color.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Black&lt;br/&gt;&lt;/span&gt;Margins will be filled with black color.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>W/B margins</string>
<string>Cropping mode</string>
</property>
<property name="tristate">
<bool>true</bool>
@@ -706,13 +670,59 @@
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QCheckBox" name="rotateFirstBox">
<item row="6" column="1">
<widget class="QCheckBox" name="noRotateBox">
<property name="toolTip">
<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>
<string>Do not rotate double page spreads in spread splitter option.</string>
</property>
<property name="text">
<string>Rotate First</string>
<string>No rotate</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="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="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="8" column="2">
<widget class="QCheckBox" name="autoLevelBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Set the most common dark pixel value to be the black point for leveling on a page by page basis.&lt;/p&gt;&lt;p&gt;Skipped for any images that were originally color.&lt;/p&gt;&lt;p&gt;Use only if default autocontrast still results in very gray faded blacks. &lt;/p&gt;&lt;p&gt;Reccomended to use with Custom Gamma = 1.0 (Disabled).&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Aggressive Black Point</string>
</property>
</widget>
</item>
@@ -863,7 +873,7 @@
<tabstop>chunkSizeBox</tabstop>
<tabstop>noRotateBox</tabstop>
<tabstop>interPanelCropBox</tabstop>
<tabstop>reduceRainbowBox</tabstop>
<tabstop>eraseRainbowBox</tabstop>
<tabstop>heightBox</tabstop>
<tabstop>croppingPowerSlider</tabstop>
<tabstop>editorButton</tabstop>

View File

@@ -247,6 +247,8 @@ class WorkerThread(QThread):
options.upscale = True
if GUI.gammaBox.isChecked() and float(GUI.gammaValue) > 0.09:
options.gamma = float(GUI.gammaValue)
if GUI.autoLevelBox.isChecked():
options.autolevel = True
options.cropping = GUI.croppingBox.checkState().value
if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked:
options.croppingp = float(GUI.croppingPowerValue)
@@ -260,8 +262,8 @@ class WorkerThread(QThread):
options.batchsplit = 2
if GUI.colorBox.isChecked():
options.forcecolor = True
if GUI.reduceRainbowBox.isChecked():
options.reducerainbow = True
if GUI.eraseRainbowBox.isChecked():
options.eraserainbow = True
if GUI.maximizeStrips.isChecked():
options.maximizestrips = True
if GUI.disableProcessingBox.isChecked():
@@ -863,6 +865,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'rotateBox': GUI.rotateBox.checkState().value,
'qualityBox': GUI.qualityBox.checkState().value,
'gammaBox': GUI.gammaBox.checkState().value,
'autoLevelBox': GUI.autoLevelBox.checkState().value,
'croppingBox': GUI.croppingBox.checkState().value,
'croppingPowerSlider': float(self.croppingPowerValue) * 100,
'preserveMarginBox': self.preserveMarginBox.value(),
@@ -872,7 +875,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'webtoonBox': GUI.webtoonBox.checkState().value,
'outputSplit': GUI.outputSplit.checkState().value,
'colorBox': GUI.colorBox.checkState().value,
'reduceRainbowBox': GUI.reduceRainbowBox.checkState().value,
'eraseRainbowBox': GUI.eraseRainbowBox.checkState().value,
'disableProcessingBox': GUI.disableProcessingBox.checkState().value,
'comicinfoTitleBox': GUI.comicinfoTitleBox.checkState().value,
'mozJpegBox': GUI.mozJpegBox.checkState().value,

View File

@@ -251,16 +251,32 @@ class Ui_mainWindow(object):
self.gridLayout_2 = QGridLayout(self.optionWidget)
self.gridLayout_2.setObjectName(u"gridLayout_2")
self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
self.gammaBox = QCheckBox(self.optionWidget)
self.gammaBox.setObjectName(u"gammaBox")
self.gridLayout_2.addWidget(self.gammaBox, 2, 2, 1, 1)
self.mangaBox = QCheckBox(self.optionWidget)
self.mangaBox.setObjectName(u"mangaBox")
self.gridLayout_2.addWidget(self.mangaBox, 1, 0, 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.interPanelCropBox = QCheckBox(self.optionWidget)
self.interPanelCropBox.setObjectName(u"interPanelCropBox")
self.interPanelCropBox.setTristate(True)
self.gridLayout_2.addWidget(self.interPanelCropBox, 6, 2, 1, 1)
self.mangaBox = QCheckBox(self.optionWidget)
self.mangaBox.setObjectName(u"mangaBox")
self.fileFusionBox = QCheckBox(self.optionWidget)
self.fileFusionBox.setObjectName(u"fileFusionBox")
self.gridLayout_2.addWidget(self.mangaBox, 1, 0, 1, 1)
self.gridLayout_2.addWidget(self.fileFusionBox, 6, 0, 1, 1)
self.authorEdit = QLineEdit(self.optionWidget)
self.authorEdit.setObjectName(u"authorEdit")
@@ -274,26 +290,31 @@ class Ui_mainWindow(object):
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.rotateFirstBox = QCheckBox(self.optionWidget)
self.rotateFirstBox.setObjectName(u"rotateFirstBox")
self.gridLayout_2.addWidget(self.croppingBox, 4, 2, 1, 1)
self.gridLayout_2.addWidget(self.rotateFirstBox, 8, 1, 1, 1)
self.webtoonBox = QCheckBox(self.optionWidget)
self.webtoonBox.setObjectName(u"webtoonBox")
self.eraseRainbowBox = QCheckBox(self.optionWidget)
self.eraseRainbowBox.setObjectName(u"eraseRainbowBox")
self.gridLayout_2.addWidget(self.webtoonBox, 2, 0, 1, 1)
self.gridLayout_2.addWidget(self.eraseRainbowBox, 7, 2, 1, 1)
self.colorBox = QCheckBox(self.optionWidget)
self.colorBox.setObjectName(u"colorBox")
self.chunkSizeCheckBox = QCheckBox(self.optionWidget)
self.chunkSizeCheckBox.setObjectName(u"chunkSizeCheckBox")
self.gridLayout_2.addWidget(self.colorBox, 3, 2, 1, 1)
self.gridLayout_2.addWidget(self.chunkSizeCheckBox, 7, 1, 1, 1)
self.deleteBox = QCheckBox(self.optionWidget)
self.deleteBox.setObjectName(u"deleteBox")
self.rotateBox = QCheckBox(self.optionWidget)
self.rotateBox.setObjectName(u"rotateBox")
self.rotateBox.setTristate(True)
self.gridLayout_2.addWidget(self.deleteBox, 5, 1, 1, 1)
self.gridLayout_2.addWidget(self.rotateBox, 1, 1, 1, 1)
self.outputSplit = QCheckBox(self.optionWidget)
self.outputSplit.setObjectName(u"outputSplit")
self.gridLayout_2.addWidget(self.outputSplit, 3, 1, 1, 1)
self.comicinfoTitleBox = QCheckBox(self.optionWidget)
self.comicinfoTitleBox.setObjectName(u"comicinfoTitleBox")
@@ -306,46 +327,46 @@ class Ui_mainWindow(object):
self.gridLayout_2.addWidget(self.qualityBox, 1, 2, 1, 1)
self.reduceRainbowBox = QCheckBox(self.optionWidget)
self.reduceRainbowBox.setObjectName(u"reduceRainbowBox")
self.spreadShiftBox = QCheckBox(self.optionWidget)
self.spreadShiftBox.setObjectName(u"spreadShiftBox")
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.setObjectName(u"mozJpegBox")
self.mozJpegBox.setTristate(True)
self.gridLayout_2.addWidget(self.mozJpegBox, 4, 0, 1, 1)
self.gridLayout_2.addWidget(self.spreadShiftBox, 5, 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.webtoonBox = QCheckBox(self.optionWidget)
self.webtoonBox.setObjectName(u"webtoonBox")
self.gridLayout_2.addWidget(self.chunkSizeCheckBox, 7, 1, 1, 1)
self.gridLayout_2.addWidget(self.webtoonBox, 2, 0, 1, 1)
self.spreadShiftBox = QCheckBox(self.optionWidget)
self.spreadShiftBox.setObjectName(u"spreadShiftBox")
self.colorBox = QCheckBox(self.optionWidget)
self.colorBox.setObjectName(u"colorBox")
self.gridLayout_2.addWidget(self.spreadShiftBox, 5, 0, 1, 1)
self.gridLayout_2.addWidget(self.colorBox, 3, 2, 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.maximizeStrips = QCheckBox(self.optionWidget)
self.maximizeStrips.setObjectName(u"maximizeStrips")
self.gridLayout_2.addWidget(self.maximizeStrips, 4, 1, 1, 1)
self.noRotateBox = QCheckBox(self.optionWidget)
self.noRotateBox.setObjectName(u"noRotateBox")
self.gridLayout_2.addWidget(self.noRotateBox, 6, 1, 1, 1)
self.deleteBox = QCheckBox(self.optionWidget)
self.deleteBox.setObjectName(u"deleteBox")
self.gridLayout_2.addWidget(self.deleteBox, 5, 1, 1, 1)
self.upscaleBox = QCheckBox(self.optionWidget)
self.upscaleBox.setObjectName(u"upscaleBox")
@@ -353,32 +374,16 @@ class Ui_mainWindow(object):
self.gridLayout_2.addWidget(self.upscaleBox, 2, 1, 1, 1)
self.outputSplit = QCheckBox(self.optionWidget)
self.outputSplit.setObjectName(u"outputSplit")
self.mozJpegBox = QCheckBox(self.optionWidget)
self.mozJpegBox.setObjectName(u"mozJpegBox")
self.mozJpegBox.setTristate(True)
self.gridLayout_2.addWidget(self.outputSplit, 3, 1, 1, 1)
self.gridLayout_2.addWidget(self.mozJpegBox, 4, 0, 1, 1)
self.rotateBox = QCheckBox(self.optionWidget)
self.rotateBox.setObjectName(u"rotateBox")
self.rotateBox.setTristate(True)
self.autoLevelBox = QCheckBox(self.optionWidget)
self.autoLevelBox.setObjectName(u"autoLevelBox")
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.maximizeStrips = QCheckBox(self.optionWidget)
self.maximizeStrips.setObjectName(u"maximizeStrips")
self.gridLayout_2.addWidget(self.maximizeStrips, 4, 1, 1, 1)
self.rotateFirstBox = QCheckBox(self.optionWidget)
self.rotateFirstBox.setObjectName(u"rotateFirstBox")
self.gridLayout_2.addWidget(self.rotateFirstBox, 8, 1, 1, 1)
self.gridLayout_2.addWidget(self.autoLevelBox, 8, 2, 1, 1)
self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
@@ -468,8 +473,8 @@ class Ui_mainWindow(object):
QWidget.setTabOrder(self.disableProcessingBox, self.chunkSizeBox)
QWidget.setTabOrder(self.chunkSizeBox, self.noRotateBox)
QWidget.setTabOrder(self.noRotateBox, self.interPanelCropBox)
QWidget.setTabOrder(self.interPanelCropBox, self.reduceRainbowBox)
QWidget.setTabOrder(self.reduceRainbowBox, self.heightBox)
QWidget.setTabOrder(self.interPanelCropBox, self.eraseRainbowBox)
QWidget.setTabOrder(self.eraseRainbowBox, self.heightBox)
QWidget.setTabOrder(self.heightBox, self.croppingPowerSlider)
QWidget.setTabOrder(self.croppingPowerSlider, self.editorButton)
QWidget.setTabOrder(self.editorButton, self.wikiButton)
@@ -533,33 +538,49 @@ class Ui_mainWindow(object):
self.preserveMarginLabel.setText(QCoreApplication.translate("mainWindow", u"Preserve Margin %", None))
self.croppingPowerLabel.setText(QCoreApplication.translate("mainWindow", u"Cropping power:", 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))
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.interPanelCropBox.setText(QCoreApplication.translate("mainWindow", u"Inter-panel crop", None))
self.gammaBox.setText(QCoreApplication.translate("mainWindow", u"Custom gamma", None))
#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))
#endif // QT_CONFIG(tooltip)
self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Right-to-left mode", 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.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.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)
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))
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)
self.croppingBox.setText(QCoreApplication.translate("mainWindow", u"Cropping mode", None))
self.rotateFirstBox.setText(QCoreApplication.translate("mainWindow", u"Rotate First", 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))
self.eraseRainbowBox.setToolTip(QCoreApplication.translate("mainWindow", u"Erase rainbow effect on color eink screen by attenuating interfering frequencies", None))
#endif // QT_CONFIG(tooltip)
self.webtoonBox.setText(QCoreApplication.translate("mainWindow", u"Webtoon mode", None))
self.eraseRainbowBox.setText(QCoreApplication.translate("mainWindow", u"Rainbow eraser", 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))
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.colorBox.setText(QCoreApplication.translate("mainWindow", u"Color mode", None))
self.chunkSizeCheckBox.setText(QCoreApplication.translate("mainWindow", u"Chunk size", None))
#if QT_CONFIG(tooltip)
self.deleteBox.setToolTip(QCoreApplication.translate("mainWindow", u"Delete input file(s) or directory. It's not recoverable!", 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)
self.deleteBox.setText(QCoreApplication.translate("mainWindow", u"Delete input", None))
self.rotateBox.setText(QCoreApplication.translate("mainWindow", u"Spread splitter", 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.comicinfoTitleBox.setToolTip(QCoreApplication.translate("mainWindow", u"Write Title from ComicInfo.xml", None))
#endif // QT_CONFIG(tooltip)
@@ -569,61 +590,49 @@ class Ui_mainWindow(object):
#endif // QT_CONFIG(tooltip)
self.qualityBox.setText(QCoreApplication.translate("mainWindow", u"Panel View 4/2/HQ", None))
#if QT_CONFIG(tooltip)
self.reduceRainbowBox.setToolTip(QCoreApplication.translate("mainWindow", u"Reduce rainbow effect on color eink by slightly blurring images", 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)
self.reduceRainbowBox.setText(QCoreApplication.translate("mainWindow", u"Rainbow blur", 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.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)
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.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))
self.spreadShiftBox.setText(QCoreApplication.translate("mainWindow", u"Spread shift", 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))
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.chunkSizeCheckBox.setText(QCoreApplication.translate("mainWindow", u"Chunk size", None))
self.webtoonBox.setText(QCoreApplication.translate("mainWindow", u"Webtoon mode", 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))
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.spreadShiftBox.setText(QCoreApplication.translate("mainWindow", u"Spread shift", None))
self.colorBox.setText(QCoreApplication.translate("mainWindow", u"Color mode", 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))
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.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.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))
self.croppingBox.setText(QCoreApplication.translate("mainWindow", u"Cropping 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)
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))
self.noRotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"Do not rotate double page spreads in spread splitter option.", None))
#endif // QT_CONFIG(tooltip)
self.rotateFirstBox.setText(QCoreApplication.translate("mainWindow", u"Rotate First", None))
self.noRotateBox.setText(QCoreApplication.translate("mainWindow", u"No rotate", 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.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.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.autoLevelBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Set the most common dark pixel value to be the black point for leveling on a page by page basis.</p><p>Skipped for any images that were originally color.</p><p>Use only if default autocontrast still results in very gray faded blacks. </p><p>Reccomended to use with Custom Gamma = 1.0 (Disabled).</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.autoLevelBox.setText(QCoreApplication.translate("mainWindow", u"Aggressive Black Point", None))
self.gammaLabel.setText(QCoreApplication.translate("mainWindow", u"Gamma: Auto", None))
#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))

View File

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

View File

@@ -32,7 +32,7 @@ from typing import List
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
from tempfile import mkdtemp, gettempdir, TemporaryFile
from shutil import move, copytree, rmtree, copyfile
from multiprocessing import Pool
from multiprocessing import Pool, cpu_count
from uuid import uuid4
from natsort import os_sort_keygen, os_sorted
from slugify import slugify as slugify_ext
@@ -41,13 +41,14 @@ from pathlib import Path
from subprocess import STDOUT, PIPE, CalledProcessError
from psutil import virtual_memory, disk_usage
from html import escape as hescape
import pymupdf
import numpy as np
from .shared import getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run
from .comicarchive import SEVENZIP, available_archive_tools
from . import comic2panel
from . import image
from . import comicarchive
from . import pdfjpgextract
from . import dualmetafix
from . import metadata
from . import kindle
@@ -639,18 +640,24 @@ def imgFileProcessing(work):
workImg = image.ComicPageParser((dirpath, afile), opt)
for i in workImg.payload:
img = image.ComicPage(opt, *i)
is_color = (opt.forcecolor and img.color)
if opt.cropping == 2 and not opt.webtoon:
img.cropPageNumber(opt.croppingp, opt.croppingm)
if opt.cropping == 1 and not opt.webtoon:
img.cropMargin(opt.croppingp, opt.croppingm)
if opt.interpanelcrop > 0:
img.cropInterPanelEmptySections("horizontal" if opt.interpanelcrop == 1 else "both")
img.gammaCorrectImage()
img.autocontrastImage()
img.resizeImage()
img.optimizeForDisplay(opt.reducerainbow)
if opt.forcecolor and img.color:
img.optimizeForDisplay(opt.eraserainbow, is_color)
if is_color:
pass
elif opt.forcepng:
img.convertToGrayscale()
img.quantizeImage()
else:
img.convertToGrayscale()
@@ -660,6 +667,135 @@ def imgFileProcessing(work):
return str(sys.exc_info()[1]), sanitizeTrace(sys.exc_info()[2])
def render_page(vector):
"""Render a page range of a document.
Notes:
The PyMuPDF document cannot be part of the argument, because that
cannot be pickled. So we are being passed in just its filename.
This is no performance issue, because we are a separate process and
need to open the document anyway.
Any page-specific function can be processed here - rendering is just
an example - text extraction might be another.
The work must however be self-contained: no inter-process communication
or synchronization is possible with this design.
Care must also be taken with which parameters are contained in the
argument, because it will be passed in via pickling by the Pool class.
So any large objects will increase the overall duration.
Args:
vector: a list containing required parameters.
"""
# recreate the arguments
idx = vector[0] # this is the segment number we have to process
cpu = vector[1] # number of CPUs
filename = vector[2] # document filename
output_dir = vector[3]
target_height = vector[4]
with pymupdf.open(filename) as doc: # open the document
num_pages = doc.page_count # get number of pages
# pages per segment: make sure that cpu * seg_size >= num_pages!
seg_size = int(num_pages / cpu + 1)
seg_from = idx * seg_size # our first page number
seg_to = min(seg_from + seg_size, num_pages) # last page number
for i in range(seg_from, seg_to): # work through our page segment
page = doc[i]
zoom = target_height / page.rect.height
mat = pymupdf.Matrix(zoom, zoom)
# TODO: decide colorspace earlier so later color check is cheaper.
pix = page.get_pixmap(matrix=mat, colorspace='RGB', alpha=False)
pix.save(os.path.join(output_dir, "p-%i.png" % i))
print("Processed page numbers %i through %i" % (seg_from, seg_to - 1))
def extract_page(vector):
"""For pages with single image (and no text). Otherwise it's recommended to use render_page()
Notes:
The PyMuPDF document cannot be part of the argument, because that
cannot be pickled. So we are being passed in just its filename.
This is no performance issue, because we are a separate process and
need to open the document anyway.
Any page-specific function can be processed here - rendering is just
an example - text extraction might be another.
The work must however be self-contained: no inter-process communication
or synchronization is possible with this design.
Care must also be taken with which parameters are contained in the
argument, because it will be passed in via pickling by the Pool class.
So any large objects will increase the overall duration.
Args:
vector: a list containing required parameters.
"""
# recreate the arguments
idx = vector[0] # this is the segment number we have to process
cpu = vector[1] # number of CPUs
filename = vector[2] # document filename
output_dir = vector[3]
with pymupdf.open(filename) as doc: # open the document
num_pages = doc.page_count # get number of pages
# pages per segment: make sure that cpu * seg_size >= num_pages!
seg_size = int(num_pages / cpu + 1)
seg_from = idx * seg_size # our first page number
seg_to = min(seg_from + seg_size, num_pages) # last page number
for i in range(seg_from, seg_to): # work through our page segment
output_path = os.path.join(output_dir, "p-%i.png" % i)
page = doc.load_page(i)
image_list = page.get_images()
if len(image_list) > 1:
raise UserWarning("mupdf_pdf_extract_page_image() function can be used only with single image pages.")
if not image_list:
width, height = int(page.rect.width), int(page.rect.height)
blank_page = Image.new("RGB", (width, height), "white")
blank_page.save(output_path)
xref = image_list[0][0]
d = doc.extract_image(xref)
if d['cs-name'] == 'DeviceCMYK':
pix = pymupdf.Pixmap(doc, xref)
pix = pymupdf.Pixmap(pymupdf.csRGB, pix)
pix.save(output_path)
else:
with open(Path(output_path).with_suffix('.' + d['ext']), "wb") as imgout:
imgout.write(d["image"])
print("Processed page numbers %i through %i" % (seg_from, seg_to - 1))
def mupdf_pdf_process_pages_parallel(filename, output_dir, target_height):
render = False
with pymupdf.open(filename) as doc:
for page in doc:
page_text = page.get_text().strip()
if page_text != "":
render = True
break
if len(page.get_images()) > 1:
render = True
break
cpu = cpu_count()
# make vectors of arguments for the processes
vectors = [(i, cpu, filename, output_dir, target_height) for i in range(cpu)]
print("Starting %i processes for '%s'." % (cpu, filename))
start = perf_counter()
with Pool() as pool:
results = pool.map(
render_page if render else extract_page, vectors
)
end = perf_counter()
print(f"MuPDF: {end - start} sec")
def getWorkFolder(afile):
if os.path.isdir(afile):
if disk_usage(gettempdir())[2] < getDirectorySize(afile) * 2.5:
@@ -678,19 +814,37 @@ def getWorkFolder(afile):
if disk_usage(gettempdir())[2] < os.path.getsize(afile) * 2.5:
raise UserWarning("Not enough disk space to perform conversion.")
if afile.lower().endswith('.pdf'):
pdf = pdfjpgextract.PdfJpgExtract(afile)
path, njpg = pdf.extract()
workdir = path
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
path = workdir
sanitizePermissions(path)
if njpg == 0:
target_height = options.profileData[1][1]
if options.cropping == 1:
target_height = target_height + target_height*0.20 #Account for possible margin at the top and bottom
elif options.cropping == 2:
target_height = target_height + target_height*0.25 #Account for possible margin at the top and bottom with page number
try:
mupdf_pdf_process_pages_parallel(afile, workdir, target_height)
except Exception as e:
rmtree(path, True)
raise UserWarning("Failed to extract images from PDF file.")
raise UserWarning(f"Failed to extract images from PDF file. {e}")
else:
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
try:
cbx = comicarchive.ComicArchive(afile)
path = cbx.extract(workdir)
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:
rmtree(workdir, True)
raise UserWarning(e)
@@ -820,13 +974,16 @@ def removeNonImages(filetree):
for root, dirs, files in os.walk(filetree):
for name in files:
_, ext = getImageFileName(name)
if ext not in ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2'):
if 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)
if not os.listdir(Path(filetree).parent):
raise UserWarning('No images detected, nested archives are not supported.')
def sanitizeTree(filetree):
@@ -880,6 +1037,7 @@ def sanitizePermissions(filetree):
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD)
for name in dirs:
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD | S_IEXEC)
dot_clean(filetree)
def dot_clean(filetree):
@@ -1119,6 +1277,8 @@ def makeParser():
help="Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]")
processing_options.add_argument("-g", "--gamma", type=float, dest="gamma", default="0.0",
help="Apply gamma correction to linearize the image [Default=Auto]")
output_options.add_argument("--autolevel", action="store_true", dest="autolevel", default=False,
help="Set most common dark pixel value to be black point for leveling.")
processing_options.add_argument("-c", "--cropping", type=int, dest="cropping", default="2",
help="Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]")
processing_options.add_argument("--cp", "--croppingpower", type=float, dest="croppingp", default="1.0",
@@ -1135,8 +1295,8 @@ def makeParser():
help="Disable autodetection and force white borders")
processing_options.add_argument("--forcecolor", action="store_true", dest="forcecolor", default=False,
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.")
output_options.add_argument("--eraserainbow", action="store_true", dest="eraserainbow", default=False,
help="Erase rainbow effect on color eink screen by attenuating interfering frequencies")
processing_options.add_argument("--forcepng", action="store_true", dest="forcepng", default=False,
help="Create PNG files instead JPEG")
processing_options.add_argument("--mozjpeg", action="store_true", dest="mozjpeg", default=False,

View File

@@ -74,7 +74,7 @@ class ComicArchive:
if platform.system() == 'Darwin':
extraction_commands.append(
['unar', self.filepath, '-f', '-o', targetdir]
['unar', self.filepath, '-D', '-f', '-o', targetdir]
)
extraction_commands.reverse()
@@ -87,7 +87,7 @@ class ComicArchive:
for cmd in extraction_commands:
try:
subprocess_run(cmd, capture_output=True, check=True)
return targetdir
return targetdir
except FileNotFoundError:
missing.append(cmd[0])
except CalledProcessError:

View File

@@ -25,6 +25,8 @@ from pathlib import Path
from functools import cached_property
import mozjpeg_lossless_optimization
from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter, ImageDraw
from .rainbow_artifacts_eraser import erase_rainbow_artifacts
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
@@ -341,16 +343,31 @@ class ComicPage:
image.save(targetPath, 'JPEG', optimize=1, quality=85)
return targetPath
def autocontrastImage(self):
def gammaCorrectImage(self):
gamma = self.opt.gamma
if gamma < 0.1:
gamma = self.gamma
if self.gamma != 1.0 and self.color:
gamma = 1.0
if gamma == 1.0:
self.image = ImageOps.autocontrast(self.image)
pass
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):
if self.opt.autolevel and not self.color:
self.convertToGrayscale()
h = self.image.histogram()
most_common_dark_pixel_count = max(h[:64])
black_point = h.index(most_common_dark_pixel_count)
bp = black_point
self.image = self.image.point(lambda p: p if p > bp else bp)
# don't autocontrast grayscale pages that were originally color
if not self.opt.forcecolor and self.color:
return
self.image = ImageOps.autocontrast(self.image, preserve_tone=True)
def convertToGrayscale(self):
self.image = self.image.convert('L')
@@ -358,19 +375,16 @@ class ComicPage:
def quantizeImage(self):
# 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
self.image = self.image.convert("L").convert("RGB")
self.image = self.image.convert("RGB")
palImg = Image.new('P', (1, 1))
palImg.putpalette(self.palette)
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 optimizeForDisplay(self, eraserainbow, is_color):
# Erase rainbow artifacts for grayscale and color images by removing spectral frequencies that cause Moire interference with color filter array
if eraserainbow:
self.image = erase_rainbow_artifacts(self.image, is_color)
def resizeImage(self):
ratio_device = float(self.size[1]) / float(self.size[0])

View File

@@ -1,79 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2014 Ciro Mattia Gonano <ciromattia@gmail.com>
# Copyright (c) 2013-2019 Pawel Jastrzebski <pawelj@iosphe.re>
#
# Based upon the code snippet by Ned Batchelder
# (http://nedbatchelder.com/blog/200712/extracting_jpgs_from_pdfs.html)
#
# Permission to use, copy, modify, and/or distribute this software for
# any purpose with or without fee is hereby granted, provided that the
# above copyright notice and this permission notice appear in all
# copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
#
import os
from random import choice
from string import ascii_uppercase, digits
# skip stray images a few pixels in size in some PDFs
# typical images are many thousands in length
# https://github.com/ciromattia/kcc/pull/546
STRAY_IMAGE_LENGTH_THRESHOLD = 300
class PdfJpgExtract:
def __init__(self, fname):
self.fname = fname
self.filename = os.path.splitext(fname)
self.path = self.filename[0] + "-KCC-" + ''.join(choice(ascii_uppercase + digits) for _ in range(3))
def getPath(self):
return self.path
def extract(self):
pdf = open(self.fname, "rb").read()
startmark = b"\xff\xd8"
startfix = 0
endmark = b"\xff\xd9"
endfix = 2
i = 0
njpg = 0
os.makedirs(self.path)
while True:
istream = pdf.find(b"stream", i)
if istream < 0:
break
istart = pdf.find(startmark, istream, istream + 20)
if istart < 0:
i = istream + 20
continue
iend = pdf.find(b"endstream", istart)
if iend < 0:
raise Exception("Didn't find end of stream!")
iend = pdf.find(endmark, iend - 20)
if iend < 0:
raise Exception("Didn't find end of JPG!")
istart += startfix
iend += endfix
i = iend
if iend - istart < STRAY_IMAGE_LENGTH_THRESHOLD:
continue
jpg = pdf[istart:iend]
jpgfile = open(self.path + "/jpg%d.jpg" % njpg, "wb")
jpgfile.write(jpg)
jpgfile.close()
njpg += 1
return self.path, njpg

View File

@@ -0,0 +1,246 @@
import numpy as np
from PIL import Image
def fourier_transform_image(img):
"""
Memory-optimized version that modifies the array in place when possible.
"""
# Convert with minimal copy
img_array = np.asarray(img, dtype=np.float32)
# Use rfft2 if the image is real to save memory
# and computation time (approximately 2x faster)
fft_result = np.fft.rfft2(img_array)
return fft_result
def attenuate_diagonal_frequencies(fft_spectrum, freq_threshold=0.30, target_angle=135,
angle_tolerance=10, attenuation_factor=0.10):
"""
Attenuates specific frequencies in the Fourier domain (optimized version for rfft2).
Args:
fft_spectrum: Result of 2D real Fourier transform (from rfft2)
freq_threshold: Frequency threshold in cycles/pixel (default: 0.3, theoretical max: 0.5)
target_angle: Target angle in degrees (default: 135)
angle_tolerance: Angular tolerance in degrees (default: 15)
attenuation_factor: Attenuation factor (0.1 = 90% attenuation)
Returns:
np.ndarray: Modified FFT with applied attenuation (same format as input)
"""
# Get dimensions of the rfft2 result
if fft_spectrum.ndim == 2:
height, width_rfft = fft_spectrum.shape
else: # 3D array (color channels)
height, width_rfft = fft_spectrum.shape[:2]
# For rfft2, the original width is (width_rfft - 1) * 2
width_original = (width_rfft - 1) * 2
# Create frequency grids for rfft2 format
freq_y = np.fft.fftfreq(height, d=1.0)
freq_x = np.fft.rfftfreq(width_original, d=1.0) # Use rfftfreq for the X dimension
# Use broadcasting to create grids without meshgrid (more efficient)
freq_y_grid = freq_y.reshape(-1, 1) # Column
freq_x_grid = freq_x.reshape(1, -1) # Row
# Calculate squared radial frequencies (avoid sqrt)
freq_radial_sq = freq_x_grid**2 + freq_y_grid**2
freq_threshold_sq = freq_threshold**2
# Frequency condition
freq_condition = freq_radial_sq >= freq_threshold_sq
# Early exit if no frequency satisfies the condition
if not np.any(freq_condition):
return fft_spectrum
# Calculate angles only where necessary
# Use atan2 directly with broadcasting
angles_rad = np.arctan2(freq_y_grid, freq_x_grid)
# Convert to degrees and normalize in a single operation
angles_deg = np.rad2deg(angles_rad) % 360
# Calculation of complementary angle
target_angle_2 = (target_angle + 180) % 360
# Calulation of perpendicular angles (135° + 45° to maximize compatibility until we know for sure which angle configure for each device)
target_angle_3 = (target_angle + 90) % 360
target_angle_4 = (target_angle_3 + 180) % 360
# Create angular conditions in a vectorized way
angle_condition = np.zeros_like(angles_deg, dtype=bool)
# Process both angles simultaneously
for angle in [target_angle, target_angle_2, target_angle_3, target_angle_4]:
min_angle = (angle - angle_tolerance) % 360
max_angle = (angle + angle_tolerance) % 360
if min_angle > max_angle: # Interval crosses 0°
angle_condition |= (angles_deg >= min_angle) | (angles_deg <= max_angle)
else: # Normal interval
angle_condition |= (angles_deg >= min_angle) & (angles_deg <= max_angle)
# Combine conditions
combined_condition = freq_condition & angle_condition
# Apply attenuation directly (avoid creating a full mask)
if attenuation_factor == 0:
# Special case: complete suppression
if fft_spectrum.ndim == 2:
fft_spectrum[combined_condition] = 0
else: # 3D array
fft_spectrum[combined_condition, :] = 0
return fft_spectrum
elif attenuation_factor == 1:
# Special case: no attenuation
return fft_spectrum
else:
# General case: partial attenuation
if fft_spectrum.ndim == 2:
fft_spectrum[combined_condition] *= attenuation_factor
else: # 3D array
fft_spectrum[combined_condition, :] *= attenuation_factor
return fft_spectrum
def inverse_fourier_transform_image(fft_spectrum, is_color, original_shape=None):
"""
Performs an optimized inverse Fourier transform to reconstruct a PIL image.
Args:
fft_spectrum: Fourier transform result (complex array from rfft2)
is_color: Boolean indicating if the image is to be treated as color
Returns:
PIL.Image: Reconstructed image
"""
# Perform inverse Fourier transform with original shape if provided
if original_shape is not None:
img_reconstructed = np.fft.irfft2(fft_spectrum, s=original_shape)
else:
img_reconstructed = np.fft.irfft2(fft_spectrum)
# Normalize values between 0 and 255
img_reconstructed = np.clip(img_reconstructed, 0, 255)
img_reconstructed = img_reconstructed.astype(np.uint8)
# Convert to PIL image
if is_color and img_reconstructed.ndim == 3:
pil_image = Image.fromarray(img_reconstructed, mode='RGB')
else:
pil_image = Image.fromarray(img_reconstructed, mode='L')
return pil_image
def rgb_to_yuv(rgb_array):
"""
Convert RGB to YUV color space.
Y = luminance, U and V = chrominance
"""
# Coefficients for RGB to YUV conversion
rgb_to_yuv_matrix = np.array([
[0.299, 0.587, 0.114], # Y
[-0.14713, -0.28886, 0.436], # U
[0.615, -0.51499, -0.10001] # V
])
# Reshape for matrix multiplication
original_shape = rgb_array.shape
rgb_flat = rgb_array.reshape(-1, 3)
# Apply transformation
yuv_flat = rgb_flat @ rgb_to_yuv_matrix.T
# Reshape back
yuv_array = yuv_flat.reshape(original_shape)
return yuv_array
def yuv_to_rgb(yuv_array):
"""
Convert YUV to RGB color space.
"""
# Coefficients for YUV to RGB conversion
yuv_to_rgb_matrix = np.array([
[1.0, 0.0, 1.13983], # R
[1.0, -0.39465, -0.58060], # G
[1.0, 2.03211, 0.0] # B
])
# Reshape for matrix multiplication
original_shape = yuv_array.shape
yuv_flat = yuv_array.reshape(-1, 3)
# Apply transformation
rgb_flat = yuv_flat @ yuv_to_rgb_matrix.T
# Reshape back
rgb_array = rgb_flat.reshape(original_shape)
return rgb_array
def erase_rainbow_artifacts(img, is_color):
"""
Remove rainbow artifacts from grayscale or color images.
Args:
img: PIL Image (grayscale or RGB)
is_color: Boolean indicating if the image is to be treated as color
Returns:
PIL.Image: Cleaned image
"""
# Auto-detect color mode if not specified
if is_color is None:
color = img.mode in ('RGB', 'RGBA', 'L') and len(np.array(img).shape) == 3
if is_color and img.mode in ('RGB', 'RGBA'):
# Convert to RGB if needed
if img.mode == 'RGBA':
img = img.convert('RGB')
# Convert to numpy array
img_array = np.array(img, dtype=np.float32)
# Convert to YUV color space
yuv_array = rgb_to_yuv(img_array)
# Extract luminance channel (Y)
luminance = yuv_array[:, :, 0]
# Process only the luminance channel
fft_spectrum = fourier_transform_image(luminance)
clean_spectrum = attenuate_diagonal_frequencies(fft_spectrum)
clean_luminance = np.fft.irfft2(clean_spectrum, s=luminance.shape)
# Normalize and clip luminance
clean_luminance = np.clip(clean_luminance, 0, 255)
# Replace luminance in YUV array
yuv_array[:, :, 0] = clean_luminance
# Convert back to RGB
rgb_array = yuv_to_rgb(yuv_array)
rgb_array = np.clip(rgb_array, 0, 255).astype(np.uint8)
# Convert back to PIL image
clean_image = Image.fromarray(rgb_array, mode='RGB')
else:
# Grayscale processing (original behavior)
if img.mode != 'L':
img = img.convert('L')
# Get original image dimensions
original_shape = (img.height, img.width)
fft_spectrum = fourier_transform_image(img)
clean_spectrum = attenuate_diagonal_frequencies(fft_spectrum)
clean_image = inverse_fourier_transform_image(clean_spectrum, is_color, original_shape)
return clean_image

View File

@@ -116,10 +116,16 @@ def dependencyCheck(level):
missing.append('python-slugify 1.2.1+')
try:
from PIL import __version__ as pillowVersion
if Version('5.2.0') > Version(pillowVersion):
missing.append('Pillow 5.2.0+')
if Version('11.3.0') > Version(pillowVersion):
missing.append('Pillow 11.3.0+')
except ImportError:
missing.append('Pillow 5.2.0+')
missing.append('Pillow 11.3.0+')
try:
from pymupdf import __version__ as pymupdfVersion
if Version('1.26.1') > Version(pymupdfVersion):
missing.append('PyMuPDF 1.26.1+')
except ImportError:
missing.append('PyMuPDF 1.26.1+')
if len(missing) > 0:
print('ERROR: ' + ', '.join(missing) + ' is not installed!')
sys.exit(1)

View File

@@ -1,5 +1,5 @@
PySide6>=6.5.1
Pillow>=5.2.0
Pillow>=11.3.0
psutil>=5.9.5
requests>=2.31.0
python-slugify>=1.2.1
@@ -9,3 +9,4 @@ mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0
distro>=1.8.0
numpy>=1.22.4
PyMuPDF>=1.26.1

View File

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