mirror of
https://github.com/ciromattia/kcc
synced 2026-06-11 17:10:34 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 21249854b9 |
@@ -99,16 +99,8 @@ 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
|
For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation
|
||||||
|
|
||||||
## FAQ
|
## 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.
|
- 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
|
||||||
@@ -230,7 +222,6 @@ PROCESSING:
|
|||||||
Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]
|
Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]
|
||||||
-g GAMMA, --gamma GAMMA
|
-g GAMMA, --gamma GAMMA
|
||||||
Apply gamma correction to linearize the image [Default=Auto]
|
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
|
-c CROPPING, --cropping CROPPING
|
||||||
Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]
|
Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]
|
||||||
--cp CROPPINGP, --croppingpower CROPPINGP
|
--cp CROPPINGP, --croppingpower CROPPINGP
|
||||||
@@ -263,8 +254,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
|
||||||
--eraserainbow Erase rainbow effect on color eink screen by attenuating interfering frequencies
|
|
||||||
|
|
||||||
CUSTOM PROFILE:
|
CUSTOM PROFILE:
|
||||||
--customwidth CUSTOMWIDTH
|
--customwidth CUSTOMWIDTH
|
||||||
@@ -311,8 +301,6 @@ 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.
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ channels:
|
|||||||
- defaults
|
- defaults
|
||||||
dependencies:
|
dependencies:
|
||||||
- python=3.11
|
- python=3.11
|
||||||
- Pillow>=11.3.0
|
- Pillow>=5.2.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
|
||||||
|
|||||||
+155
-175
@@ -453,13 +453,16 @@
|
|||||||
<property name="bottomMargin">
|
<property name="bottomMargin">
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<item row="2" column="2">
|
<item row="4" column="2">
|
||||||
<widget class="QCheckBox" name="gammaBox">
|
<widget class="QCheckBox" name="croppingBox">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string><html><head/><body><p style='white-space:pre'>Disable automatic gamma correction.</p></body></html></string>
|
<string><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></string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Custom gamma</string>
|
<string>Cropping mode</string>
|
||||||
|
</property>
|
||||||
|
<property name="tristate">
|
||||||
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -473,6 +476,29 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QCheckBox" name="webtoonBox">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><html><head/><body><p style='white-space:pre'>Enable special parsing mode for Korean Webtoons.</p></body></html></string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Webtoon mode</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QCheckBox" name="rotateBox">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><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></string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Spread splitter</string>
|
||||||
|
</property>
|
||||||
|
<property name="tristate">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item row="3" column="0">
|
<item row="3" column="0">
|
||||||
<widget class="QCheckBox" name="borderBox">
|
<widget class="QCheckBox" name="borderBox">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
@@ -486,6 +512,16 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="2" column="2">
|
||||||
|
<widget class="QCheckBox" name="gammaBox">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><html><head/><body><p style='white-space:pre'>Disable automatic gamma correction.</p></body></html></string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Custom gamma</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item row="6" column="2">
|
<item row="6" column="2">
|
||||||
<widget class="QCheckBox" name="interPanelCropBox">
|
<widget class="QCheckBox" name="interPanelCropBox">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
@@ -499,13 +535,46 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0">
|
<item row="3" column="2">
|
||||||
<widget class="QCheckBox" name="fileFusionBox">
|
<widget class="QCheckBox" name="colorBox">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string><html><head/><body><p>Combines all selected files into a single file. (Helpful for combining chapters into volumes.)</p></body></html></string>
|
<string><html><head/><body><p style='white-space:pre'>Disable conversion to grayscale.</p></body></html></string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>File Fusion</string>
|
<string>Color mode</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="QCheckBox" name="qualityBox">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><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></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><html><head/><body><p style='white-space:pre'>Do not process any image, ignore profile and processing options.</p></body></html></string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Disable processing</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="1">
|
||||||
|
<widget class="QCheckBox" name="maximizeStrips">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><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></string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>1x4 to 2x2 strips</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -531,155 +600,6 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="8" column="1">
|
|
||||||
<widget class="QCheckBox" name="rotateFirstBox">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><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></string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<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><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></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><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></string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Spread splitter</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><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></string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Output split</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><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></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="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="5" column="2">
|
|
||||||
<widget class="QCheckBox" name="disableProcessingBox">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><html><head/><body><p style='white-space:pre'>Do not process any image, ignore profile and processing options.</p></body></html></string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Disable processing</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="0">
|
|
||||||
<widget class="QCheckBox" name="webtoonBox">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><html><head/><body><p style='white-space:pre'>Enable special parsing mode for Korean Webtoons.</p></body></html></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><html><head/><body><p style='white-space:pre'>Disable conversion to grayscale.</p></body></html></string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Color mode</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="2">
|
|
||||||
<widget class="QCheckBox" name="croppingBox">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><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></string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Cropping mode</string>
|
|
||||||
</property>
|
|
||||||
<property name="tristate">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="1">
|
|
||||||
<widget class="QCheckBox" name="maximizeStrips">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><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></string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>1x4 to 2x2 strips</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="5" column="1">
|
<item row="5" column="1">
|
||||||
<widget class="QCheckBox" name="deleteBox">
|
<widget class="QCheckBox" name="deleteBox">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
@@ -690,19 +610,6 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="1">
|
|
||||||
<widget class="QCheckBox" name="upscaleBox">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><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></string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Stretch/Upscale</string>
|
|
||||||
</property>
|
|
||||||
<property name="tristate">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="0">
|
<item row="4" column="0">
|
||||||
<widget class="QCheckBox" name="mozJpegBox">
|
<widget class="QCheckBox" name="mozJpegBox">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
@@ -716,13 +623,86 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="8" column="2">
|
<item row="5" column="0">
|
||||||
<widget class="QCheckBox" name="autoLevelBox">
|
<widget class="QCheckBox" name="spreadShiftBox">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string><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></string>
|
<string>Shift first page to opposite side in landscape for two page spread alignment</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Aggressive Black Point</string>
|
<string>Spread shift</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="6" column="0">
|
||||||
|
<widget class="QCheckBox" name="fileFusionBox">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><html><head/><body><p>Combines all selected files into a single file. (Helpful for combining chapters into volumes.)</p></body></html></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><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></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><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></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><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></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>
|
||||||
@@ -873,7 +853,7 @@
|
|||||||
<tabstop>chunkSizeBox</tabstop>
|
<tabstop>chunkSizeBox</tabstop>
|
||||||
<tabstop>noRotateBox</tabstop>
|
<tabstop>noRotateBox</tabstop>
|
||||||
<tabstop>interPanelCropBox</tabstop>
|
<tabstop>interPanelCropBox</tabstop>
|
||||||
<tabstop>eraseRainbowBox</tabstop>
|
<tabstop>reduceRainbowBox</tabstop>
|
||||||
<tabstop>heightBox</tabstop>
|
<tabstop>heightBox</tabstop>
|
||||||
<tabstop>croppingPowerSlider</tabstop>
|
<tabstop>croppingPowerSlider</tabstop>
|
||||||
<tabstop>editorButton</tabstop>
|
<tabstop>editorButton</tabstop>
|
||||||
|
|||||||
@@ -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, sanitizeTrace, walkLevel, subprocess_run
|
from .shared import HTMLStripper, available_archive_tools, 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
|
||||||
@@ -247,8 +247,6 @@ class WorkerThread(QThread):
|
|||||||
options.upscale = True
|
options.upscale = True
|
||||||
if GUI.gammaBox.isChecked() and float(GUI.gammaValue) > 0.09:
|
if GUI.gammaBox.isChecked() and float(GUI.gammaValue) > 0.09:
|
||||||
options.gamma = float(GUI.gammaValue)
|
options.gamma = float(GUI.gammaValue)
|
||||||
if GUI.autoLevelBox.isChecked():
|
|
||||||
options.autolevel = True
|
|
||||||
options.cropping = GUI.croppingBox.checkState().value
|
options.cropping = GUI.croppingBox.checkState().value
|
||||||
if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked:
|
if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked:
|
||||||
options.croppingp = float(GUI.croppingPowerValue)
|
options.croppingp = float(GUI.croppingPowerValue)
|
||||||
@@ -262,8 +260,8 @@ class WorkerThread(QThread):
|
|||||||
options.batchsplit = 2
|
options.batchsplit = 2
|
||||||
if GUI.colorBox.isChecked():
|
if GUI.colorBox.isChecked():
|
||||||
options.forcecolor = True
|
options.forcecolor = True
|
||||||
if GUI.eraseRainbowBox.isChecked():
|
if GUI.reduceRainbowBox.isChecked():
|
||||||
options.eraserainbow = True
|
options.reducerainbow = True
|
||||||
if GUI.maximizeStrips.isChecked():
|
if GUI.maximizeStrips.isChecked():
|
||||||
options.maximizestrips = True
|
options.maximizestrips = True
|
||||||
if GUI.disableProcessingBox.isChecked():
|
if GUI.disableProcessingBox.isChecked():
|
||||||
@@ -280,8 +278,6 @@ 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:
|
||||||
@@ -865,7 +861,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
'rotateBox': GUI.rotateBox.checkState().value,
|
'rotateBox': GUI.rotateBox.checkState().value,
|
||||||
'qualityBox': GUI.qualityBox.checkState().value,
|
'qualityBox': GUI.qualityBox.checkState().value,
|
||||||
'gammaBox': GUI.gammaBox.checkState().value,
|
'gammaBox': GUI.gammaBox.checkState().value,
|
||||||
'autoLevelBox': GUI.autoLevelBox.checkState().value,
|
|
||||||
'croppingBox': GUI.croppingBox.checkState().value,
|
'croppingBox': GUI.croppingBox.checkState().value,
|
||||||
'croppingPowerSlider': float(self.croppingPowerValue) * 100,
|
'croppingPowerSlider': float(self.croppingPowerValue) * 100,
|
||||||
'preserveMarginBox': self.preserveMarginBox.value(),
|
'preserveMarginBox': self.preserveMarginBox.value(),
|
||||||
@@ -875,7 +870,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
'webtoonBox': GUI.webtoonBox.checkState().value,
|
'webtoonBox': GUI.webtoonBox.checkState().value,
|
||||||
'outputSplit': GUI.outputSplit.checkState().value,
|
'outputSplit': GUI.outputSplit.checkState().value,
|
||||||
'colorBox': GUI.colorBox.checkState().value,
|
'colorBox': GUI.colorBox.checkState().value,
|
||||||
'eraseRainbowBox': GUI.eraseRainbowBox.checkState().value,
|
'reduceRainbowBox': GUI.reduceRainbowBox.checkState().value,
|
||||||
'disableProcessingBox': GUI.disableProcessingBox.checkState().value,
|
'disableProcessingBox': GUI.disableProcessingBox.checkState().value,
|
||||||
'comicinfoTitleBox': GUI.comicinfoTitleBox.checkState().value,
|
'comicinfoTitleBox': GUI.comicinfoTitleBox.checkState().value,
|
||||||
'mozJpegBox': GUI.mozJpegBox.checkState().value,
|
'mozJpegBox': GUI.mozJpegBox.checkState().value,
|
||||||
@@ -886,7 +881,6 @@ 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,
|
||||||
@@ -1172,7 +1166,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 = SEVENZIP in available_archive_tools()
|
self.sevenzip = '7z' 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')
|
||||||
|
|||||||
@@ -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\xc9|\x88\xde\
|
\x00\x00\x01\x97~\xfd]]\
|
||||||
\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\
|
||||||
|
|||||||
+128
-146
@@ -251,32 +251,65 @@ 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.gammaBox = QCheckBox(self.optionWidget)
|
self.croppingBox = QCheckBox(self.optionWidget)
|
||||||
self.gammaBox.setObjectName(u"gammaBox")
|
self.croppingBox.setObjectName(u"croppingBox")
|
||||||
|
self.croppingBox.setTristate(True)
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.gammaBox, 2, 2, 1, 1)
|
self.gridLayout_2.addWidget(self.croppingBox, 4, 2, 1, 1)
|
||||||
|
|
||||||
self.mangaBox = QCheckBox(self.optionWidget)
|
self.mangaBox = QCheckBox(self.optionWidget)
|
||||||
self.mangaBox.setObjectName(u"mangaBox")
|
self.mangaBox.setObjectName(u"mangaBox")
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.mangaBox, 1, 0, 1, 1)
|
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 = QCheckBox(self.optionWidget)
|
||||||
self.borderBox.setObjectName(u"borderBox")
|
self.borderBox.setObjectName(u"borderBox")
|
||||||
self.borderBox.setTristate(True)
|
self.borderBox.setTristate(True)
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.borderBox, 3, 0, 1, 1)
|
self.gridLayout_2.addWidget(self.borderBox, 3, 0, 1, 1)
|
||||||
|
|
||||||
|
self.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.fileFusionBox = QCheckBox(self.optionWidget)
|
self.colorBox = QCheckBox(self.optionWidget)
|
||||||
self.fileFusionBox.setObjectName(u"fileFusionBox")
|
self.colorBox.setObjectName(u"colorBox")
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.fileFusionBox, 6, 0, 1, 1)
|
self.gridLayout_2.addWidget(self.colorBox, 3, 2, 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")
|
||||||
@@ -290,100 +323,57 @@ 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.rotateFirstBox = QCheckBox(self.optionWidget)
|
|
||||||
self.rotateFirstBox.setObjectName(u"rotateFirstBox")
|
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.rotateFirstBox, 8, 1, 1, 1)
|
|
||||||
|
|
||||||
self.eraseRainbowBox = QCheckBox(self.optionWidget)
|
|
||||||
self.eraseRainbowBox.setObjectName(u"eraseRainbowBox")
|
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.eraseRainbowBox, 7, 2, 1, 1)
|
|
||||||
|
|
||||||
self.chunkSizeCheckBox = QCheckBox(self.optionWidget)
|
|
||||||
self.chunkSizeCheckBox.setObjectName(u"chunkSizeCheckBox")
|
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.chunkSizeCheckBox, 7, 1, 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.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")
|
|
||||||
|
|
||||||
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.spreadShiftBox = QCheckBox(self.optionWidget)
|
|
||||||
self.spreadShiftBox.setObjectName(u"spreadShiftBox")
|
|
||||||
|
|
||||||
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.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.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 = 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.upscaleBox = QCheckBox(self.optionWidget)
|
|
||||||
self.upscaleBox.setObjectName(u"upscaleBox")
|
|
||||||
self.upscaleBox.setTristate(True)
|
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.upscaleBox, 2, 1, 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.autoLevelBox = QCheckBox(self.optionWidget)
|
self.spreadShiftBox = QCheckBox(self.optionWidget)
|
||||||
self.autoLevelBox.setObjectName(u"autoLevelBox")
|
self.spreadShiftBox.setObjectName(u"spreadShiftBox")
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.autoLevelBox, 8, 2, 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.setObjectName(u"upscaleBox")
|
||||||
|
self.upscaleBox.setTristate(True)
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.upscaleBox, 2, 1, 1, 1)
|
||||||
|
|
||||||
|
self.outputSplit = QCheckBox(self.optionWidget)
|
||||||
|
self.outputSplit.setObjectName(u"outputSplit")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.outputSplit, 3, 1, 1, 1)
|
||||||
|
|
||||||
|
self.noRotateBox = QCheckBox(self.optionWidget)
|
||||||
|
self.noRotateBox.setObjectName(u"noRotateBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.noRotateBox, 6, 1, 1, 1)
|
||||||
|
|
||||||
|
self.reduceRainbowBox = QCheckBox(self.optionWidget)
|
||||||
|
self.reduceRainbowBox.setObjectName(u"reduceRainbowBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.reduceRainbowBox, 7, 2, 1, 1)
|
||||||
|
|
||||||
|
self.chunkSizeCheckBox = QCheckBox(self.optionWidget)
|
||||||
|
self.chunkSizeCheckBox.setObjectName(u"chunkSizeCheckBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.chunkSizeCheckBox, 7, 1, 1, 1)
|
||||||
|
|
||||||
|
self.comicinfoTitleBox = QCheckBox(self.optionWidget)
|
||||||
|
self.comicinfoTitleBox.setObjectName(u"comicinfoTitleBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.comicinfoTitleBox, 7, 0, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
|
self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
|
||||||
@@ -473,8 +463,8 @@ class Ui_mainWindow(object):
|
|||||||
QWidget.setTabOrder(self.disableProcessingBox, self.chunkSizeBox)
|
QWidget.setTabOrder(self.disableProcessingBox, self.chunkSizeBox)
|
||||||
QWidget.setTabOrder(self.chunkSizeBox, self.noRotateBox)
|
QWidget.setTabOrder(self.chunkSizeBox, self.noRotateBox)
|
||||||
QWidget.setTabOrder(self.noRotateBox, self.interPanelCropBox)
|
QWidget.setTabOrder(self.noRotateBox, self.interPanelCropBox)
|
||||||
QWidget.setTabOrder(self.interPanelCropBox, self.eraseRainbowBox)
|
QWidget.setTabOrder(self.interPanelCropBox, self.reduceRainbowBox)
|
||||||
QWidget.setTabOrder(self.eraseRainbowBox, self.heightBox)
|
QWidget.setTabOrder(self.reduceRainbowBox, self.heightBox)
|
||||||
QWidget.setTabOrder(self.heightBox, self.croppingPowerSlider)
|
QWidget.setTabOrder(self.heightBox, self.croppingPowerSlider)
|
||||||
QWidget.setTabOrder(self.croppingPowerSlider, self.editorButton)
|
QWidget.setTabOrder(self.croppingPowerSlider, self.editorButton)
|
||||||
QWidget.setTabOrder(self.editorButton, self.wikiButton)
|
QWidget.setTabOrder(self.editorButton, self.wikiButton)
|
||||||
@@ -538,101 +528,93 @@ 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.gammaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Disable automatic gamma correction.</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)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.gammaBox.setText(QCoreApplication.translate("mainWindow", u"Custom gamma", None))
|
self.croppingBox.setText(QCoreApplication.translate("mainWindow", u"Cropping mode", None))
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.mangaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable right-to-left reading.</p></body></html>", None))
|
self.mangaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable right-to-left reading.</p></body></html>", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Right-to-left mode", None))
|
self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Right-to-left mode", None))
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.borderBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Autodetection<br/></span>The color of margins fill will be detected automatically.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - White<br/></span>Margins will be filled with white color.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Black<br/></span>Margins will be filled with black color.</p></body></html>", None))
|
self.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.borderBox.setText(QCoreApplication.translate("mainWindow", u"W/B margins", None))
|
self.webtoonBox.setText(QCoreApplication.translate("mainWindow", u"Webtoon mode", 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.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.rotateFirstBox.setText(QCoreApplication.translate("mainWindow", u"Rotate First", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.eraseRainbowBox.setToolTip(QCoreApplication.translate("mainWindow", u"Erase rainbow effect on color eink screen by attenuating interfering frequencies", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.eraseRainbowBox.setText(QCoreApplication.translate("mainWindow", u"Rainbow eraser", 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 "Chunk size MB" 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.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))
|
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.rotateBox.setText(QCoreApplication.translate("mainWindow", u"Spread splitter", None))
|
self.rotateBox.setText(QCoreApplication.translate("mainWindow", u"Spread splitter", None))
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.outputSplit.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Automatic mode<br/></span>The output will be split automatically.</p><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Checked - Volume mode<br/></span>Every subdirectory will be considered as a separate volume.</p></body></html>", None))
|
self.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.outputSplit.setText(QCoreApplication.translate("mainWindow", u"Output split", None))
|
self.borderBox.setText(QCoreApplication.translate("mainWindow", u"W/B margins", None))
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.comicinfoTitleBox.setToolTip(QCoreApplication.translate("mainWindow", u"Write Title from ComicInfo.xml", 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.comicinfoTitleBox.setText(QCoreApplication.translate("mainWindow", u"ComicInfo Title", None))
|
self.gammaBox.setText(QCoreApplication.translate("mainWindow", u"Custom gamma", 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.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.qualityBox.setText(QCoreApplication.translate("mainWindow", u"Panel View 4/2/HQ", None))
|
self.interPanelCropBox.setText(QCoreApplication.translate("mainWindow", u"Inter-panel crop", None))
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.spreadShiftBox.setToolTip(QCoreApplication.translate("mainWindow", u"Shift first page to opposite side in landscape for two page spread alignment", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.spreadShiftBox.setText(QCoreApplication.translate("mainWindow", u"Spread shift", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.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.webtoonBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable special parsing mode for Korean Webtoons.</p></body></html>", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.webtoonBox.setText(QCoreApplication.translate("mainWindow", u"Webtoon mode", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
#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)
|
#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.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.croppingBox.setText(QCoreApplication.translate("mainWindow", u"Cropping mode", None))
|
self.qualityBox.setText(QCoreApplication.translate("mainWindow", u"Panel View 4/2/HQ", 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)
|
#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.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.maximizeStrips.setText(QCoreApplication.translate("mainWindow", u"1x4 to 2x2 strips", None))
|
self.maximizeStrips.setText(QCoreApplication.translate("mainWindow", u"1x4 to 2x2 strips", 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.authorEdit.setToolTip(QCoreApplication.translate("mainWindow", u"Default Author is KCC", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.noRotateBox.setText(QCoreApplication.translate("mainWindow", u"No rotate", None))
|
self.authorEdit.setPlaceholderText(QCoreApplication.translate("mainWindow", u"Default Author", 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.deleteBox.setToolTip(QCoreApplication.translate("mainWindow", u"Delete input file(s) or directory. It's not recoverable!", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.deleteBox.setText(QCoreApplication.translate("mainWindow", u"Delete input", None))
|
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)
|
#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)
|
#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))
|
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.autoLevelBox.setText(QCoreApplication.translate("mainWindow", u"Aggressive Black Point", 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)
|
||||||
|
self.upscaleBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Nothing<br/></span>Images smaller than device resolution will not be resized.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Stretching<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be not preserved.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Upscaling<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be preserved.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.upscaleBox.setText(QCoreApplication.translate("mainWindow", u"Stretch/Upscale", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.outputSplit.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Automatic mode<br/></span>The output will be split automatically.</p><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Checked - Volume mode<br/></span>Every subdirectory will be considered as a separate volume.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.outputSplit.setText(QCoreApplication.translate("mainWindow", u"Output split", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.noRotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"Do not rotate double page spreads in spread splitter option.", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.noRotateBox.setText(QCoreApplication.translate("mainWindow", u"No rotate", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.reduceRainbowBox.setToolTip(QCoreApplication.translate("mainWindow", u"Reduce rainbow effect on color eink by slightly blurring images", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.reduceRainbowBox.setText(QCoreApplication.translate("mainWindow", u"Rainbow blur", 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 "Chunk size MB" 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)
|
||||||
|
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))
|
||||||
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))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
__version__ = '9.0.0'
|
__version__ = '8.0.0'
|
||||||
__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'
|
||||||
|
|||||||
@@ -32,23 +32,21 @@ from typing import List
|
|||||||
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||||
from tempfile import mkdtemp, gettempdir, TemporaryFile
|
from tempfile import mkdtemp, gettempdir, TemporaryFile
|
||||||
from shutil import move, copytree, rmtree, copyfile
|
from shutil import move, copytree, rmtree, copyfile
|
||||||
from multiprocessing import Pool, cpu_count
|
from multiprocessing import Pool
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from natsort import os_sort_keygen, os_sorted
|
from natsort import os_sort_keygen
|
||||||
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
|
||||||
from subprocess import STDOUT, PIPE, CalledProcessError
|
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
|
||||||
import pymupdf
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from .shared import getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run
|
from .shared import available_archive_tools, 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
|
||||||
|
from . import pdfjpgextract
|
||||||
from . import dualmetafix
|
from . import dualmetafix
|
||||||
from . import metadata
|
from . import metadata
|
||||||
from . import kindle
|
from . import kindle
|
||||||
@@ -640,24 +638,18 @@ 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_color = (opt.forcecolor and img.color)
|
|
||||||
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.gammaCorrectImage()
|
|
||||||
|
|
||||||
img.autocontrastImage()
|
img.autocontrastImage()
|
||||||
img.resizeImage()
|
img.resizeImage()
|
||||||
img.optimizeForDisplay(opt.eraserainbow, is_color)
|
img.optimizeForDisplay(opt.reducerainbow)
|
||||||
|
if opt.forcecolor and img.color:
|
||||||
if is_color:
|
|
||||||
pass
|
pass
|
||||||
elif opt.forcepng:
|
elif opt.forcepng:
|
||||||
img.convertToGrayscale()
|
|
||||||
img.quantizeImage()
|
img.quantizeImage()
|
||||||
else:
|
else:
|
||||||
img.convertToGrayscale()
|
img.convertToGrayscale()
|
||||||
@@ -667,135 +659,6 @@ def imgFileProcessing(work):
|
|||||||
return str(sys.exc_info()[1]), sanitizeTrace(sys.exc_info()[2])
|
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):
|
def getWorkFolder(afile):
|
||||||
if os.path.isdir(afile):
|
if os.path.isdir(afile):
|
||||||
if disk_usage(gettempdir())[2] < getDirectorySize(afile) * 2.5:
|
if disk_usage(gettempdir())[2] < getDirectorySize(afile) * 2.5:
|
||||||
@@ -814,37 +677,19 @@ def getWorkFolder(afile):
|
|||||||
if disk_usage(gettempdir())[2] < os.path.getsize(afile) * 2.5:
|
if disk_usage(gettempdir())[2] < os.path.getsize(afile) * 2.5:
|
||||||
raise UserWarning("Not enough disk space to perform conversion.")
|
raise UserWarning("Not enough disk space to perform conversion.")
|
||||||
if afile.lower().endswith('.pdf'):
|
if afile.lower().endswith('.pdf'):
|
||||||
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
pdf = pdfjpgextract.PdfJpgExtract(afile)
|
||||||
path = workdir
|
path, njpg = pdf.extract()
|
||||||
|
workdir = path
|
||||||
sanitizePermissions(path)
|
sanitizePermissions(path)
|
||||||
target_height = options.profileData[1][1]
|
if njpg == 0:
|
||||||
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)
|
rmtree(path, True)
|
||||||
raise UserWarning(f"Failed to extract images from PDF file. {e}")
|
raise UserWarning("Failed to extract images from PDF file.")
|
||||||
else:
|
else:
|
||||||
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
||||||
try:
|
try:
|
||||||
cbx = comicarchive.ComicArchive(afile)
|
cbx = comicarchive.ComicArchive(afile)
|
||||||
path = cbx.extract(workdir)
|
path = cbx.extract(workdir)
|
||||||
sanitizePermissions(path)
|
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)
|
||||||
@@ -974,7 +819,7 @@ def removeNonImages(filetree):
|
|||||||
for root, dirs, files in os.walk(filetree):
|
for root, dirs, files in os.walk(filetree):
|
||||||
for name in files:
|
for name in files:
|
||||||
_, ext = getImageFileName(name)
|
_, ext = getImageFileName(name)
|
||||||
if ext not in ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2', '.avif'):
|
if ext not in ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2'):
|
||||||
if os.path.exists(os.path.join(root, name)):
|
if os.path.exists(os.path.join(root, name)):
|
||||||
os.remove(os.path.join(root, name))
|
os.remove(os.path.join(root, name))
|
||||||
# remove empty nested folders
|
# remove empty nested folders
|
||||||
@@ -982,36 +827,30 @@ def removeNonImages(filetree):
|
|||||||
if not files and not dirs:
|
if not files and not dirs:
|
||||||
os.rmdir(root)
|
os.rmdir(root)
|
||||||
|
|
||||||
if not os.listdir(Path(filetree).parent):
|
|
||||||
raise UserWarning('No images detected, nested archives are not supported.')
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
_, ext = getImageFileName(name)
|
_, ext = getImageFileName(name)
|
||||||
|
|
||||||
# 9999 page limit
|
# 9999 page limit
|
||||||
unique_name = f'kcc-{page:04}'
|
slugified = f'kcc-{page:04}'
|
||||||
page += 1
|
page += 1
|
||||||
|
|
||||||
newKey = os.path.join(root, unique_name + ext)
|
newKey = os.path.join(root, slugified + 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, is_natural_sorted)
|
slugified = slugify(name)
|
||||||
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
|
||||||
@@ -1037,7 +876,6 @@ 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)
|
||||||
dot_clean(filetree)
|
|
||||||
|
|
||||||
|
|
||||||
def dot_clean(filetree):
|
def dot_clean(filetree):
|
||||||
@@ -1179,27 +1017,23 @@ def createNewTome(parent):
|
|||||||
return tomePath, tomePathRoot
|
return tomePath, tomePathRoot
|
||||||
|
|
||||||
|
|
||||||
def slugify(value, is_natural_sorted):
|
def slugify(value):
|
||||||
if options.format == 'CBZ' and is_natural_sorted:
|
if options.format == 'CBZ':
|
||||||
return value
|
return value
|
||||||
if options.format != 'CBZ':
|
value = slugify_ext(value, regex_pattern=r'[^-a-z0-9_\.]+').strip('.')
|
||||||
# convert all unicode to ascii via slugify
|
value = sub(r'0*([0-9]{4,})', r'\1', sub(r'([0-9]+)', r'0000\1', value, count=2))
|
||||||
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 SEVENZIP in available_archive_tools():
|
if '7z' 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([SEVENZIP, 'a', '-tzip', zipfilename, os.path.join(basedir, "*")], capture_output=True, check=True)
|
subprocess_run(['7z', '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:
|
||||||
@@ -1264,8 +1098,6 @@ 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")
|
||||||
@@ -1277,8 +1109,6 @@ def makeParser():
|
|||||||
help="Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]")
|
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",
|
processing_options.add_argument("-g", "--gamma", type=float, dest="gamma", default="0.0",
|
||||||
help="Apply gamma correction to linearize the image [Default=Auto]")
|
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",
|
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]")
|
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",
|
processing_options.add_argument("--cp", "--croppingpower", type=float, dest="croppingp", default="1.0",
|
||||||
@@ -1295,8 +1125,8 @@ def makeParser():
|
|||||||
help="Disable autodetection and force white borders")
|
help="Disable autodetection and force white borders")
|
||||||
processing_options.add_argument("--forcecolor", action="store_true", dest="forcecolor", default=False,
|
processing_options.add_argument("--forcecolor", action="store_true", dest="forcecolor", default=False,
|
||||||
help="Don't convert images to grayscale")
|
help="Don't convert images to grayscale")
|
||||||
output_options.add_argument("--eraserainbow", action="store_true", dest="eraserainbow", default=False,
|
output_options.add_argument("--reducerainbow", action="store_true", dest="reducerainbow", default=False,
|
||||||
help="Erase rainbow effect on color eink screen by attenuating interfering frequencies")
|
help="Reduce rainbow effect on color eink by slightly blurring images.")
|
||||||
processing_options.add_argument("--forcepng", action="store_true", dest="forcepng", default=False,
|
processing_options.add_argument("--forcepng", action="store_true", dest="forcepng", default=False,
|
||||||
help="Create PNG files instead JPEG")
|
help="Create PNG files instead JPEG")
|
||||||
processing_options.add_argument("--mozjpeg", action="store_true", dest="mozjpeg", default=False,
|
processing_options.add_argument("--mozjpeg", action="store_true", dest="mozjpeg", default=False,
|
||||||
@@ -1403,7 +1233,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 SEVENZIP not in available_archive_tools():
|
if '7z' 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':
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
# PERFORMANCE OF THIS SOFTWARE.
|
# PERFORMANCE OF THIS SOFTWARE.
|
||||||
#
|
#
|
||||||
|
|
||||||
from functools import cached_property, lru_cache
|
from functools import cached_property
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import distro
|
import distro
|
||||||
@@ -28,7 +28,6 @@ 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:
|
||||||
@@ -40,7 +39,7 @@ class ComicArchive:
|
|||||||
@cached_property
|
@cached_property
|
||||||
def type(self):
|
def type(self):
|
||||||
extraction_commands = [
|
extraction_commands = [
|
||||||
[SEVENZIP, 'l', '-y', '-p1', self.filepath],
|
['7z', 'l', '-y', '-p1', self.filepath],
|
||||||
]
|
]
|
||||||
|
|
||||||
if distro.id() == 'fedora' or distro.like() == 'fedora':
|
if distro.id() == 'fedora' or distro.like() == 'fedora':
|
||||||
@@ -69,12 +68,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],
|
||||||
[SEVENZIP, 'x', '-y', '-xr!__MACOSX', '-xr!.DS_Store', '-xr!thumbs.db', '-xr!Thumbs.db', '-o' + targetdir, self.filepath],
|
['7z', '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, '-D', '-f', '-o', targetdir]
|
['unar', self.filepath, '-f', '-o', targetdir]
|
||||||
)
|
)
|
||||||
|
|
||||||
extraction_commands.reverse()
|
extraction_commands.reverse()
|
||||||
@@ -101,13 +100,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([SEVENZIP, 'a', '-y', self.filepath, sourcefile],
|
process = subprocess_run(['7z', '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([SEVENZIP, 'x', '-y', '-so', self.filepath, 'ComicInfo.xml'],
|
process = subprocess_run(['7z', '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)
|
||||||
@@ -115,16 +114,3 @@ 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
|
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ from pathlib import Path
|
|||||||
from functools import cached_property
|
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 .rainbow_artifacts_eraser import erase_rainbow_artifacts
|
|
||||||
from .page_number_crop_alg import get_bbox_crop_margin_page_number, get_bbox_crop_margin
|
from .page_number_crop_alg import get_bbox_crop_margin_page_number, get_bbox_crop_margin
|
||||||
from .inter_panel_crop_alg import crop_empty_inter_panel
|
from .inter_panel_crop_alg import crop_empty_inter_panel
|
||||||
|
|
||||||
@@ -269,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-a' if options.rotatefirst else '-kcc-d'
|
self.targetPathOrder = '-kcc-d'
|
||||||
if not options.norotate:
|
if not options.norotate:
|
||||||
self.rotated = True
|
self.rotated = True
|
||||||
elif 'S1' in mode:
|
elif 'S1' in mode:
|
||||||
@@ -343,31 +341,16 @@ class ComicPage:
|
|||||||
image.save(targetPath, 'JPEG', optimize=1, quality=85)
|
image.save(targetPath, 'JPEG', optimize=1, quality=85)
|
||||||
return targetPath
|
return targetPath
|
||||||
|
|
||||||
def gammaCorrectImage(self):
|
def autocontrastImage(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:
|
||||||
pass
|
self.image = ImageOps.autocontrast(self.image)
|
||||||
else:
|
else:
|
||||||
self.image = Image.eval(self.image, lambda a: int(255 * (a / 255.) ** gamma))
|
self.image = ImageOps.autocontrast(Image.eval(self.image, lambda a: int(255 * (a / 255.) ** gamma)))
|
||||||
|
|
||||||
def 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):
|
def convertToGrayscale(self):
|
||||||
self.image = self.image.convert('L')
|
self.image = self.image.convert('L')
|
||||||
@@ -375,16 +358,19 @@ 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("RGB")
|
self.image = self.image.convert("L").convert("RGB")
|
||||||
|
|
||||||
palImg = Image.new('P', (1, 1))
|
palImg = Image.new('P', (1, 1))
|
||||||
palImg.putpalette(self.palette)
|
palImg.putpalette(self.palette)
|
||||||
self.image = self.image.quantize(palette=palImg)
|
self.image = self.image.quantize(palette=palImg)
|
||||||
|
|
||||||
def optimizeForDisplay(self, eraserainbow, is_color):
|
def optimizeForDisplay(self, reducerainbow):
|
||||||
# Erase rainbow artifacts for grayscale and color images by removing spectral frequencies that cause Moire interference with color filter array
|
# Reduce rainbow artifacts for grayscale images by breaking up dither patterns that cause Moire interference with color filter array
|
||||||
if eraserainbow:
|
if reducerainbow and not self.color:
|
||||||
self.image = erase_rainbow_artifacts(self.image, is_color)
|
unsharpFilter = ImageFilter.UnsharpMask(radius=1, percent=100)
|
||||||
|
self.image = self.image.filter(unsharpFilter)
|
||||||
|
self.image = self.image.filter(ImageFilter.BoxBlur(1.0))
|
||||||
|
self.image = self.image.filter(unsharpFilter)
|
||||||
|
|
||||||
def resizeImage(self):
|
def resizeImage(self):
|
||||||
ratio_device = float(self.size[1]) / float(self.size[0])
|
ratio_device = float(self.size[1]) / float(self.size[0])
|
||||||
@@ -487,9 +473,6 @@ 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.')
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# -*- 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
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
# 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
|
||||||
@@ -116,20 +117,27 @@ 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('11.3.0') > Version(pillowVersion):
|
if Version('5.2.0') > Version(pillowVersion):
|
||||||
missing.append('Pillow 11.3.0+')
|
missing.append('Pillow 5.2.0+')
|
||||||
except ImportError:
|
except ImportError:
|
||||||
missing.append('Pillow 11.3.0+')
|
missing.append('Pillow 5.2.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:
|
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)
|
||||||
|
|||||||
+1
-2
@@ -1,5 +1,5 @@
|
|||||||
PySide6>=6.5.1
|
PySide6>=6.5.1
|
||||||
Pillow>=11.3.0
|
Pillow>=5.2.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
|
||||||
@@ -9,4 +9,3 @@ 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
|
||||||
PyMuPDF>=1.26.1
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ setuptools.setup(
|
|||||||
packages=['kindlecomicconverter'],
|
packages=['kindlecomicconverter'],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'pyside6>=6.5.1',
|
'pyside6>=6.5.1',
|
||||||
'Pillow>=11.3.0',
|
'Pillow>=5.2.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,8 +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',
|
'numpy>=1.22.4,<2.0.0'
|
||||||
'PyMuPDF>=1.26.1',
|
|
||||||
],
|
],
|
||||||
classifiers=[],
|
classifiers=[],
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
|
|||||||
Reference in New Issue
Block a user